Skip to content

Commit

Permalink
feat: access-api uses DID env variable when building its ucanto serve…
Browse files Browse the repository at this point in the history
…r id (#275)

Motivation:
* #237

Notes
* This is intended to be safe to merge/deploy as is. The new
functionality can be opted-into via env vars
* The ucanto server will only be constructed with an opts.id that is a
signer `withDID(...)` iff `process.env.DID` is set to a DID
* If `process.env.DID` is unset, the ucanto server id signer will have a
`did()` corresponding to `config.PRIVATE_KEY` (it will be a `did:key` of
the corresponding public key)
* Before this PR, many tests of the access-api worker were configured
implicitly via dotenv with whatever the `/.env.local` file happened to
contain. I added a worker test that asserts functionality that is
dependent on the details of the env vars, so I made an affordance for
[tests to be able to configure the access-api worker with environment
variables defined in the test case
itself](https://github.com/web3-storage/w3protocol/pull/275/files#diff-e20ffc550d006747790e7b67da280446c1cd1d2d4f72ccb5df04f62cbb6423daR149).
  • Loading branch information
gobengo committed Dec 12, 2022
1 parent a4f20a9 commit 311da78
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 14 deletions.
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
25 changes: 24 additions & 1 deletion 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 @@ -29,11 +32,13 @@ export function loadConfig(env) {
}
}

const DID = env.DID
const PRIVATE_KEY = vars.PRIVATE_KEY
const signer = configureSigner({ DID, PRIVATE_KEY })
return {
DEBUG: boolValue(vars.DEBUG),
ENV: parseRuntimeEnv(vars.ENV),

PRIVATE_KEY: vars.PRIVATE_KEY,
POSTMARK_TOKEN: vars.POSTMARK_TOKEN,
SENTRY_DSN: vars.SENTRY_DSN,
LOGTAIL_TOKEN: vars.LOGTAIL_TOKEN,
Expand All @@ -49,6 +54,8 @@ export function loadConfig(env) {
// eslint-disable-next-line no-undef
COMMITHASH: ACCOUNT_COMMITHASH,

signer,

// bindings
METRICS:
/** @type {import("./bindings").AnalyticsEngine} */ (
Expand Down Expand Up @@ -105,3 +112,19 @@ 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
}
return signer.withDID(DID.match({}).from(did))
}
5 changes: 1 addition & 4 deletions packages/access-api/src/utils/context.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Signer } from '@ucanto/principal/ed25519'
import { Logging } from '@web3-storage/worker-utils/logging'
import Toucan from 'toucan-js'
import pkg from '../../package.json'
Expand Down Expand Up @@ -41,13 +40,11 @@ export function getContext(request, env, ctx) {
commit: config.COMMITHASH,
env: config.ENV,
})

const keypair = Signer.parse(config.PRIVATE_KEY)
const url = new URL(request.url)
const db = new D1QB(config.DB)
return {
log,
signer: keypair,
signer: config.signer,
config,
url,
kvs: {
Expand Down
50 changes: 50 additions & 0 deletions packages/access-api/test/config.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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: 'did:web:exampe.com',
}
const signer = configModule.configureSigner(config)
assert.ok(signer)
assert.equal(signer.did().toString(), config.DID)
const { keys } = signer.toArchive()
const didKeys = Object.keys(keys)
assert.deepEqual(didKeys, [testKeypair.public.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
23 changes: 23 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,29 @@ describe('ucan', function () {
t.deepEqual(rsp, ['test pass'])
})

test('should support ucan invoking to a did:web aud', async function () {
const serviceDidWeb = 'did:web:example.com'
const { mf, issuer, service } = await context({
environment: {
...process.env,
DID: serviceDidWeb,
},
})
const ucan = await UCAN.issue({
issuer,
audience: service.withDID(serviceDidWeb),
capabilities: [{ can: 'testing/pass', with: 'mailto:admin@dag.house' }],
})
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
2 changes: 2 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 311da78

Please sign in to comment.