@storacha/client
Version:
Client for the storacha.network w3up api
1,003 lines • 53 kB
JavaScript
import assert from 'assert';
import { parseLink } from '@ucanto/server';
import * as Server from '@ucanto/server';
import { Agent, AgentData, claimAccess, requestAccess, } from '@storacha/access/agent';
import { randomBytes, randomCAR } from './helpers/random.js';
import { toCAR } from './helpers/car.js';
import { File } from './helpers/shims.js';
import { authorizeContentServe, Client } from '../src/client.js';
import * as Test from './test.js';
import { receiptsEndpoint } from './helpers/utils.js';
import { Absentee } from '@ucanto/principal';
import { DIDMailto } from '../src/capability/access.js';
import * as Result from './helpers/result.js';
import { alice, confirmConfirmationUrl, gateway, } from '@storacha/upload-api/test/utils';
import * as SpaceCapability from '@storacha/capabilities/space';
import { getConnection, getContentServeMockService } from './mocks/service.js';
import { gatewayServiceConnection } from '../src/service.js';
/** @type {Test.Suite} */
export const testClient = {
uploadFile: Test.withContext({
'should upload a file to the service': async (assert, { connection, provisionsStorage, uploadTable, registry }) => {
const bytes = await randomBytes(128);
const file = new Blob([bytes]);
const expectedCar = await toCAR(bytes);
/** @type {import('@storacha/upload-client/types').CARLink|undefined} */
let carCID;
const alice = new Client(await AgentData.create(), {
// @ts-ignore
serviceConf: {
access: connection,
upload: connection,
},
receiptsEndpoint: new URL(receiptsEndpoint),
});
const space = await alice.createSpace('upload-test', {
skipGatewayAuthorization: true,
});
const auth = await space.createAuthorization(alice);
await alice.addSpace(auth);
await alice.setCurrentSpace(space.did());
// Then we setup a billing for this account
await provisionsStorage.put({
// @ts-expect-error
provider: connection.id.did(),
account: alice.agent.did(),
consumer: space.did(),
});
const dataCID = await alice.uploadFile(file, {
onShardStored: (meta) => {
carCID = meta.cid;
},
});
assert.deepEqual(await uploadTable.exists(space.did(), dataCID), {
ok: true,
});
Result.try(await registry.find(space.did(), expectedCar.cid.multihash));
assert.equal(carCID?.toString(), expectedCar.cid.toString());
assert.equal(dataCID.toString(), expectedCar.roots[0].toString());
},
'should not allow upload without a current space': async (assert, { connection }) => {
const alice = new Client(await AgentData.create(), {
// @ts-ignore
serviceConf: {
access: connection,
upload: connection,
},
});
const bytes = await randomBytes(128);
const file = new Blob([bytes]);
await assert.rejects(alice.uploadFile(file), {
message: 'missing current space: use createSpace() or setCurrentSpace()',
});
},
}),
uploadDirectory: Test.withContext({
'should upload a directory to the service': async (assert, { connection, provisionsStorage, uploadTable }) => {
const bytesList = [await randomBytes(128), await randomBytes(32)];
const files = bytesList.map((bytes, index) => new File([bytes], `${index}.txt`));
/** @type {import('@storacha/upload-client/types').CARLink|undefined} */
let carCID;
const alice = new Client(await AgentData.create(), {
// @ts-ignore
serviceConf: {
access: connection,
upload: connection,
},
receiptsEndpoint: new URL(receiptsEndpoint),
});
const space = await alice.createSpace('upload-dir-test', {
skipGatewayAuthorization: true,
});
const auth = await space.createAuthorization(alice);
await alice.addSpace(auth);
await alice.setCurrentSpace(space.did());
// Then we setup a billing for this account
await provisionsStorage.put({
// @ts-expect-error
provider: connection.id.did(),
account: alice.agent.did(),
consumer: space.did(),
});
const dataCID = await alice.uploadDirectory(files, {
onShardStored: (meta) => {
carCID = meta.cid;
},
});
assert.deepEqual(await uploadTable.exists(space.did(), dataCID), {
ok: true,
});
assert.ok(carCID);
assert.ok(dataCID);
},
}),
uploadCar: Test.withContext({
'uploads a CAR file to the service': async (assert, { connection, provisionsStorage, uploadTable, registry }) => {
const car = await randomCAR(32);
let carCID = /** @type {import('../src/types.js').CARLink|null} */ (null);
const alice = new Client(await AgentData.create(), {
// @ts-ignore
serviceConf: {
access: connection,
upload: connection,
},
receiptsEndpoint: new URL(receiptsEndpoint),
});
const space = await alice.createSpace('car-space', {
skipGatewayAuthorization: true,
});
await alice.addSpace(await space.createAuthorization(alice));
await alice.setCurrentSpace(space.did());
// Then we setup a billing for this account
await provisionsStorage.put({
// @ts-expect-error
provider: connection.id.did(),
account: alice.agent.did(),
consumer: space.did(),
});
const root = await alice.uploadCAR(car, {
onShardStored: (meta) => {
carCID = meta.cid;
},
});
assert.deepEqual(await uploadTable.exists(space.did(), root), {
ok: true,
});
if (carCID == null) {
return assert.ok(carCID);
}
Result.try(await registry.find(space.did(), carCID.multihash));
},
}),
getReceipt: Test.withContext({
'should find a receipt': async (assert, { connection }) => {
const taskCid = parseLink('bafyreibo6nqtvp67daj7dkmeb5c2n6bg5bunxdmxq3lghtp3pmjtzpzfma');
const alice = new Client(await AgentData.create(), {
receiptsEndpoint: new URL('http://localhost:9201'),
// @ts-ignore
serviceConf: {
access: connection,
upload: connection,
},
});
const receipt = await alice.getReceipt(taskCid);
// This is a real `piece/accept` receipt exported as fixture
assert.ok(receipt);
assert.ok(receipt?.ran.link().equals(taskCid));
assert.ok(receipt?.out.ok);
},
}),
currentSpace: {
'should return undefined or space': async (assert) => {
const alice = new Client(await AgentData.create());
const current0 = alice.currentSpace();
assert.equal(current0, undefined);
const space = await alice.createSpace('new-space', {
skipGatewayAuthorization: true,
});
await alice.addSpace(await space.createAuthorization(alice));
await alice.setCurrentSpace(space.did());
const current1 = alice.currentSpace();
assert.ok(current1);
assert.equal(current1?.did(), space.did());
},
},
spaces: Test.withContext({
'should get agent spaces': async (assert) => {
const alice = new Client(await AgentData.create());
const name = `space-${Date.now()}`;
const space = await alice.createSpace(name, {
skipGatewayAuthorization: true,
});
const auth = await space.createAuthorization(alice);
await alice.addSpace(auth);
const spaces = alice.spaces();
assert.equal(spaces.length, 1);
assert.equal(spaces[0].did(), space.did());
assert.equal(spaces[0].name, name);
assert.equal(spaces[0].access.type, 'public');
},
'should create space with accessType private': async (assert) => {
const alice = new Client(await AgentData.create());
const space = await alice.createSpace('private-space', {
access: {
type: 'private',
encryption: {
provider: 'google-kms',
algorithm: 'RSA_DECRYPT_OAEP_3072_SHA256',
},
},
// Creates a temporary space without saving it
skipGatewayAuthorization: true,
});
const auth = await space.createAuthorization(alice);
await alice.addSpace(auth);
assert.equal(space.access.type, 'private');
if (space.access.type === 'private') {
assert.equal(space.access.encryption.provider, 'google-kms');
assert.equal(space.access.encryption.algorithm, 'RSA_DECRYPT_OAEP_3072_SHA256');
}
const spaces = alice.spaces();
assert.equal(spaces.length, 1);
assert.equal(spaces[0].access.type, 'private');
if (spaces[0].access.type === 'private') {
assert.equal(spaces[0].access.encryption.provider, 'google-kms');
assert.equal(spaces[0].access.encryption.algorithm, 'RSA_DECRYPT_OAEP_3072_SHA256');
}
},
'should create space with accessType public': async (assert) => {
const alice = new Client(await AgentData.create());
const space = await alice.createSpace('public-space', {
access: { type: 'public' },
skipGatewayAuthorization: true,
});
const auth = await space.createAuthorization(alice);
await alice.addSpace(auth);
assert.equal(space.access.type, 'public');
assert.ok(!('encryption' in space.access)); // public spaces have no encryption provider
const spaces = alice.spaces();
assert.equal(spaces.length, 1);
assert.equal(spaces[0].access.type, 'public');
assert.ok(!('encryption' in spaces[0].access));
},
'should default to public accessType when no accessType is provided': async (assert) => {
const alice = new Client(await AgentData.create());
const space = await alice.createSpace('default-space', {
skipGatewayAuthorization: true,
});
const auth = await space.createAuthorization(alice);
await alice.addSpace(auth);
assert.equal(space.access.type, 'public');
assert.ok(!('encryption' in space.access));
const spaces = alice.spaces();
assert.equal(spaces.length, 1);
assert.equal(spaces[0].access.type, 'public');
assert.ok(!('encryption' in spaces[0].access));
},
'should recover private space from mnemonic preserving accessType': async (assert) => {
const alice = new Client(await AgentData.create());
// Create a private space and get its mnemonic
const originalSpace = await alice.createSpace('recovery-test-private', {
access: {
type: 'private',
encryption: {
provider: 'google-kms',
algorithm: 'RSA_DECRYPT_OAEP_3072_SHA256',
},
},
skipGatewayAuthorization: true,
});
const mnemonic = originalSpace.toMnemonic();
// Create a new agent/client to simulate recovery
const bob = new Client(await AgentData.create());
// Recover the space from mnemonic
const recoveredSpace = await bob.agent.recoverSpace(mnemonic, {
name: 'recovered-private-space',
access: {
type: 'private',
encryption: {
provider: 'google-kms',
algorithm: 'RSA_DECRYPT_OAEP_3072_SHA256',
},
},
});
assert.equal(recoveredSpace.access.type, 'private');
if (recoveredSpace.access.type === 'private') {
assert.equal(recoveredSpace.access.encryption.provider, 'google-kms');
assert.equal(recoveredSpace.access.encryption.algorithm, 'RSA_DECRYPT_OAEP_3072_SHA256');
}
assert.equal(recoveredSpace.name, 'recovered-private-space');
assert.equal(recoveredSpace.did(), originalSpace.did());
// Add the recovered space and verify accessType is preserved
const auth = await recoveredSpace.createAuthorization(bob);
await bob.addSpace(auth);
const spaces = bob.spaces();
assert.equal(spaces.length, 1);
assert.equal(spaces[0].access.type, 'private');
if (spaces[0].access.type === 'private') {
assert.equal(spaces[0].access.encryption.provider, 'google-kms');
assert.equal(spaces[0].access.encryption.algorithm, 'RSA_DECRYPT_OAEP_3072_SHA256');
}
assert.equal(spaces[0].name, 'recovered-private-space');
},
'should recover public space from mnemonic preserving accessType': async (assert) => {
const alice = new Client(await AgentData.create());
// Create a public space and get its mnemonic
const originalSpace = await alice.createSpace('recovery-test-public', {
access: { type: 'public' },
skipGatewayAuthorization: true,
});
const mnemonic = originalSpace.toMnemonic();
// Create a new agent/client to simulate recovery
const bob = new Client(await AgentData.create());
// Recover the space from mnemonic
const recoveredSpace = await bob.agent.recoverSpace(mnemonic, {
name: 'recovered-public-space',
access: { type: 'public' },
});
assert.equal(recoveredSpace.access.type, 'public');
assert.equal(recoveredSpace.name, 'recovered-public-space');
assert.equal(recoveredSpace.did(), originalSpace.did());
// Add the recovered space and verify accessType is preserved
const auth = await recoveredSpace.createAuthorization(bob);
await bob.addSpace(auth);
const spaces = bob.spaces();
assert.equal(spaces.length, 1);
assert.equal(spaces[0].access.type, 'public');
assert.equal(spaces[0].name, 'recovered-public-space');
},
'should recover space from mnemonic with default accessType': async (assert) => {
const alice = new Client(await AgentData.create());
// Create a space and get its mnemonic
const originalSpace = await alice.createSpace('recovery-test-default', {
skipGatewayAuthorization: true,
});
const mnemonic = originalSpace.toMnemonic();
// Create a new agent/client to simulate recovery without specifying accessType
const bob = new Client(await AgentData.create());
// Recover the space from mnemonic without accessType (should default to public)
const recoveredSpace = await bob.agent.recoverSpace(mnemonic, {
name: 'recovered-default-space',
});
assert.equal(recoveredSpace.access.type, 'public');
assert.equal(recoveredSpace.name, 'recovered-default-space');
assert.equal(recoveredSpace.did(), originalSpace.did());
// Add the recovered space and verify accessType defaults to public
const auth = await recoveredSpace.createAuthorization(bob);
await bob.addSpace(auth);
const spaces = bob.spaces();
assert.equal(spaces.length, 1);
assert.equal(spaces[0].access.type, 'public');
assert.equal(spaces[0].name, 'recovered-default-space');
},
'should preserve accessType when renaming space with withName': async (assert) => {
const alice = new Client(await AgentData.create());
// Create a private space
const originalSpace = await alice.createSpace('original-name', {
access: {
type: 'private',
encryption: {
provider: 'google-kms',
algorithm: 'RSA_DECRYPT_OAEP_3072_SHA256',
},
},
skipGatewayAuthorization: true,
});
// Create a renamed version of the space
const renamedSpace = originalSpace.withName('new-name');
assert.equal(renamedSpace.access.type, 'private');
if (renamedSpace.access.type === 'private') {
assert.equal(renamedSpace.access.encryption.provider, 'google-kms');
assert.equal(renamedSpace.access.encryption.algorithm, 'RSA_DECRYPT_OAEP_3072_SHA256');
}
assert.equal(renamedSpace.name, 'new-name');
assert.equal(renamedSpace.did(), originalSpace.did());
// Test with public space too
const publicSpace = await alice.createSpace('public-original', {
access: { type: 'public' },
skipGatewayAuthorization: true,
});
const renamedPublicSpace = publicSpace.withName('public-renamed');
assert.equal(renamedPublicSpace.access.type, 'public');
assert.ok(!('encryption' in renamedPublicSpace.access));
assert.equal(renamedPublicSpace.name, 'public-renamed');
assert.equal(renamedPublicSpace.did(), publicSpace.did());
},
'should import private space from delegation preserving accessType': async (assert) => {
const alice = new Client(await AgentData.create());
const bob = new Client(await AgentData.create());
// Alice creates a private space
const space = await alice.createSpace('delegation-test-private', {
access: {
type: 'private',
encryption: {
provider: 'google-kms',
algorithm: 'RSA_DECRYPT_OAEP_3072_SHA256',
},
},
skipGatewayAuthorization: true,
});
// Alice creates an authorization for the space with full access
await alice.addSpace(await space.createAuthorization(alice, {
access: { '*': {} },
expiration: Infinity,
}));
await alice.setCurrentSpace(space.did());
// Alice creates a delegation for Bob
const delegation = await alice.createDelegation(bob.agent, ['*']);
// Bob imports the space from delegation
await bob.addSpace(delegation);
const bobSpaces = bob.spaces();
assert.equal(bobSpaces.length, 1);
assert.equal(bobSpaces[0].access.type, 'private');
if (bobSpaces[0].access.type === 'private') {
assert.equal(bobSpaces[0].access.encryption.provider, 'google-kms');
assert.equal(bobSpaces[0].access.encryption.algorithm, 'RSA_DECRYPT_OAEP_3072_SHA256');
}
assert.equal(bobSpaces[0].did(), space.did());
},
'should import public space from delegation preserving accessType': async (assert) => {
const alice = new Client(await AgentData.create());
const bob = new Client(await AgentData.create());
// Alice creates a public space
const space = await alice.createSpace('delegation-test-public', {
access: { type: 'public' },
skipGatewayAuthorization: true,
});
// Alice creates an authorization for the space with full access
await alice.addSpace(await space.createAuthorization(alice, {
access: { '*': {} },
expiration: Infinity,
}));
await alice.setCurrentSpace(space.did());
// Alice creates a delegation for Bob
const delegation = await alice.createDelegation(bob.agent, ['*']);
// Bob imports the space from delegation
await bob.addSpace(delegation);
const bobSpaces = bob.spaces();
assert.equal(bobSpaces.length, 1);
assert.equal(bobSpaces[0].access.type, 'public');
assert.ok(!('encryption' in bobSpaces[0].access));
assert.equal(bobSpaces[0].did(), space.did());
},
'should handle delegation from space with missing accessType (backwards compatibility)': async (assert) => {
const alice = new Client(await AgentData.create());
const bob = new Client(await AgentData.create());
// Alice creates a space without specifying accessType (defaults to public)
const space = await alice.createSpace('delegation-test-default', {
skipGatewayAuthorization: true,
});
// Alice creates an authorization for the space with full access
await alice.addSpace(await space.createAuthorization(alice, {
access: { '*': {} },
expiration: Infinity,
}));
await alice.setCurrentSpace(space.did());
// Alice creates a delegation for Bob
const delegation = await alice.createDelegation(bob.agent, ['*']);
// Bob imports the space from delegation
await bob.addSpace(delegation);
const bobSpaces = bob.spaces();
assert.equal(bobSpaces.length, 1);
assert.equal(bobSpaces[0].access.type, 'public'); // Should default to public
assert.ok(!('encryption' in bobSpaces[0].access));
assert.equal(bobSpaces[0].did(), space.did());
},
'should add space': async () => {
const alice = new Client(await AgentData.create());
const bob = new Client(await AgentData.create());
const space = await alice.createSpace('new-space', {
skipGatewayAuthorization: true,
});
await alice.addSpace(await space.createAuthorization(alice, {
access: { '*': {} },
expiration: Infinity,
}));
await alice.setCurrentSpace(space.did());
const delegation = await alice.createDelegation(bob.agent, ['*']);
assert.equal(bob.spaces().length, 0);
await bob.addSpace(delegation);
assert.equal(bob.spaces().length, 1);
const spaces = bob.spaces();
assert.equal(spaces.length, 1);
assert.equal(spaces[0].did(), space.did());
},
'should create a space with recovery account': async (assert, { client, mail, connect, grantAccess }) => {
// Step 1: Create a client for Alice and login
const aliceEmail = 'alice@web.mail';
const aliceLogin = client.login(aliceEmail);
const message = await mail.take();
assert.deepEqual(message.to, aliceEmail);
await grantAccess(message);
const aliceAccount = await aliceLogin;
// Step 2: Alice creates a space with her account as the recovery account
const space = await client.createSpace('recovery-space-test', {
account: aliceAccount, // The account is the recovery account
skipGatewayAuthorization: true,
});
assert.ok(space);
// Step 3: Verify the recovery account by connecting to a new device
const secondClient = await connect();
const secondLogin = secondClient.login(aliceEmail);
const secondMessage = await mail.take();
assert.deepEqual(secondMessage.to, aliceEmail);
await grantAccess(secondMessage);
const aliceAccount2 = await secondLogin;
await secondClient.addSpace(await space.createAuthorization(aliceAccount2));
await secondClient.setCurrentSpace(space.did());
// Step 4: Verify the space is accessible from the new device
const spaceInfo = await secondClient.capability.space.info(space.did());
assert.ok(spaceInfo);
},
'should create a space without recovery account and fail access from another device': async (assert, { client, mail, connect, grantAccess }) => {
// Step 1: Create a client for Alice and login
const aliceEmail = 'alice@web.mail';
const aliceLogin = client.login(aliceEmail);
const message = await mail.take();
assert.deepEqual(message.to, aliceEmail);
await grantAccess(message);
await aliceLogin;
// Step 2: Alice creates a space without providing a recovery account
const space = await client.createSpace('no-recovery-space-test', {
skipGatewayAuthorization: true,
});
assert.ok(space);
// Step 3: Attempt to access the space from a new device
const secondClient = await connect();
const secondLogin = secondClient.login(aliceEmail);
const secondMessage = await mail.take();
assert.deepEqual(secondMessage.to, aliceEmail);
await grantAccess(secondMessage);
const aliceAccount2 = await secondLogin;
// Step 4: Add the space to the new device and set it as current space
await secondClient.addSpace(await space.createAuthorization(aliceAccount2));
await secondClient.setCurrentSpace(space.did());
// Step 5: Verify the space is accessible from the new device
await assert.rejects(secondClient.capability.space.info(space.did()), {
message: `no proofs available for resource ${space.did()} and ability space/info`,
});
},
'should fail to create a space due to provisioning error': async (assert, { client, mail, grantAccess }) => {
// Step 1: Create a client for Alice and login
const aliceEmail = 'alice@web.mail';
const aliceLogin = client.login(aliceEmail);
const message = await mail.take();
assert.deepEqual(message.to, aliceEmail);
await grantAccess(message);
const aliceAccount = await aliceLogin;
// Step 2: Mock the provisioning to fail
const originalProvision = aliceAccount.provision;
aliceAccount.provision = async () => ({
error: { name: 'ProvisionError', message: 'Provisioning failed' },
});
// Step 3: Attempt to create a space with the account
await assert.rejects(client.createSpace('provision-fail-space-test', {
account: aliceAccount,
}), {
message: 'failed to provision account: Provisioning failed',
});
// Restore the original provision method
aliceAccount.provision = originalProvision;
},
'should fail to create a space due to delegate access error': async (assert, { client, mail, connect, grantAccess }) => {
// Step 1: Create a client for Alice and login
const aliceEmail = 'alice@web.mail';
const aliceLogin = client.login(aliceEmail);
const message = await mail.take();
assert.deepEqual(message.to, aliceEmail);
await grantAccess(message);
const aliceAccount = await aliceLogin;
// Step 2: Mock the delegate access to fail
const originalDelegate = client.capability.access.delegate;
client.capability.access.delegate = async (...args) => {
return {
error: { name: 'DelegateError', message: 'Delegation failed' },
};
};
// Step 3: Attempt to create a space with the account
// Skip gateway authorization to avoid other potential failures
let errorCaught = false;
try {
await client.createSpace('delegate-fail-space-test', {
account: aliceAccount,
skipGatewayAuthorization: true,
});
console.log('ERROR: Space creation should have failed but succeeded');
}
catch (error) {
console.log('Error caught as expected:',
/** @type {Error} */ (error).message);
errorCaught = true;
assert.equal(
/** @type {Error} */ (error).message, 'failed to authorize recovery account: Delegation failed');
}
assert.ok(errorCaught, 'Expected error was not thrown');
// Restore the original delegate method
client.capability.access.delegate = originalDelegate;
},
}),
shareSpace: Test.withContext({
'should share the space with another account': async (assert, { client: aliceClient, mail, grantAccess, connection }) => {
// Step 1: Create a client for Alice and login
const aliceEmail = 'alice@web.mail';
const aliceLogin = aliceClient.login(aliceEmail);
const message = await mail.take();
assert.deepEqual(message.to, aliceEmail);
await grantAccess(message);
const aliceAccount = await aliceLogin;
// Step 2: Alice creates a space
const space = await aliceClient.createSpace('share-space-test', {
account: aliceAccount,
skipGatewayAuthorization: true,
});
assert.ok(space);
// Step 3: Alice shares the space with Bob
const bobEmail = 'bob@web.mail';
await aliceClient.shareSpace(bobEmail, space.did());
// Step 4: Bob access his device and his device gets authorized
const bobAccount = Absentee.from({ id: DIDMailto.fromEmail(bobEmail) });
const bobAgentData = await AgentData.create();
const bobClient = await Agent.create(bobAgentData, {
connection,
});
// Authorization
await requestAccess(bobClient, bobAccount, [{ can: '*' }]);
await confirmConfirmationUrl(bobClient.connection, await mail.take());
// Step 5: Claim Access to the shared space
await claimAccess(bobClient, bobClient.issuer.did(), {
addProofs: true,
});
// Step 6: Bob verifies access to the space
const spaceInfo = await bobClient.getSpaceInfo(space.did());
assert.ok(spaceInfo);
assert.equal(spaceInfo.did, space.did());
// Step 7: The shared space should be part of Bob's spaces
const spaces = bobClient.spaces;
assert.equal(spaces.size, 1);
assert.equal(spaces.get(space.did())?.name, space.name);
// Step 8: Make sure Alice and Bob's clients/devices are different
assert.notEqual(aliceClient.did(), bobClient.did());
},
'should fail to share the space if the delegate call returns an error': async (assert, { client, mail, grantAccess }) => {
// Step 1: Create a client for Alice and login
const aliceEmail = 'alice@web.mail';
const aliceLogin = client.login(aliceEmail);
const message = await mail.take();
assert.deepEqual(message.to, aliceEmail);
await grantAccess(message);
const aliceAccount = await aliceLogin;
// Step 2: Alice creates a space
const space = await client.createSpace('share-space-delegate-fail-test', {
account: aliceAccount,
skipGatewayAuthorization: true,
});
assert.ok(space);
// Step 3: Mock the delegate call to return an error
const originalDelegate = client.capability.access.delegate;
// @ts-ignore
client.capability.access.delegate = async () => {
return { error: { message: 'Delegate failed' } };
};
// Step 4: Attempt to share the space with Bob and expect failure
const bobEmail = 'bob@web.mail';
await assert.rejects(client.shareSpace(bobEmail, space.did()), {
message: `failed to share space with ${bobEmail}: Delegate failed`,
});
// Restore the original delegate method
client.capability.access.delegate = originalDelegate;
},
'should reset current space when sharing': async (assert, { client, mail, grantAccess }) => {
// Step 1: Create a client for Alice and login
const aliceEmail = 'alice@web.mail';
const aliceLogin = client.login(aliceEmail);
const message = await mail.take();
assert.deepEqual(message.to, aliceEmail);
await grantAccess(message);
const aliceAccount = await aliceLogin;
// Step 2: Alice creates a space
const spaceA = await client.createSpace('test-space-a', {
account: aliceAccount,
skipGatewayAuthorization: true,
});
assert.ok(spaceA);
// Step 3: Alice creates another space to share with a friend
const spaceB = await client.createSpace('test-space-b', {
account: aliceAccount,
skipGatewayAuthorization: true,
});
assert.ok(spaceB);
// Step 4: Alice set the current space to space A and shares the space B with Bob
await client.setCurrentSpace(spaceA.did());
await client.shareSpace('bob@web.mail', spaceB.did());
// Step 5: Check that current space from Alice is still space A
const currentSpace = client.currentSpace();
assert.equal(currentSpace?.did(), spaceA.did(), 'current space is not space A');
},
}),
authorizeGateway: Test.withContext({
'should explicitly authorize a gateway to serve content from a space': async (assert, { mail, grantAccess, connection }) => {
// Step 1: Create a client for Alice and login
const aliceClient = new Client(await AgentData.create({
principal: alice,
}), {
// @ts-ignore
serviceConf: {
access: connection,
upload: connection,
},
});
const aliceEmail = 'alice@web.mail';
const aliceLogin = aliceClient.login(aliceEmail);
const message = await mail.take();
assert.deepEqual(message.to, aliceEmail);
await grantAccess(message);
const aliceAccount = await aliceLogin;
// Step 2: Alice creates a space
const spaceA = await aliceClient.createSpace('authorize-gateway-space', {
account: aliceAccount,
skipGatewayAuthorization: true,
});
assert.ok(spaceA);
const gatewayService = getContentServeMockService();
const gatewayConnection = getConnection(gateway, gatewayService).connection;
// Step 3: Alice authorizes the gateway to serve content from the space
const delegationResult = await authorizeContentServe(aliceClient, spaceA, gatewayConnection);
assert.ok(delegationResult.ok);
const { delegation } = delegationResult.ok;
// Step 4: Find the delegation for the default gateway
assert.equal(delegation.audience.did(), gateway.did());
assert.ok(delegation.capabilities.some((c) => c.can === SpaceCapability.contentServe.can &&
c.with === spaceA.did()));
},
'should automatically authorize a gateway to serve content from a space when the space is created': async (assert, { mail, grantAccess, connection }) => {
// Step 1: Create a client for Alice and login
const aliceClient = new Client(await AgentData.create({
principal: alice,
}), {
// @ts-ignore
serviceConf: {
access: connection,
upload: connection,
},
});
const aliceEmail = 'alice@web.mail';
const aliceLogin = aliceClient.login(aliceEmail);
const message = await mail.take();
assert.deepEqual(message.to, aliceEmail);
await grantAccess(message);
const aliceAccount = await aliceLogin;
// Step 2: Alice creates a space
const gatewayService = getContentServeMockService();
const gatewayConnection = getConnection(gateway, gatewayService).connection;
try {
const spaceA = await aliceClient.createSpace('authorize-gateway-space', {
account: aliceAccount,
authorizeGatewayServices: [gatewayConnection],
});
assert.ok(spaceA, 'should create the space');
}
catch (error) {
assert.fail(error, 'should not throw when creating the space');
}
},
'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(await AgentData.create({
principal: alice,
}), {
// @ts-ignore
serviceConf: {
access: connection,
upload: connection,
gateway: gatewayServiceConnection({
id: gateway,
url: new URL('http://localhost:5001'),
}),
},
});
const aliceEmail = 'alice@web.mail';
const aliceLogin = aliceClient.login(aliceEmail);
const message = await mail.take();
assert.deepEqual(message.to, aliceEmail);
await grantAccess(message);
const aliceAccount = await aliceLogin;
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 }) => {
// Step 1: Create a client for Alice and login
const aliceClient = new Client(await AgentData.create({
principal: alice,
}), {
// @ts-ignore
serviceConf: {
access: connection,
upload: connection,
},
});
const aliceEmail = 'alice@web.mail';
const aliceLogin = aliceClient.login(aliceEmail);
const message = await mail.take();
assert.deepEqual(message.to, aliceEmail);
await grantAccess(message);
const aliceAccount = await aliceLogin;
// Step 2: Alice creates a space
const gatewayService = getContentServeMockService({
error: Server.fail('Content serve service can not process the invocation').error,
});
const gatewayConnection = getConnection(gateway, gatewayService).connection;
try {
await aliceClient.createSpace('authorize-gateway-space', {
account: aliceAccount,
authorizeGatewayServices: [gatewayConnection],
});
assert.fail('should not create the space');
}
catch (error) {
assert.match(
// @ts-expect-error
error.message, /failed to publish delegation for audience/, 'should throw when publishing the delegation');
}
},
}),
proofs: {
'should get proofs': async (assert) => {
const alice = new Client(await AgentData.create());
const bob = new Client(await AgentData.create());
const space = await alice.createSpace('proof-space', {
skipGatewayAuthorization: true,
});
await alice.addSpace(await space.createAuthorization(alice));
await alice.setCurrentSpace(space.did());
const delegation = await alice.createDelegation(bob.agent, ['store/*']);
await bob.addProof(delegation);
const proofs = bob.proofs();
assert.equal(proofs.length, 1);
assert.equal(proofs[0].cid.toString(), delegation.cid.toString());
},
},
delegations: {
'should get delegations': async (assert) => {
const alice = new Client(await AgentData.create());
const bob = new Client(await AgentData.create());
const space = await alice.createSpace('test', {
skipGatewayAuthorization: true,
});
await alice.addSpace(await space.createAuthorization(alice));
await alice.setCurrentSpace(space.did());
const name = `delegation-${Date.now()}`;
const delegation = await alice.createDelegation(bob.agent, ['upload/*', 'store/*'], {
audienceMeta: { type: 'device', name },
});
const delegations = alice.delegations();
assert.equal(delegations.length, 1);
assert.equal(delegations[0].cid.toString(), delegation.cid.toString());
assert.equal(delegations[0].meta()?.audience?.name, name);
},
},
revokeDelegation: Test.withContext({
'should revoke a delegation by CID': async (assert, { connection }) => {
const alice = new Client(await AgentData.create(), {
// @ts-ignore
serviceConf: {
access: connection,
upload: connection,
},
});
const bob = new Client(await AgentData.create(), {
// @ts-ignore
serviceConf: {
access: connection,
upload: connection,
},
});
const space = await alice.createSpace('test', {
skipGatewayAuthorization: true,
});
await alice.addSpace(await space.createAuthorization(alice, {
access: { '*': {} },
}));
await alice.setCurrentSpace(space.did());
const name = `delegation-${Date.now()}`;
const delegation = await alice.createDelegation(bob.agent, ['*'], {
audienceMeta: { type: 'device', name },
});
const result = await alice.revokeDelegation(delegation.cid);
assert.ok(result.ok);
},
'should fail to revoke a delegation it does not know about': async (assert) => {
const alice = new Client(await AgentData.create());
const bob = new Client(await AgentData.create());
const space = await alice.createSpace('test', {
skipGatewayAuthorization: true,
});
await alice.addSpace(await space.createAuthorization(alice));
await alice.setCurrentSpace(space.did());
const name = `delegation-${Date.now()}`;
const delegation = await alice.createDelegation(bob.agent, ['space/*'], {
audienceMeta: { type: 'device', name },
});
const result = await bob.revokeDelegation(delegation.cid);
assert.ok(result.error, 'revoke succeeded when it should not have');
},
}),
defaultProvider: {
'should return the connection ID': async (assert) => {
const alice = new Client(await AgentData.create());
assert.equal(alice.defaultProvider(), 'did:web:up.storacha.network');
},
},
capability: {
'should allow typed access to capability specific clients': async () => {
const client = new Client(await AgentData.create());
assert.equal(typeof client.capability.access.authorize, 'function');
assert.equal(typeof client.capability.access.claim, 'function');
assert.equal(typeof client.capability.space.info, 'function');
assert.equal(typeof client.capability.blob.add, 'function');
assert.equal(typeof client.capability.blob.list, 'function');
assert.equal(typeof client.capability.blob.remove, 'function');
assert.equal(typeof client.capability.upload.add, 'function');
assert.equal(typeof client.capability.upload.list, 'function');
assert.equal(typeof client.capability.upload.remove, 'function');
},
},
remove: Test.withContext({
'should remove an uploaded file from the service with its shards': async (assert, { connection, provisionsStorage, uploadTable }) => {
const bytes = await randomBytes(128);
const alice = new Client(await AgentData.create(), {
// @ts-ignore
serviceConf: {
access: connection,
upload: connection,
},
receiptsEndpoint: new URL(receiptsEndpoint),
});
// setup space
const space = await alice.createSpace('upload-test', {
skipGatewayAuthorization: true,
});
const auth = await space.createAuthorization(alice);
await alice.addSpace(auth);
await alice.setCurrentSpace(space.did());
// Then we setup a billing for this account
await provisionsStorage.put({
// @ts-expect-error
provider: connection.id.did(),
account: alice.agent.did(),
consumer: space.did(),
});
const content = new Blob([bytes]);
const fileLink = await alice.uploadFile(content);
assert.deepEqual(await uploadTable.exists(space.did(), fileLink), {
ok: true,
});
assert.deepEqual(await alice
.remove(fileLink, { shards: true })
.then((ok) => ({ ok: {} }))
.catch((error) => error), { ok: {} });
assert.deepEqual(await uploadTable.exists(space.did(), fileLink), {
ok: false,
});
},
'should remove an uploaded file from the service without its shards by default': async (assert, { connection, provisionsStorage, uploadTable }) => {
const bytes = await randomBytes(128);
const alice = new Client(await AgentData.create(), {
// @ts-ignore
serviceConf: {
access: connection,
upload: connection,
},
receiptsEndpoint: new URL(receiptsEndpoint),
});
// setup space
const space = await alice.createSpace('upload-test', {
skipGatewayAuthorization: true,
});
const auth = await space.createAuthorization(alice);
await alice.addSpace(auth);
await alice.setCurrentSpace(space.did());
// Then we setup a billing for this account
await provisionsStorage.put({
// @ts-expect-error
provider: connection.id.did(),
account: alice.agent.did(),
consumer: space.did(),
});
const content = new Blob([bytes]);
const fileLink = await alice.uploadFile(content);
assert.deepEqual(await uploadTable.exists(space.did(), fileLink), {
ok: true,
});
assert.deepEqual(await alice
.remove(fileLink)
.then((ok) => ({ ok: {} }))
.catch((error) => error), { ok: {} });
assert.deepEqual(await uploadTable.exists(space.did(), fileLink), {
ok: false,
});
},
'should fail to remove uploaded shards if upload is not found': async (assert, { connection }) => {
const bytes =