-
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: access-api forwards store/ and upload/ invocations to upload-api (
- Loading branch information
Showing
17 changed files
with
722 additions
and
12 deletions.
There are no files selected for viewing
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
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,133 @@ | ||
import * as Client from '@ucanto/client' | ||
import * as CAR from '@ucanto/transport/car' | ||
import * as CBOR from '@ucanto/transport/cbor' | ||
import { DID } from '@ucanto/core' | ||
import * as HTTP from '@ucanto/transport/http' | ||
// eslint-disable-next-line no-unused-vars | ||
import * as Ucanto from '@ucanto/interface' | ||
import { createProxyHandler } from '../ucanto/proxy.js' | ||
|
||
/** | ||
* @typedef {import('../ucanto/types.js').InferService<Omit<import('@web3-storage/capabilities/store'), 'store'>>} StoreServiceInferred | ||
* @typedef {import('../ucanto/types.js').InferService<Omit<import('@web3-storage/capabilities/upload'), '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 {{ default: Connection } & Record<Ucanto.UCAN.DID, Connection>} options.connections | ||
*/ | ||
function createProxyService(options) { | ||
const handleInvocation = createProxyHandler(options) | ||
// eslint-disable-next-line unicorn/no-array-reduce | ||
const service = options.methods.reduce((obj, method) => { | ||
obj[method] = handleInvocation | ||
return obj | ||
}, /** @type {Record<M, typeof handleInvocation>} */ ({})) | ||
return service | ||
} | ||
|
||
/** | ||
* @typedef UcantoHttpConnectionOptions | ||
* @property {Ucanto.UCAN.DID} audience | ||
* @property {typeof globalThis.fetch} options.fetch | ||
* @property {URL} options.url | ||
*/ | ||
|
||
/** | ||
* @param {UcantoHttpConnectionOptions} options | ||
* @returns {Ucanto.ConnectionView<any>} | ||
*/ | ||
function createUcantoHttpConnection(options) { | ||
return Client.connect({ | ||
id: DID.parse(options.audience), | ||
encoder: CAR, | ||
decoder: CBOR, | ||
channel: HTTP.open({ | ||
fetch: options.fetch, | ||
url: options.url, | ||
}), | ||
}) | ||
} | ||
|
||
const uploadApiEnvironments = { | ||
production: { | ||
audience: /** @type {const} */ ('did:web:web3.storage'), | ||
url: new URL('https://up.web3.storage'), | ||
}, | ||
staging: { | ||
audience: /** @type {const} */ ('did:web:staging.web3.storage'), | ||
url: new URL('https://staging.up.web3.storage'), | ||
}, | ||
} | ||
|
||
/** | ||
* @typedef {keyof typeof uploadApiEnvironments} UploadApiEnvironmentName | ||
* @typedef {typeof uploadApiEnvironments[UploadApiEnvironmentName]['audience']} UploadApiAudience | ||
*/ | ||
|
||
/** | ||
* @param {object} options | ||
* @param {typeof globalThis.fetch} [options.fetch] | ||
* @param {object} options.uploadApi | ||
* @param {URL} [options.uploadApi.production] | ||
* @param {URL} [options.uploadApi.staging] | ||
*/ | ||
function getDefaultConnections(options) { | ||
const { fetch = globalThis.fetch, uploadApi } = options | ||
return { | ||
default: createUcantoHttpConnection({ | ||
...uploadApiEnvironments.production, | ||
...(uploadApi.production && { url: uploadApi.production }), | ||
fetch, | ||
}), | ||
...(uploadApi.staging && { | ||
[uploadApiEnvironments.staging.audience]: createUcantoHttpConnection({ | ||
...uploadApiEnvironments.staging, | ||
url: uploadApi.staging, | ||
fetch, | ||
}), | ||
}), | ||
} | ||
} | ||
|
||
/** | ||
* @template {Ucanto.ConnectionView<any>} [Connection=Ucanto.ConnectionView<any>] | ||
* @param {object} options | ||
* @param {Ucanto.Signer} [options.signer] | ||
* @param {typeof globalThis.fetch} [options.fetch] | ||
* @param {{ default: Connection, [K: Ucanto.UCAN.DID]: Connection }} [options.connections] | ||
* @param {Record<Ucanto.UCAN.DID, URL>} [options.audienceToUrl] | ||
* @param {object} options.uploadApi | ||
* @param {URL} [options.uploadApi.production] | ||
* @param {URL} [options.uploadApi.staging] | ||
*/ | ||
export function createUploadProxy(options) { | ||
return createProxyService({ | ||
...options, | ||
connections: options.connections || getDefaultConnections(options), | ||
methods: ['list', 'add', 'remove', 'upload'], | ||
}) | ||
} | ||
|
||
/** | ||
* @template {Ucanto.ConnectionView<any>} [Connection=Ucanto.ConnectionView<any>] | ||
* @param {object} options | ||
* @param {Ucanto.Signer} [options.signer] | ||
* @param {typeof globalThis.fetch} [options.fetch] | ||
* @param {{ default: Connection, [K: Ucanto.UCAN.DID]: Connection }} [options.connections] | ||
* @param {Record<Ucanto.UCAN.DID, URL>} [options.audienceToUrl] | ||
* @param {object} options.uploadApi | ||
* @param {URL} [options.uploadApi.production] | ||
* @param {URL} [options.uploadApi.staging] | ||
*/ | ||
export function createStoreProxy(options) { | ||
return createProxyService({ | ||
...options, | ||
connections: options.connections || getDefaultConnections(options), | ||
methods: ['list', 'add', 'remove', 'store'], | ||
}) | ||
} |
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 @@ | ||
// eslint-disable-next-line no-unused-vars | ||
import * as Ucanto from '@ucanto/interface' | ||
import * as Client from '@ucanto/client' | ||
|
||
/** | ||
* @template {Ucanto.ConnectionView<any>} [Connection=Ucanto.ConnectionView<any>] | ||
* @param {object} options | ||
* @param {{ default: Connection, [K: Ucanto.UCAN.DID]: Connection }} options.connections | ||
* @param {Ucanto.Signer} [options.signer] | ||
*/ | ||
export function createProxyHandler(options) { | ||
/** | ||
* @template {import('@ucanto/interface').Capability} Capability | ||
* @param {Ucanto.Invocation<Capability>} invocationIn | ||
* @param {Ucanto.InvocationContext} context | ||
* @returns {Promise<Ucanto.Result<any, { error: true }>>} | ||
*/ | ||
return async function handleInvocation(invocationIn, context) { | ||
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` | ||
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) | ||
|
||
const [result] = await Client.execute( | ||
[ | ||
Client.invoke({ | ||
issuer: proxyInvocationIssuer, | ||
capability: capabilities[0], | ||
audience, | ||
proofs: [invocationIn], | ||
}), | ||
], | ||
/** @type {Client.ConnectionView<any>} */ (connection) | ||
) | ||
return result | ||
} | ||
} |
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 |
---|---|---|
@@ -1,10 +1,41 @@ | ||
import { | ||
InferInvokedCapability, | ||
Match, | ||
ParsedCapability, | ||
RequestDecoder, | ||
RequestEncoder, | ||
ResponseDecoder, | ||
ResponseEncoder, | ||
ServiceMethod, | ||
TheCapabilityParser, | ||
} from '@ucanto/interface' | ||
|
||
export interface ClientCodec extends RequestEncoder, ResponseDecoder {} | ||
|
||
export interface ServerCodec extends RequestDecoder, ResponseEncoder {} | ||
|
||
/** | ||
* 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> | ||
>, | ||
Success = any | ||
> = { | ||
[K in KeysWithValue<S, CP>]: ServiceMethod< | ||
InferInvokedCapability<S[K] extends CP ? S[K] : never>, | ||
Success, | ||
{ error: true } | ||
> | ||
} |
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
Oops, something went wrong.