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

feat: access-api uses DID env variable when building its ucanto server id #275

Merged
merged 9 commits into from
Dec 12, 2022
1 change: 1 addition & 0 deletions packages/access-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@ucanto/principal": "^4.0.2",
"@ucanto/server": "^4.0.2",
"@ucanto/transport": "^4.0.2",
"@ucanto/validator": "^4.0.2",
"@web3-storage/access": "workspace:^",
"@web3-storage/capabilities": "workspace:^",
"@web3-storage/worker-utils": "0.4.3-dev",
Expand Down
5 changes: 5 additions & 0 deletions packages/access-api/src/bindings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ export interface Env {
// vars
ENV: string
DEBUG: string
/**
* publicly advertised decentralized identifier of the running api service
* * this may be used to filter incoming ucanto invocations
*/
DID: string
// secrets
PRIVATE_KEY: string
SENTRY_DSN: string
Expand Down
38 changes: 38 additions & 0 deletions packages/access-api/src/config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { DID } from '@ucanto/validator'
import { Signer } from '@ucanto/principal/ed25519'

/**
* Loads configuration variables from the global environment and returns a JS object
* keyed by variable names.
Expand Down Expand Up @@ -49,6 +52,8 @@ export function loadConfig(env) {
// eslint-disable-next-line no-undef
COMMITHASH: ACCOUNT_COMMITHASH,

DID: env.DID,

// bindings
METRICS:
/** @type {import("./bindings").AnalyticsEngine} */ (
Expand Down Expand Up @@ -105,3 +110,36 @@ export function createAnalyticsEngine() {
_store: store,
}
}

/**
* Given a config, return a ucanto Signer object representing the service
*
* @param {object} config
* @param {string} [config.DID] - public identifier of the running service. e.g. a did:key or a did:web
* @param {string} config.PRIVATE_KEY - multiformats private key of primary signing key
*/
export function configureSigner(config) {
const signer = Signer.parse(config.PRIVATE_KEY)
const did = config.DID
if (!did) {
return signer
}
if (!isDID(did)) {
throw new Error(`Invalid DID: ${did}`)
}
return signer.withDID(did)
}

/**
* Return whether or not the provided object looks like a decentralized identifier (aka DID)
*
* @see https://www.w3.org/TR/did-core/#did-syntax
* @param {any} object
* @returns {object is `did:${string}:${string}`}
*/
function isDID(object) {
try {
return Boolean(DID.match({}).from(object))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What’s the point of catching here to then throw ? You can simply just do .from instead of whole isDID thing.

Alternatively there’s also .is method which you could instead which will return boolean

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 5184a55

} catch {}
return false
}
7 changes: 3 additions & 4 deletions packages/access-api/src/utils/context.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Signer } from '@ucanto/principal/ed25519'
import { Logging } from '@web3-storage/worker-utils/logging'
import Toucan from 'toucan-js'
import pkg from '../../package.json'
import { loadConfig } from '../config.js'
import { configureSigner, loadConfig } from '../config.js'
import { Spaces } from '../kvs/spaces.js'
import { Validations } from '../kvs/validations.js'
import { Email } from './email.js'
Expand Down Expand Up @@ -42,12 +41,12 @@ export function getContext(request, env, ctx) {
env: config.ENV,
})

const keypair = Signer.parse(config.PRIVATE_KEY)
const signer = configureSigner(config)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you do this inside the loadConfig ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 b9533c9

const url = new URL(request.url)
const db = new D1QB(config.DB)
return {
log,
signer: keypair,
signer,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
signer,
signer: Signer.parse(config.PRIVATE_KEY).withDID(config.ENV === 'production' ? 'did:web:ucan.web3.storage': `did:web:ucan-${config.ENV}.web3.storage`),

can't we just do this ? any error will be caught at dev time or CI.

Copy link
Contributor Author

@gobengo gobengo Dec 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want the advertised DID to come from env variable. A '.web3.storage' domain won't be the best choice 100% of the times, e.g. on localhost or testing various scenarios. env variable gives me flexibility to get something working. once something works/tests end to end, can narrow down the configurability.

config,
url,
kvs: {
Expand Down
47 changes: 47 additions & 0 deletions packages/access-api/test/config.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import assert from 'assert'
import * as configModule from '../src/config.js'

/** keypair that can be used for testing */
const testKeypair = {
private: {
/**
* Private key encoded as multiformats
*/
multiformats:
'MgCYWjE6vp0cn3amPan2xPO+f6EZ3I+KwuN1w2vx57vpJ9O0Bn4ci4jn8itwc121ujm7lDHkCW24LuKfZwIdmsifVysY=',
},
public: {
/**
* Public key encoded as a did:key
*/
did: 'did:key:z6MkqBzPG7oNu7At8fktasQuS7QR7Tj7CujaijPMAgzdmAxD',
},
}

describe('@web3-storage/access-api/src/config configureSigner', () => {
it('creates a signer using config.{DID,PRIVATE_KEY}', async () => {
const config = {
PRIVATE_KEY: testKeypair.private.multiformats,
DID: testKeypair.public.did,
}
const signer = configModule.configureSigner(config)
assert.ok(signer)
assert.equal(signer.did().toString(), config.DID)
})
it('errors if config.DID is provided but not a did', () => {
assert.throws(() => {
configModule.configureSigner({
DID: 'not a did',
PRIVATE_KEY: testKeypair.private.multiformats,
})
}, 'Invalid DID')
})
it('infers did from config.PRIVATE_KEY when config.DID is omitted', async () => {
const config = {
PRIVATE_KEY: testKeypair.private.multiformats,
}
const signer = configModule.configureSigner(config)
assert.ok(signer)
assert.equal(signer.did().toString(), testKeypair.public.did)
})
})
44 changes: 35 additions & 9 deletions packages/access-api/test/helpers/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,46 @@ dotenv.config({
path: path.join(__dirname, '..', '..', '..', '..', '.env.tpl'),
})

export const bindings = {
ENV: 'test',
DEBUG: 'false',
PRIVATE_KEY: process.env.PRIVATE_KEY || '',
POSTMARK_TOKEN: process.env.POSTMARK_TOKEN || '',
SENTRY_DSN: process.env.SENTRY_DSN || '',
LOGTAIL_TOKEN: process.env.LOGTAIL_TOKEN || '',
W3ACCESS_METRICS: createAnalyticsEngine(),
/**
* @typedef {Omit<import('../../src/bindings').Env, 'SPACES'|'VALIDATIONS'|'__D1_BETA__'>} AccessApiBindings - bindings object expected by access-api workers
*/

/**
* Given a map of environment vars, return a map of bindings that can be passed with access-api worker invocations.
*
* @param {{ [key: string]: string | undefined }} env - environment variables
* @returns {AccessApiBindings} - env bindings expected by access-api worker objects
*/
function createBindings(env) {
return {
ENV: 'test',
DEBUG: 'false',
DID: env.DID || '',
PRIVATE_KEY: env.PRIVATE_KEY || '',
POSTMARK_TOKEN: env.POSTMARK_TOKEN || '',
SENTRY_DSN: env.SENTRY_DSN || '',
LOGTAIL_TOKEN: env.LOGTAIL_TOKEN || '',
W3ACCESS_METRICS: createAnalyticsEngine(),
}
}

/**
* Good default bindings useful for tests - configured via process.env
*/
export const bindings = createBindings(process.env)

export const serviceAuthority = Signer.parse(bindings.PRIVATE_KEY)

export async function context() {
/**
* @param {object} [options]
* @param {Record<string,string|undefined>} options.environment - environment variables to use when configuring access-api. Defaults to process.env.
*/
export async function context(options) {
const environment = options?.environment || process.env
const principal = await Signer.generate()
const bindings = createBindings({
...environment,
})
const mf = new Miniflare({
packagePath: true,
wranglerConfigPath: true,
Expand Down
25 changes: 25 additions & 0 deletions packages/access-api/test/ucan.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,31 @@ describe('ucan', function () {
t.deepEqual(rsp, ['test pass'])
})

test('should support ucan invoking to a did:web aud', async function () {
const serviceDidWeb = 'did:web:web3.storage'
const { mf, issuer, service } = await context({
environment: {
...process.env,
PRIVATE_KEY:
'MgCYWjE6vp0cn3amPan2xPO+f6EZ3I+KwuN1w2vx57vpJ9O0Bn4ci4jn8itwc121ujm7lDHkCW24LuKfZwIdmsifVysY=',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you need a new key here ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't. Good catch. 🙏 d204331

DID: serviceDidWeb,
},
})
const ucan = await UCAN.issue({
issuer,
audience: service.withDID('did:web:web3.storage'),
capabilities: [{ can: 'testing/pass', with: 'mailto:[email protected]' }],
})
const res = await mf.dispatchFetch('http://localhost:8787/raw', {
method: 'POST',
headers: {
Authorization: `Bearer ${UCAN.format(ucan)}`,
},
})
const rsp = await res.json()
t.deepEqual(rsp, ['test pass'])
})

test('should handle exception in route handler', async function () {
const { mf, service, issuer } = ctx

Expand Down