Skip to content

Commit

Permalink
simplify upload-api-proxy and remove redundancies by e.g. inferring s…
Browse files Browse the repository at this point in the history
…tore/upload service types from the capability defs
  • Loading branch information
gobengo committed Jan 13, 2023
1 parent 3f50270 commit 08981d8
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 95 deletions.
2 changes: 1 addition & 1 deletion packages/access-api/src/service/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { UploadApiProxyService } from './upload-api-proxy.js'
*/
export function service(ctx) {
return {
...UploadApiProxyService.create({
...new UploadApiProxyService({
fetch: globalThis.fetch,
}),

Expand Down
89 changes: 15 additions & 74 deletions packages/access-api/src/service/upload-api-proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,47 +14,17 @@ import * as Ucanto from '@ucanto/interface'
import { createProxyHandler } from '../ucanto/proxy.js'

/**
* @template {Ucanto.Capability} C
* @template [Success=unknown]
* @template {{ error: true }} [Failure={error:true}]
* @callback InvocationResponder
* @param {Ucanto.Invocation<C>} invocationIn
* @param {Ucanto.InvocationContext} context
* @returns {Promise<Ucanto.Result<Success, Failure>>}
*/

/**
* Select from T the property names whose values are of type V
*
* @template T
* @template V
* @typedef { { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T] } KeysWithValue
*/

/**
* Select from T the entries where the vlaue is of type V
*
* @template T
* @template V
* @typedef { { [K in KeysWithValue<T,V>]: T[K] } } OnlyValuesOfType
*/

/**
* @template {Record<string, unknown>} S
* @typedef { { [K in KeysWithValue<S, Ucanto.TheCapabilityParser<any>>]: InvocationResponder<Ucanto.InferInvokedCapability<S[K] extends Ucanto.TheCapabilityParser<infer M> ? S[K] : never>> } } ModuleService
*/

/**
* @typedef {ModuleService<Omit<Store, 'store'>>} StoreServiceInferred
* @typedef {ModuleService<Upload>} UploadServiceInferred
* @typedef {import('../ucanto/types.js').InferService<Omit<Store, 'store'>>} StoreServiceInferred
* @typedef {import('../ucanto/types.js').InferService<Upload>} UploadServiceInferred
*/

/**
* @template {string|number|symbol} M
* @template {Ucanto.ConnectionView<any>} [Connection=Ucanto.ConnectionView<any>]
* @param {object} options
* @param {Ucanto.Signer} [options.signer]
* @param {Array<M>} options.methods
* @param {Pick<Map<dagUcan.DID, Ucanto.ConnectionView<Record<M, any>>>, 'get'>} options.connections
* @param {{ default: Connection } & Record<Ucanto.UCAN.DID, Connection>} options.connections
*/
function createProxyService(options) {
const handleInvocation = createProxyHandler(options)
Expand Down Expand Up @@ -116,20 +86,6 @@ export const uploadApiAudienceToUrl = (() => {
return object
})()

// /**
// * @param {object} options
// * @param {Ucanto.Signer} [options.signer]
// * @param {typeof globalThis.fetch} options.fetch
// * @param {Record<Ucanto.UCAN.DID, URL>} [options.audienceToUrl]
// * @returns
// */
// export const createStoreService = (options) => {
// return createProxyService({
// connections:
// methods: ['list', 'add', 'remove', 'store'],
// })
// }

export class UploadApiProxyService {
/** @type {StoreServiceInferred} */
store
Expand All @@ -142,42 +98,27 @@ export class UploadApiProxyService {
* @param {typeof globalThis.fetch} options.fetch
* @param {Record<Ucanto.UCAN.DID, URL>} [options.audienceToUrl]
*/
static create(options) {
const defaultAudience = uploadApiEnvironments.production.audience
const audienceToUrl = options.audienceToUrl || uploadApiAudienceToUrl
constructor(options) {
const proxyOptions = {
signer: options.signer,
connections: {
/** @param {Ucanto.DID} audience */
get(audience) {
const defaultedAudience =
audience in audienceToUrl ? audience : defaultAudience
return createUcantoHttpConnection({
audience: defaultedAudience,
fetch: options.fetch,
url: audienceToUrl[defaultedAudience],
})
},
default: createUcantoHttpConnection({
...uploadApiEnvironments.production,
fetch: options.fetch,
}),
[uploadApiEnvironments.staging.audience]: createUcantoHttpConnection({
...uploadApiEnvironments.staging,
fetch: options.fetch,
}),
},
}
const store = createProxyService({
this.store = createProxyService({
...proxyOptions,
methods: ['list', 'add', 'remove', 'store'],
})
const upload = createProxyService({
this.upload = createProxyService({
...proxyOptions,
methods: ['list', 'add', 'remove', 'upload'],
})
return new this(store, upload)
}

/**
* @protected
* @param {StoreServiceInferred} store
* @param {UploadServiceInferred} upload
*/
constructor(store, upload) {
this.store = store
this.upload = upload
}
}
22 changes: 10 additions & 12 deletions packages/access-api/src/ucanto/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import * as Client from '@ucanto/client'
*/

/**
* @template {Ucanto.ConnectionView<any>} [Connection=Ucanto.ConnectionView<any>]
* @param {object} options
* @param {Pick<Map<Ucanto.DID, Ucanto.ConnectionView<any>>, 'get'>} options.connections
* @param {{ default: Connection, [K: Ucanto.UCAN.DID]: Connection }} options.connections
* @param {Ucanto.Signer} [options.signer]
*/
export function createProxyHandler(options) {
Expand All @@ -25,18 +26,15 @@ export function createProxyHandler(options) {
* @returns {Promise<Ucanto.Result<any, { error: true }>>}
*/
return async function handleInvocation(invocationIn, context) {
const connection = options.connections.get(invocationIn.audience.did())
if (!connection) {
throw new Error(
`unable to get connection for audience ${invocationIn.audience.did()}}`
)
}
// eslint-disable-next-line unicorn/prefer-logical-operator-over-ternary
const proxyInvocationIssuer = options.signer
const { connections, signer } = options
const { audience, capabilities } = invocationIn
const connection = connections[audience.did()] ?? connections.default
// eslint-disable-next-line unicorn/prefer-logical-operator-over-ternary, no-unneeded-ternary
const proxyInvocationIssuer = signer
? // this results in a forwarded invocation, but the upstream will reject the signature
// created using options.signer unless options.signer signs w/ the same private key as the original issuer
// and it'd be nice to not even have to pass around `options.signer`
options.signer
signer
: // this works, but involves lying about the issuer type (it wants a Signer but context.id is only a Verifier)
// @todo obviate this type override via https://github.com/web3-storage/ucanto/issues/195
/** @type {Ucanto.Signer} */ (context.id)
Expand All @@ -45,8 +43,8 @@ export function createProxyHandler(options) {
[
Client.invoke({
issuer: proxyInvocationIssuer,
capability: invocationIn.capabilities[0],
audience: invocationIn.audience,
capability: capabilities[0],
audience,
proofs: [invocationIn],
}),
],
Expand Down
43 changes: 43 additions & 0 deletions packages/access-api/src/ucanto/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,53 @@
import {
Capability,
InferInvokedCapability,
Invocation,
InvocationContext,
Match,
ParsedCapability,
RequestDecoder,
RequestEncoder,
ResponseDecoder,
ResponseEncoder,
Result,
TheCapabilityParser,
} from '@ucanto/interface'

export interface ClientCodec extends RequestEncoder, ResponseDecoder {}

export interface ServerCodec extends RequestDecoder, ResponseEncoder {}

/**
* A single ucanto service method.
*/
export type InvocationResponder<
C extends Capability,
Success = unknown,
Failure extends { error: true } = { error: true }
> = (
invocation: Invocation<C>,
context: InvocationContext
) => Promise<Result<Success, Failure>>

/**
* Select from T the property names whose values are of type V
*/
export type KeysWithValue<T, V> = {
[K in keyof T]-?: T[K] extends V ? K : never
}[keyof T]

/**
* Infer ucanto service object from a namespace object containing CapabilityParsers
*
* @example InferService<import('@web3-storage/capabilities/upload')>
*/
export type InferService<
S extends Record<string, unknown>,
CP extends TheCapabilityParser<Match<ParsedCapability>> = TheCapabilityParser<
Match<ParsedCapability>
>
> = {
[K in KeysWithValue<S, CP>]: InvocationResponder<
InferInvokedCapability<S[K] extends CP ? S[K] : never>
>
}
14 changes: 6 additions & 8 deletions packages/access-api/test/ucanto-proxy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,12 @@ describe('ucanto-proxy', () => {
test: {
succeed: createProxyHandler({
connections: {
get: (audience) => {
return Client.connect({
id: upstreamPrincipal,
encoder: CAR,
decoder: CBOR,
channel: upstream,
})
},
default: Client.connect({
id: upstreamPrincipal,
encoder: CAR,
decoder: CBOR,
channel: upstream,
}),
},
}),
},
Expand Down

0 comments on commit 08981d8

Please sign in to comment.