From fbe074f3adc8b0a5d5204836b762a1e57e7a984d Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Fri, 13 Dec 2024 11:34:32 -0300 Subject: [PATCH 1/3] feat(w3up-client): add default gateway authorization --- packages/w3up-client/src/client.js | 26 ++++++++++++++++------- packages/w3up-client/test/client.test.js | 27 ++++++++---------------- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/packages/w3up-client/src/client.js b/packages/w3up-client/src/client.js index 096dcdbce..e0e096fa8 100644 --- a/packages/w3up-client/src/client.js +++ b/packages/w3up-client/src/client.js @@ -30,6 +30,9 @@ import { FilecoinClient } from './capability/filecoin.js' import { CouponAPI } from './coupon.js' export * as Access from './capability/access.js' import * as Result from './result.js' +import * as UcantoClient from '@ucanto/client' +import { HTTP } from '@ucanto/transport' +import * as CAR from '@ucanto/transport/car' export { AccessClient, @@ -304,16 +307,23 @@ export class Client extends Base { // Authorize the listed Gateway Services to serve content from the created space if (options.skipGatewayAuthorization !== true) { - if ( - !options.authorizeGatewayServices || - options.authorizeGatewayServices.length === 0 - ) { - throw new Error( - 'failed to authorize Gateway Services: missing option' - ) + let authorizeGatewayServices = options.authorizeGatewayServices + if (!authorizeGatewayServices || authorizeGatewayServices.length === 0) { + // If no Gateway Services are provided, authorize the Storacha Gateway Service + authorizeGatewayServices = [ + UcantoClient.connect({ + id: { + did: () => 'did:web:w3s.link', + }, + codec: CAR.outbound, + channel: HTTP.open({ + url: new URL(' https://freeway.dag.haus'), + }), + }), + ] } - for (const serviceConnection of options.authorizeGatewayServices) { + for (const serviceConnection of authorizeGatewayServices) { await authorizeContentServe(this, space, serviceConnection) } } diff --git a/packages/w3up-client/test/client.test.js b/packages/w3up-client/test/client.test.js index b64463bf4..c9e1d5350 100644 --- a/packages/w3up-client/test/client.test.js +++ b/packages/w3up-client/test/client.test.js @@ -656,7 +656,7 @@ export const testClient = { assert.fail(error, 'should not throw when creating the space') } }, - 'should throw when the content serve authorization fails due to missing service configuration': + 'should authorize the Storacha Gateway Service when no Gateway Services are provided': async (assert, { mail, grantAccess, connection }) => { // Step 1: Create a client for Alice and login const aliceClient = new Client( @@ -679,23 +679,14 @@ export const testClient = { await grantAccess(message) const aliceAccount = await aliceLogin - try { - const spaceA = await aliceClient.createSpace( - 'authorize-gateway-space', - { - account: aliceAccount, - authorizeGatewayServices: [], // No services to authorize - } - ) - assert.fail(spaceA, 'should not create the space') - } catch (error) { - assert.match( - // @ts-expect-error - error.message, - /missing option/, - 'should throw when creating the space' - ) - } + const spaceA = await aliceClient.createSpace( + 'authorize-gateway-space', + { + account: aliceAccount, + authorizeGatewayServices: [], // If no Gateway Services are provided, authorize the Storacha Gateway Service + } + ) + assert.ok(spaceA, 'should create the space') }, 'should throw when content serve service can not process the invocation': async (assert, { mail, grantAccess, connection }) => { From 2988a9b830e3cc6ab5c64cda805c7bc68e58d9eb Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Fri, 13 Dec 2024 14:11:13 -0300 Subject: [PATCH 2/3] add test with mock gateway --- packages/w3up-client/package.json | 1 + packages/w3up-client/src/client.js | 9 ++- packages/w3up-client/test/client.test.js | 4 ++ .../test/helpers/gateway-server.js | 60 +++++++++++++++++++ packages/w3up-client/test/mocks/service.js | 21 +++++-- 5 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 packages/w3up-client/test/helpers/gateway-server.js diff --git a/packages/w3up-client/package.json b/packages/w3up-client/package.json index 0fb38e4a6..3fdafa870 100644 --- a/packages/w3up-client/package.json +++ b/packages/w3up-client/package.json @@ -140,6 +140,7 @@ "mock": "run-p mock:*", "mock:bucket-200": "PORT=8989 STATUS=200 node test/helpers/bucket-server.js", "mock:receipts-server": "PORT=9201 node test/helpers/receipts-server.js", + "mock:gateway-server": "PORT=5001 node test/helpers/gateway-server.js", "coverage": "c8 report -r html && open coverage/index.html", "rc": "npm version prerelease --preid rc", "docs": "npm run build && typedoc --out docs-generated" diff --git a/packages/w3up-client/src/client.js b/packages/w3up-client/src/client.js index e0e096fa8..42b6af9f4 100644 --- a/packages/w3up-client/src/client.js +++ b/packages/w3up-client/src/client.js @@ -313,11 +313,16 @@ export class Client extends Base { authorizeGatewayServices = [ UcantoClient.connect({ id: { - did: () => 'did:web:w3s.link', + did: () => + /** @type {`did:${string}:${string}`} */ ( + process.env.DEFAULT_GATEWAY_ID ?? 'did:web:w3s.link' + ), }, codec: CAR.outbound, channel: HTTP.open({ - url: new URL(' https://freeway.dag.haus'), + url: new URL( + process.env.DEFAULT_GATEWAY_URL ?? 'https://freeway.dag.haus' + ), }), }), ] diff --git a/packages/w3up-client/test/client.test.js b/packages/w3up-client/test/client.test.js index c9e1d5350..198779c59 100644 --- a/packages/w3up-client/test/client.test.js +++ b/packages/w3up-client/test/client.test.js @@ -658,6 +658,7 @@ export const testClient = { }, 'should authorize the Storacha Gateway Service when no Gateway Services are provided': async (assert, { mail, grantAccess, connection }) => { + Test // Step 1: Create a client for Alice and login const aliceClient = new Client( await AgentData.create({ @@ -679,6 +680,9 @@ export const testClient = { await grantAccess(message) const aliceAccount = await aliceLogin + process.env.DEFAULT_GATEWAY_ID = gateway.did() + process.env.DEFAULT_GATEWAY_URL = 'http://localhost:5001' + const spaceA = await aliceClient.createSpace( 'authorize-gateway-space', { diff --git a/packages/w3up-client/test/helpers/gateway-server.js b/packages/w3up-client/test/helpers/gateway-server.js new file mode 100644 index 000000000..d5df5b83d --- /dev/null +++ b/packages/w3up-client/test/helpers/gateway-server.js @@ -0,0 +1,60 @@ +import { createServer } from 'node:http' +import { + createUcantoServer, + getContentServeMockService, +} from '../mocks/service.js' +import { gateway } from '../../../upload-api/test/helpers/utils.js' + +const port = 5001 + +const server = createServer(async (req, res) => { + res.setHeader('Access-Control-Allow-Origin', '*') + res.setHeader('Access-Control-Allow-Methods', '*') + res.setHeader('Access-Control-Allow-Headers', '*') + if (req.method === 'OPTIONS') return res.end() + + if (req.method === 'POST') { + console.time('Service Setup') + const service = getContentServeMockService() + const server = createUcantoServer(gateway, service) + console.timeEnd('Service Setup') + + console.time('Collect Request Body') + const bodyBuffer = Buffer.concat(await collect(req)) + console.timeEnd('Collect Request Body') + + console.time('Server Request') + const { headers, body, status } = await server.request({ + body: new Uint8Array( + bodyBuffer.buffer, + bodyBuffer.byteOffset, + bodyBuffer.byteLength + ), + headers: /** @type {Record} */ ( + Object.fromEntries(Object.entries(req.headers)) + ), + }) + console.timeEnd('Server Request') + return new Response(body, { headers, status: status ?? 200 }) + } + res.end() +}) + +/** @param {import('node:stream').Readable} stream */ +const collect = (stream) => { + return /** @type {Promise} */ ( + new Promise((resolve, reject) => { + const chunks = /** @type {Buffer[]} */ ([]) + stream.on('data', (chunk) => chunks.push(Buffer.from(chunk))) + stream.on('error', (err) => reject(err)) + stream.on('end', () => resolve(chunks)) + }) + ) +} + +// eslint-disable-next-line no-console +server.listen(port, () => + console.log(`[Mock] Gateway Server Listening on :${port}`) +) + +process.on('SIGTERM', () => process.exit(0)) diff --git a/packages/w3up-client/test/mocks/service.js b/packages/w3up-client/test/mocks/service.js index 4b8805fde..0d199d5bf 100644 --- a/packages/w3up-client/test/mocks/service.js +++ b/packages/w3up-client/test/mocks/service.js @@ -12,7 +12,8 @@ import * as AccessCaps from '@web3-storage/capabilities' export function getContentServeMockService(result = { ok: {} }) { return { access: { - delegate: Server.provide(AccessCaps.Access.delegate, async () => { + delegate: Server.provide(AccessCaps.Access.delegate, async (data) => { + console.log('Access Caps Delegate', data) return result }), }, @@ -20,19 +21,29 @@ export function getContentServeMockService(result = { ok: {} }) { } /** - * Generic function to create connection to any type of mock service with any type of signer id. + * Creates a new Ucanto server with the given options. * * @param {any} id * @param {any} service - * @param {string | undefined} [url] */ -export function getConnection(id, service, url = undefined) { - const server = Server.create({ +export function createUcantoServer(id, service) { + return Server.create({ id: id, service, codec: CAR.inbound, validateAuthorization: () => ({ ok: {} }), }) +} + +/** + * Generic function to create connection to any type of mock service with any type of signer id. + * + * @param {any} id + * @param {any} service + * @param {string | undefined} [url] + */ +export function getConnection(id, service, url = undefined) { + const server = createUcantoServer(id, service) const connection = Client.connect({ id: id, codec: CAR.outbound, From 1a4a913a89892f385b8d74ba8c952e970bf95112 Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Mon, 16 Dec 2024 10:19:19 -0300 Subject: [PATCH 3/3] fix tests --- packages/w3up-client/src/account.js | 4 ++-- packages/w3up-client/src/client.js | 8 ++++++- packages/w3up-client/test/client.test.js | 1 - .../test/helpers/gateway-server.js | 21 ++++++++++--------- packages/w3up-client/test/mocks/service.js | 2 +- 5 files changed, 21 insertions(+), 15 deletions(-) diff --git a/packages/w3up-client/src/account.js b/packages/w3up-client/src/account.js index c6e418202..997cdb942 100644 --- a/packages/w3up-client/src/account.js +++ b/packages/w3up-client/src/account.js @@ -242,8 +242,8 @@ export class AccountPlan { * or when the abort signal is aborted. * * @param {object} [options] - * @param {number} [options.interval=1000] - The polling interval in milliseconds (default is 1000ms). - * @param {number} [options.timeout=900000] - The maximum time to wait in milliseconds before throwing a timeout error (default is 15 minutes). + * @param {number} [options.interval] - The polling interval in milliseconds (default is 1000ms). + * @param {number} [options.timeout] - The maximum time to wait in milliseconds before throwing a timeout error (default is 15 minutes). * @param {AbortSignal} [options.signal] - An optional AbortSignal to cancel the waiting process. * @returns {Promise} - Resolves once a payment plan is selected within the timeout. * @throws {Error} - Throws an error if there is an issue retrieving the payment plan or if the timeout is exceeded. diff --git a/packages/w3up-client/src/client.js b/packages/w3up-client/src/client.js index 42b6af9f4..4f48b2fd8 100644 --- a/packages/w3up-client/src/client.js +++ b/packages/w3up-client/src/client.js @@ -258,6 +258,8 @@ export class Client extends Base { * In addition, it authorizes the listed Gateway Services to serve content from the created space. * It is done by delegating the `space/content/serve/*` capability to the Gateway Service. * User can skip the Gateway authorization by setting the `skipGatewayAuthorization` option to `true`. + * If no gateways are specified or the `skipGatewayAuthorization` flag is not set, the client will automatically grant access + * to the Storacha Gateway by default (https://freewaying.dag.haus/). * * @typedef {import('./types.js').ConnectionView} ConnectionView * @@ -315,12 +317,14 @@ export class Client extends Base { id: { did: () => /** @type {`did:${string}:${string}`} */ ( + /* c8 ignore next - default prod gateway id is not used in tests */ process.env.DEFAULT_GATEWAY_ID ?? 'did:web:w3s.link' ), }, codec: CAR.outbound, channel: HTTP.open({ url: new URL( + /* c8 ignore next - default prod gateway url is not used in tests */ process.env.DEFAULT_GATEWAY_URL ?? 'https://freeway.dag.haus' ), }), @@ -627,7 +631,9 @@ export const authorizeContentServe = async ( /* c8 ignore next 8 - can't mock this error */ if (verificationResult.out.error) { throw new Error( - `failed to publish delegation for audience ${options.audience}: ${verificationResult.out.error.message}`, + `failed to publish delegation for audience ${audience.did()}: ${ + verificationResult.out.error.message + }`, { cause: verificationResult.out.error, } diff --git a/packages/w3up-client/test/client.test.js b/packages/w3up-client/test/client.test.js index 198779c59..401267a18 100644 --- a/packages/w3up-client/test/client.test.js +++ b/packages/w3up-client/test/client.test.js @@ -658,7 +658,6 @@ export const testClient = { }, 'should authorize the Storacha Gateway Service when no Gateway Services are provided': async (assert, { mail, grantAccess, connection }) => { - Test // Step 1: Create a client for Alice and login const aliceClient = new Client( await AgentData.create({ diff --git a/packages/w3up-client/test/helpers/gateway-server.js b/packages/w3up-client/test/helpers/gateway-server.js index d5df5b83d..34dd7c814 100644 --- a/packages/w3up-client/test/helpers/gateway-server.js +++ b/packages/w3up-client/test/helpers/gateway-server.js @@ -14,28 +14,29 @@ const server = createServer(async (req, res) => { if (req.method === 'OPTIONS') return res.end() if (req.method === 'POST') { - console.time('Service Setup') const service = getContentServeMockService() const server = createUcantoServer(gateway, service) - console.timeEnd('Service Setup') - console.time('Collect Request Body') const bodyBuffer = Buffer.concat(await collect(req)) - console.timeEnd('Collect Request Body') - console.time('Server Request') + const reqHeaders = /** @type {Record} */ ( + Object.fromEntries(Object.entries(req.headers)) + ) + const { headers, body, status } = await server.request({ body: new Uint8Array( bodyBuffer.buffer, bodyBuffer.byteOffset, bodyBuffer.byteLength ), - headers: /** @type {Record} */ ( - Object.fromEntries(Object.entries(req.headers)) - ), + headers: reqHeaders, }) - console.timeEnd('Server Request') - return new Response(body, { headers, status: status ?? 200 }) + + for (const [key, value] of Object.entries(headers)) { + res.setHeader(key, value) + } + res.writeHead(status ?? 200) + res.end(body) } res.end() }) diff --git a/packages/w3up-client/test/mocks/service.js b/packages/w3up-client/test/mocks/service.js index 0d199d5bf..58d8a2e9f 100644 --- a/packages/w3up-client/test/mocks/service.js +++ b/packages/w3up-client/test/mocks/service.js @@ -13,7 +13,7 @@ export function getContentServeMockService(result = { ok: {} }) { return { access: { delegate: Server.provide(AccessCaps.Access.delegate, async (data) => { - console.log('Access Caps Delegate', data) + // console.log('Access Caps Delegate', data) return result }), },