Skip to content

Commit

Permalink
feat(w3up-client): add default gateway authorization (#1604)
Browse files Browse the repository at this point in the history
### Context  
While updating the `web3-storage` docs, `w3cli`, and `@storacha/client`,
I found it inconvenient to repeatedly define the gateway DID and URL
every time I created a space. Also, if we don't provide the
`skipGatewayAuthorization=true`, it will throw an error saying it is
required to provide the gateway services to authorize.

### Changes
Following @travis suggestion (see:
storacha/docs#21 (comment)), I've
updated the client to automatically authorize the Storacha Gateway
(Production) by default if `skipGatewayAuthorization` is not set to
`true` and `authorizeGatewayServices` is not provided or is empty.

Once approved, this change will need to be ported to
[upload-service/w3up-client](https://github.com/storacha/upload-service/tree/main/packages/w3up-client).

And the PR storacha/freeway#135 needs to be
merged before this change gets released.
  • Loading branch information
fforbeck authored Dec 19, 2024
1 parent 22d0bf9 commit e669b55
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 34 deletions.
1 change: 1 addition & 0 deletions packages/w3up-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions packages/w3up-client/src/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<import('@web3-storage/access').PlanGetSuccess>} - 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.
Expand Down
39 changes: 30 additions & 9 deletions packages/w3up-client/src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -255,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<import('./types.js').ContentServeService>} ConnectionView
*
Expand Down Expand Up @@ -304,16 +309,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 <authorizeGatewayServices> 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)
}
}
Expand Down Expand Up @@ -609,7 +628,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,
}
Expand Down
30 changes: 12 additions & 18 deletions packages/w3up-client/test/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -679,23 +679,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 <authorizeGatewayServices> 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 }) => {
Expand Down
61 changes: 61 additions & 0 deletions packages/w3up-client/test/helpers/gateway-server.js
Original file line number Diff line number Diff line change
@@ -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<string, string>} */ (
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<Buffer[]>} */ (
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))
21 changes: 16 additions & 5 deletions packages/w3up-client/test/mocks/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,38 @@ 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
}),
},
}
}

/**
* 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,
Expand Down

0 comments on commit e669b55

Please sign in to comment.