Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(w3up-client): add default gateway authorization #1604

Merged
merged 3 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -612,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,
}
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
Loading