-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add support for access/authorize and update (#392)
closes #386
- Loading branch information
1 parent
fc53a16
commit 9c8ca0b
Showing
19 changed files
with
1,178 additions
and
634 deletions.
There are no files selected for viewing
21 changes: 21 additions & 0 deletions
21
packages/access-api/migrations/0004_add_accounts_table.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
-- Migration number: 0004 2023-01-24T15:09:12.316Z | ||
CREATE TABLE | ||
IF NOT EXISTS accounts ( | ||
did TEXT NOT NULL PRIMARY KEY, | ||
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 (did) | ||
); | ||
|
||
CREATE TABLE | ||
IF NOT EXISTS delegations ( | ||
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), | ||
FOREIGN KEY (audience) REFERENCES accounts (did) ON UPDATE CASCADE ON DELETE CASCADE | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
// eslint-disable-next-line no-unused-vars | ||
import * as Ucanto from '@ucanto/interface' | ||
import { | ||
delegationsToBytes, | ||
expirationToDate, | ||
} from '@web3-storage/access/encoding' | ||
import { Kysely } from 'kysely' | ||
import { D1Dialect } from 'kysely-d1' | ||
import { GenericPlugin } from '../utils/d1.js' | ||
|
||
/** | ||
* @typedef {import('@web3-storage/access/src/types.js').DelegationRecord} DelegationRecord | ||
*/ | ||
|
||
/** | ||
* Accounts | ||
*/ | ||
export class Accounts { | ||
/** | ||
* | ||
* @param {D1Database} d1 | ||
*/ | ||
constructor(d1) { | ||
/** @type {GenericPlugin<DelegationRecord>} */ | ||
const objectPlugin = new GenericPlugin({ | ||
// eslint-disable-next-line unicorn/no-null | ||
expires_at: (v) => (typeof v === 'string' ? new Date(v) : null), | ||
inserted_at: (v) => new Date(v), | ||
updated_at: (v) => new Date(v), | ||
}) | ||
this.d1 = /** @type {Kysely<import('../bindings').D1Schema>} */ ( | ||
new Kysely({ | ||
dialect: new D1Dialect({ database: d1 }), | ||
plugins: [objectPlugin], | ||
}) | ||
) | ||
} | ||
|
||
/** | ||
* @param {Ucanto.URI<"did:">} did | ||
*/ | ||
async create(did) { | ||
const result = await this.d1 | ||
.insertInto('accounts') | ||
.values({ | ||
did, | ||
}) | ||
.onConflict((oc) => oc.column('did').doNothing()) | ||
.returning('accounts.did') | ||
.execute() | ||
return { data: result } | ||
} | ||
|
||
/** | ||
* | ||
* @param {Ucanto.Delegation} del | ||
*/ | ||
async addDelegation(del) { | ||
const result = await this.d1 | ||
.insertInto('delegations') | ||
.values({ | ||
cid: del.cid.toV1().toString(), | ||
audience: del.audience.did(), | ||
issuer: del.issuer.did(), | ||
bytes: delegationsToBytes([del]), | ||
expires_at: expirationToDate(del.expiration), | ||
}) | ||
.onConflict((oc) => oc.column('cid').doNothing()) | ||
.returningAll() | ||
.executeTakeFirst() | ||
return result | ||
} | ||
|
||
/** | ||
* @param {Ucanto.URI<"did:">} did | ||
*/ | ||
async get(did) { | ||
return await this.d1 | ||
.selectFrom('accounts') | ||
.selectAll() | ||
.where('accounts.did', '=', did) | ||
.executeTakeFirst() | ||
} | ||
|
||
/** | ||
* @param {Ucanto.URI<"did:">} did | ||
*/ | ||
async getDelegations(did) { | ||
return await this.d1 | ||
.selectFrom('delegations') | ||
.selectAll() | ||
.where('delegations.audience', '=', did) | ||
.execute() | ||
} | ||
|
||
/** | ||
* @param {string} cid | ||
*/ | ||
async getDelegationsByCid(cid) { | ||
return await this.d1 | ||
.selectFrom('delegations') | ||
.selectAll() | ||
.where('delegations.cid', '=', cid) | ||
.where((qb) => | ||
qb | ||
.where('delegations.expires_at', '>=', new Date()) | ||
// eslint-disable-next-line unicorn/no-null | ||
.orWhere('delegations.expires_at', 'is', null) | ||
) | ||
.executeTakeFirst() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
// @ts-ignore | ||
// eslint-disable-next-line no-unused-vars | ||
import * as Ucanto from '@ucanto/interface' | ||
import * as Server from '@ucanto/server' | ||
import * as Access from '@web3-storage/capabilities/access' | ||
import * as Mailto from '../utils/did-mailto.js' | ||
import * as DID from '@ipld/dag-ucan/did' | ||
import { delegationToString } from '@web3-storage/access/encoding' | ||
|
||
/** | ||
* @param {import('../bindings').RouteContext} ctx | ||
*/ | ||
export function accessAuthorizeProvider(ctx) { | ||
return Server.provide( | ||
Access.authorize, | ||
async ({ capability, invocation }) => { | ||
const session = await Access.session | ||
.invoke({ | ||
issuer: ctx.signer, | ||
audience: DID.parse(capability.nb.as), | ||
with: ctx.signer.did(), | ||
lifetimeInSeconds: 86_400 * 7, // 7 days | ||
nb: { | ||
key: capability.with, | ||
}, | ||
}) | ||
.delegate() | ||
|
||
const encoded = delegationToString(session) | ||
|
||
await ctx.models.accounts.create(capability.nb.as) | ||
|
||
const url = `${ctx.url.protocol}//${ctx.url.host}/validate-email?ucan=${encoded}&mode=session` | ||
// For testing | ||
if (ctx.config.ENV === 'test') { | ||
return url | ||
} | ||
|
||
await ctx.email.sendValidation({ | ||
to: Mailto.toEmail(capability.nb.as), | ||
url, | ||
}) | ||
} | ||
) | ||
} |
Oops, something went wrong.