Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1677177114 demo kyselisansempty #445

Closed
wants to merge 46 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
7ecd857
basic access/delegate cap
gobengo Feb 1, 2023
e1823f4
simple happy test for access/delegate
gobengo Feb 1, 2023
62770fa
add access/delegate example from spec including a non-empty nb.delega…
gobengo Feb 1, 2023
cbf3f89
access/delegate test various ways of allocating keys in the nb.delega…
gobengo Feb 2, 2023
235e46e
accesss/delegate derives checks .nb.delegations claim is subset of pr…
gobengo Feb 2, 2023
8922c0b
remove ts-ignore from access/delegate derives
gobengo Feb 2, 2023
2579081
access/delegate capParser has derives from '*' and 'access/*'
gobengo Feb 2, 2023
1e256f1
jsdoc access/delegate
gobengo Feb 2, 2023
4f1f293
simplify types in access/delegate caps
gobengo Feb 2, 2023
a6e52bf
test access/delegate malformed
gobengo Feb 2, 2023
d03e081
remove unnecessary extra set creations in capabilities access delegat…
gobengo Feb 6, 2023
edeee6b
some access/delegate capability parser mocha tests are now split into…
gobengo Feb 6, 2023
2991b71
start access/delegate invocation handler in access-api (and access-cl…
gobengo Feb 7, 2023
c960fb4
fix access-api lint
gobengo Feb 7, 2023
5955c4c
Merge branch 'main' into 1675728220-handle-access-delegate
gobengo Feb 8, 2023
02581a2
test access/delegate handler extracts proven delegations from invocat…
gobengo Feb 8, 2023
09d6a67
mv access-delegate handler to access-api/src/service/access-delegate.js
gobengo Feb 8, 2023
89830a8
access-delegate.test.js tests invoking handler directly as well as in…
gobengo Feb 9, 2023
cce52da
start delegate + claim test
gobengo Feb 9, 2023
eb6ede0
more delegate then claim test
gobengo Feb 9, 2023
9651d68
testCanDelegateThenClaim against accessHandler sans miniflare
gobengo Feb 9, 2023
71592fb
basic createAccessClaimHandler and testCanDelegateThenClaim
gobengo Feb 9, 2023
d263013
lint
gobengo Feb 9, 2023
faa9378
start DbDelegationsStorage backed by d1
gobengo Feb 9, 2023
3e14b07
test sql DelegationsStorage incl removing delegations.aud -> accounts…
gobengo Feb 10, 2023
469c2e3
Merge branch 'main' into 1675728220-handle-access-delegate
gobengo Feb 10, 2023
ea5f48b
testDelegateThenClaim using both in-memory and sqlite DelegationsStorage
gobengo Feb 10, 2023
e083496
access-api wont handle access/delegate in ENV=production until furthe…
gobengo Feb 10, 2023
c769fae
Merge branch 'main' into 1675728220-handle-access-delegate
gobengo Feb 10, 2023
efac95a
wip encoding delegations in AccessClaim result
gobengo Feb 10, 2023
701dba4
serialize access/claim claimed delegations as car bytes
gobengo Feb 11, 2023
7ac458f
access-api no longer deps on multiformats
gobengo Feb 11, 2023
c23a522
Merge branch 'main' into 1675728220-handle-access-delegate
gobengo Feb 11, 2023
0506f97
access/delegate handler denies service when with uri is not a space w…
gobengo Feb 22, 2023
66025de
test access/delegate errors InsufficientStorage if space is not regis…
gobengo Feb 22, 2023
6186822
Merge branch 'main' into 1675728220-handle-access-delegate
gobengo Feb 22, 2023
d911d71
remove misleading comment
gobengo Feb 22, 2023
de6c168
models/delegations doesnt have noop Symbol.iterator
gobengo Feb 22, 2023
421c46d
document methods on DelegationsStorage
gobengo Feb 22, 2023
79afa8f
accessClaimProvider throws if env is production
gobengo Feb 22, 2023
d8b6dae
capabilities package has AccessDelegateSuccess and AccessDelegateFailure
gobengo Feb 22, 2023
3289aa3
AccessClaim and AccessDelegate success/fail types come from @web3-sto…
gobengo Feb 22, 2023
9cb6deb
comment
gobengo Feb 22, 2023
c62a6ac
add missing await
gobengo Feb 22, 2023
052a195
fix bug in DbDelegationsStorage when push is called with empty array,…
gobengo Feb 23, 2023
3fb76a5
remove check in Spaces model for metadata 'EMPTY' value to demonstrat…
gobengo Feb 23, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-- Migration number: 0005 2023-02-09T23:48:40.469Z

/*
goal: remove the foreign key constraint on delegations.audience -> accounts.did.
We want to be able to store delegations whose audience is not an account did.

sqlite doesn't support `alter table drop constraint`.
So here we will:
* create delegations_new table without the constraint
* insert all from delegations -> delegations_new
* rename delegations_new -> delegations
*/

CREATE TABLE
IF NOT EXISTS delegations_new (
cid TEXT NOT NULL PRIMARY KEY,
bytes BLOB NOT NULL,
audience TEXT NOT NULL,
issuer TEXT NOT NULL,
expiration TEXT,
inserted_at TEXT NOT NULL DEFAULT (strftime ('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime ('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE (cid)
);

INSERT INTO delegations_new (cid, bytes, audience, issuer, expiration, inserted_at, updated_at)
SELECT cid, bytes, audience, issuer, expiration, inserted_at, updated_at FROM delegations;

DROP TABLE delegations;
ALTER TABLE delegations_new RENAME TO delegations;
19 changes: 18 additions & 1 deletion packages/access-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"preact": "^10.11.3",
"preact-render-to-string": "^5.2.6",
"qrcode": "^1.5.1",
"streaming-iterables": "^7.1.0",
"toucan-js": "^2.7.0"
},
"devDependencies": {
Expand Down Expand Up @@ -85,7 +86,23 @@
"WebSocketPair": "readonly"
},
"rules": {
"unicorn/prefer-number-properties": "off"
"unicorn/prefer-number-properties": "off",
"jsdoc/no-undefined-types": [
"error",
{
"definedTypes": [
"AsyncIterableIterator",
"Awaited",
"D1Database",
"FetchEvent",
"Iterable",
"IterableIterator",
"KVNamespace",
"PromiseLike",
"ResponseInit"
]
}
]
}
},
"eslintIgnore": [
Expand Down
2 changes: 2 additions & 0 deletions packages/access-api/src/bindings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Validations } from './models/validations.js'
import { loadConfig } from './config.js'
import { ConnectionView, Signer as EdSigner } from '@ucanto/principal/ed25519'
import { Accounts } from './models/accounts.js'
import { DelegationsStorage } from './types/delegations.js'

export {}

Expand Down Expand Up @@ -59,6 +60,7 @@ export interface RouteContext {
spaces: Spaces
validations: Validations
accounts: Accounts
delegations: DelegationsStorage
}
uploadApi: ConnectionView
}
Expand Down
137 changes: 137 additions & 0 deletions packages/access-api/src/models/delegations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import * as Ucanto from '@ucanto/interface'
import {
delegationsToBytes,
bytesToDelegations,
} from '@web3-storage/access/encoding'

/**
* @typedef {import('@web3-storage/access/src/types').DelegationTable} DelegationRow
* @typedef {Omit<DelegationRow, 'inserted_at'|'updated_at'|'expires_at'>} DelegationRowUpdate
*/

/**
* @typedef Tables
* @property {DelegationRow} delegations
*/

/**
* @typedef {import("../types/database").Database<Tables>} DelegationsDatabase
*/

/**
* DelegationsStorage that persists using SQL.
* * should work with cloudflare D1
*/
export class DbDelegationsStorage {
/** @type {DelegationsDatabase} */
#db

/**
* @param {DelegationsDatabase} db
*/
constructor(db) {
this.#db = db
// eslint-disable-next-line no-void
void (
/** @type {import('../types/delegations').DelegationsStorage} */ (this)
)
}

get length() {
return new Promise((resolve, reject) => {
this.#db
.selectFrom('delegations')
.select((e) => e.fn.count('cid').as('size'))
.executeTakeFirstOrThrow()
.then(({ size }) => {
if (typeof size === 'string') {
const sizeNumber = parseInt(size, 10)
if (isNaN(sizeNumber)) {
throw new TypeError(
`unable to determine size number of delegations table`
)
}
return sizeNumber
}
if (typeof size === 'bigint') {
if (size > Number.MAX_SAFE_INTEGER) {
throw new TypeError(`table size too big for js Number`)
}
return Number(size)
}
return size
})
.then(resolve)
.catch(reject)
})
}

/**
* store items
*
* @param {Array<Ucanto.Delegation>} delegations
* @returns {Promise<void>}
*/
push = async (...delegations) => {
if (delegations.length === 0) {
return
}
const values = delegations.map((d) => createDelegationRowUpdate(d))
await this.#db
.insertInto('delegations')
.values(values)
.onConflict((oc) => oc.column('cid').doNothing())
.executeTakeFirst()
}

/**
* iterate through all stored items
*
* @returns {AsyncIterableIterator<Ucanto.Delegation>}
*/
async *[Symbol.asyncIterator]() {
if (this.#db.canStream) {
for await (const row of this.#db
.selectFrom('delegations')
.select(['bytes'])
.stream()) {
yield rowToDelegation(row)
}
} else {
// this db doesn't support .stream() so do something worse
for (const row of await this.#db
.selectFrom('delegations')
.select(['bytes'])
.execute()) {
yield rowToDelegation(row)
}
}
}
}

/**
* @param {Pick<DelegationRow, 'bytes'>} row
* @returns {Ucanto.Delegation}
*/
function rowToDelegation(row) {
const delegations = bytesToDelegations(row.bytes)
if (delegations.length !== 1) {
throw new Error(
`unexpected number of delegations from bytes: ${delegations.length}`
)
}
return delegations[0]
}

/**
* @param {Ucanto.Delegation} d
* @returns {DelegationRowUpdate}
*/
function createDelegationRowUpdate(d) {
return {
cid: d.cid.toV1().toString(),
audience: d.audience.did(),
issuer: d.issuer.did(),
bytes: delegationsToBytes([d]),
}
}
4 changes: 3 additions & 1 deletion packages/access-api/src/models/spaces.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ export class Spaces {
constructor(d1) {
/** @type {GenericPlugin<SpaceRecord>} */
const objectPlugin = new GenericPlugin({
metadata: (v) => JSON.parse(v),
metadata: (v) => {
return JSON.parse(v)
},
inserted_at: (v) => new Date(v),
updated_at: (v) => new Date(v),
})
Expand Down
49 changes: 49 additions & 0 deletions packages/access-api/src/service/access-claim.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import * as Server from '@ucanto/server'
import { claim } from '@web3-storage/capabilities/access'
import * as Ucanto from '@ucanto/interface'
import { collect } from 'streaming-iterables'
import * as delegationsResponse from '../utils/delegations-response.js'

/**
* @typedef {import('@web3-storage/capabilities/types').AccessClaimSuccess} AccessClaimSuccess
* @typedef {import('@web3-storage/capabilities/types').AccessClaimFailure} AccessClaimFailure
*/

/**
* @callback AccessClaimHandler
* @param {Ucanto.Invocation<import('@web3-storage/capabilities/types').AccessClaim>} invocation
* @returns {Promise<Ucanto.Result<AccessClaimSuccess, AccessClaimFailure>>}
*/

/**
* @param {object} ctx
* @param {import('../types/delegations').DelegationsStorage} ctx.delegations
* @param {Pick<import('../bindings.js').RouteContext['config'], 'ENV'>} ctx.config
*/
export function accessClaimProvider(ctx) {
const handleClaimInvocation = createAccessClaimHandler(ctx)
return Server.provide(claim, async ({ invocation }) => {
// disable until hardened in test/staging
if (ctx.config.ENV === 'production') {
throw new Error(`acccess/claim invocation handling is not enabled`)
}
return handleClaimInvocation(invocation)
})
}

/**
* @param {object} options
* @param {import('../types/delegations').DelegationsStorage} options.delegations
* @returns {AccessClaimHandler}
*/
export function createAccessClaimHandler({ delegations }) {
/** @type {AccessClaimHandler} */
return async (invocation) => {
// @todo - this should filter based on invocation param
// https://github.com/web3-storage/w3protocol/issues/394
const claimed = await collect(delegations)
return {
delegations: delegationsResponse.encode(claimed),
}
}
}
107 changes: 107 additions & 0 deletions packages/access-api/src/service/access-delegate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import * as Server from '@ucanto/server'
import { delegate } from '@web3-storage/capabilities/access'
import * as Ucanto from '@ucanto/interface'
import { createDelegationsStorage } from './delegations.js'

/**
* access/delegate failure due to the 'with' resource not having
* enough storage capacity to store the delegation.
* https://github.com/web3-storage/specs/blob/7e662a2d9ada4e3fc22a7a68f84871bff0a5380c/w3-access.md?plain=1#L94
*
* Semantics inspired by https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/507
*
* @typedef {import('@web3-storage/capabilities/types').InsufficientStorage} InsufficientStorage
*/

/**
* @typedef {import('@web3-storage/capabilities/types').AccessDelegateSuccess} AccessDelegateSuccess
* @typedef {import('@web3-storage/capabilities/types').AccessDelegateFailure} AccessDelegateFailure
* @typedef {Ucanto.Result<AccessDelegateSuccess, AccessDelegateFailure>} AccessDelegateResult
*/

/**
* @param {object} ctx
* @param {import('../types/delegations').DelegationsStorage} ctx.delegations
* @param {HasStorageProvider} ctx.hasStorageProvider
*/
export function accessDelegateProvider(ctx) {
const handleInvocation = createAccessDelegateHandler(ctx)
return Server.provide(delegate, async ({ capability, invocation }) => {
return handleInvocation(
/** @type {Ucanto.Invocation<import('@web3-storage/capabilities/types').AccessDelegate>} */ (
invocation
)
)
})
}

/**
* @callback AccessDelegateHandler
* @param {Ucanto.Invocation<import('@web3-storage/capabilities/types').AccessDelegate>} invocation
* @returns {Promise<AccessDelegateResult>}
*/

/**
* @callback HasStorageProvider
* @param {Ucanto.DID<'key'>} did
* @returns {Promise<boolean>} whether the given resource has a storage provider
*/

/**
* @param {object} options
* @param {import('../types/delegations').DelegationsStorage} [options.delegations]
* @param {HasStorageProvider} [options.hasStorageProvider]
* @param {boolean} [options.allowServiceWithoutStorageProvider] - whether to allow service if the capability resource does not have a storage provider
* @returns {AccessDelegateHandler}
*/
export function createAccessDelegateHandler({
delegations = createDelegationsStorage(),
hasStorageProvider = async () => false,
allowServiceWithoutStorageProvider = false,
} = {}) {
return async (invocation) => {
const capabability = invocation.capabilities[0]
if (
!allowServiceWithoutStorageProvider &&
!(await hasStorageProvider(capabability.with))
) {
return {
name: 'InsufficientStorage',
message: `${capabability.with} has no storage provider`,
error: true,
}
}
const delegated = extractProvenDelegations(invocation)
await delegations.push(...delegated)
return {}
}
}

/**
* @param {Ucanto.Invocation<import('@web3-storage/capabilities/types').AccessDelegate>} invocation
* @returns {Iterable<Ucanto.Delegation<Ucanto.Capabilities>>}
*/
function* extractProvenDelegations({ proofs, capabilities }) {
const nbDelegations = new Set(Object.values(capabilities[0].nb.delegations))
const proofDelegations = proofs.flatMap((proof) =>
'capabilities' in proof ? [proof] : []
)
if (nbDelegations.size > proofDelegations.length) {
throw new Error(
`UnknownDelegation: nb.delegations has more delegations than proofs`
)
}
for (const delegationLink of nbDelegations) {
// @todo avoid O(m*n) check here, but not obvious how while also using full Link#equals logic
// (could be O(minimum(m,n)) if comparing CID as strings, but that might ignore same link diff multibase)
const delegationProof = proofDelegations.find((p) =>
delegationLink.equals(p.cid)
)
if (!delegationProof) {
throw new Error(
`UnknownDelegation: missing proof for delegation cid ${delegationLink}`
)
}
yield delegationProof
}
}
Loading