From 81eb44c2789860764f313744383ff5b2622864eb Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Mon, 16 Dec 2024 11:08:57 -0300 Subject: [PATCH] set default gateway --- packages/cli/index.js | 4 +- packages/cli/space.js | 29 ++++----- packages/w3up-client/package.json | 1 + packages/w3up-client/src/client.js | 37 ++++++++--- packages/w3up-client/test/client.test.js | 30 ++++----- .../test/helpers/gateway-server.js | 61 +++++++++++++++++++ packages/w3up-client/test/mocks/service.js | 18 ++++-- 7 files changed, 133 insertions(+), 47 deletions(-) create mode 100644 packages/w3up-client/test/helpers/gateway-server.js diff --git a/packages/cli/index.js b/packages/cli/index.js index 35b4ed585..11731c2db 100644 --- a/packages/cli/index.js +++ b/packages/cli/index.js @@ -265,7 +265,9 @@ export async function remove(rootCid, opts) { */ export async function createSpace(name) { const client = await getClient() - const space = await client.createSpace(name) + const space = await client.createSpace(name, { + skipGatewayAuthorization: true, + }) await client.setCurrentSpace(space.did()) console.log(space.did()) } diff --git a/packages/cli/space.js b/packages/cli/space.js index 3e48e77fa..1ca9ec1f9 100644 --- a/packages/cli/space.js +++ b/packages/cli/space.js @@ -29,7 +29,7 @@ import * as Result from '@storacha/client/result' export const create = async (name, options) => { const client = await getClient() const spaces = client.spaces() - + let space if (options.skipGatewayAuthorization === true) { space = await client.createSpace(await chooseName(name ?? '', spaces), { @@ -37,15 +37,17 @@ export const create = async (name, options) => { }) } else { const gateways = options.authorizeGatewayServices ?? [] - const connections = gateways.map(({ id, serviceEndpoint }) => - UcantoClient.connect({ + const connections = gateways.map(({ id, serviceEndpoint }) => { + /** @type {UcantoClient.ConnectionView} */ + const connection = UcantoClient.connect({ id: { did: () => id, }, codec: CAR.outbound, channel: HTTP.open({ url: new URL(serviceEndpoint) }), - }) - ) + }) + return connection + }) space = await client.createSpace(await chooseName(name ?? '', spaces), { authorizeGatewayServices: connections, }) @@ -209,7 +211,7 @@ export const provision = async (name = '', options = {}) => { const { ok: bytes, error: fetchError } = await fetch(options.coupon) .then((response) => response.arrayBuffer()) .then((buffer) => Result.ok(new Uint8Array(buffer))) - .catch((error) => Result.error(/** @type {Error} */ (error))) + .catch((error) => Result.error(/** @type {Error} */(error))) if (fetchError) { console.error(`Failed to fetch coupon from ${options.coupon}`) @@ -245,8 +247,7 @@ export const provision = async (name = '', options = {}) => { if (result.error) { console.error( - `⚠️ Failed to set up billing account,\n ${ - Object(result.error).message ?? '' + `⚠️ Failed to set up billing account,\n ${Object(result.error).message ?? '' }` ) process.exit(1) @@ -285,7 +286,7 @@ const chooseSpace = (client, { name }) => { * @param {W3Space.Model} space * @param {CreateOptions} options */ -export const setupEmailRecovery = async (space, options = {}) => {} +export const setupEmailRecovery = async (space, options = {}) => { } /** * @param {string} email @@ -347,8 +348,8 @@ const chooseName = async (name, spaces) => { name === '' ? 'What would you like to call this space?' : space - ? `Name "${space.name}" is already taken, please choose a different one` - : null + ? `Name "${space.name}" is already taken, please choose a different one` + : null if (message == null) { return name @@ -415,9 +416,9 @@ export const setupAccount = async (client) => { return email ? await Account.loginWithClient( - /** @type {DIDMailto.EmailAddress} */ (email), - client - ) + /** @type {DIDMailto.EmailAddress} */(email), + client + ) : null } diff --git a/packages/w3up-client/package.json b/packages/w3up-client/package.json index d455aaaa4..9ac874927 100644 --- a/packages/w3up-client/package.json +++ b/packages/w3up-client/package.json @@ -131,6 +131,7 @@ "mock:bucket-0-200": "PORT=8989 STATUS=200 node test/helpers/bucket-server.js", "mock:bucket-1-200": "PORT=8990 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 0a98681de..94960b00e 100644 --- a/packages/w3up-client/src/client.js +++ b/packages/w3up-client/src/client.js @@ -29,6 +29,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, @@ -264,7 +267,7 @@ export class Client extends Base { * @param {SpaceCreateOptions} options - Options for the space creation. * @returns {Promise} The created space owned by the agent. */ - async createSpace(name, options = {}) { + async createSpace(name, options) { // Save the space to authorize the client to use the space const space = await this._agent.createSpace(name) @@ -301,16 +304,30 @@ 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: () => + /** @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' + ), + }), + }), + ] } - for (const serviceConnection of options.authorizeGatewayServices) { + for (const serviceConnection of authorizeGatewayServices) { await authorizeContentServe(this, space, serviceConnection) } } @@ -594,7 +611,7 @@ 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 939f2973b..0b1487639 100644 --- a/packages/w3up-client/test/client.test.js +++ b/packages/w3up-client/test/client.test.js @@ -645,7 +645,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( @@ -668,23 +668,17 @@ 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' - ) - } + process.env.DEFAULT_GATEWAY_ID = gateway.did() + process.env.DEFAULT_GATEWAY_URL = 'http://localhost:5001' + + 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 }) => { 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..34dd7c814 --- /dev/null +++ b/packages/w3up-client/test/helpers/gateway-server.js @@ -0,0 +1,61 @@ +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') { + const service = getContentServeMockService() + const server = createUcantoServer(gateway, service) + + const bodyBuffer = Buffer.concat(await collect(req)) + + 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: reqHeaders, + }) + + for (const [key, value] of Object.entries(headers)) { + res.setHeader(key, value) + } + res.writeHead(status ?? 200) + res.end(body) + } + 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 bfb00beca..b2a4cfe4e 100644 --- a/packages/w3up-client/test/mocks/service.js +++ b/packages/w3up-client/test/mocks/service.js @@ -20,19 +20,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,