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
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,
Copy link
Contributor

Choose a reason for hiding this comment

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

can we get the did:key and did:web from the signer instance ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

console.log('signer did', signer.did().toString())

signer did did:web:exampe.com

console.log('signer archive', signer.toArchive())

signer archive {
id: 'did:web:exampe.com',
keys: {
'did:key:z6MkqBzPG7oNu7At8fktasQuS7QR7Tj7CujaijPMAgzdmAxD': Ed25519Signer(68) [Uint8Array] [
128, 38, 22, 140, 78, 175, 167, 71, 39, 221, 169,
143, 106, 125, 177, 60, 239, 159, 232, 70, 119, 35,
226, 176, 184, 221, 112, 218, 252, 121, 238, 250, 73,
244, 237, 1, 159, 135, 34, 226, 57, 252, 138, 220,
28, 215, 109, 110, 142, 110, 229, 12, 121, 2, 91,
110, 11, 184, 167, 217, 192, 135, 102, 178, 39, 213,
202, 198
]
}
}


// 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.