@pdsjs/spaces 2.0.2 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -3
- package/src/admin.d.ts +10 -0
- package/src/admin.js +115 -0
- package/src/authority.d.ts +3 -3
- package/src/authority.js +4 -4
- package/src/dpop.d.ts +16 -0
- package/src/dpop.js +36 -0
- package/src/handlers/auth.d.ts +14 -18
- package/src/handlers/auth.js +41 -14
- package/src/handlers/manage.d.ts +4 -6
- package/src/handlers/manage.js +3 -3
- package/src/handlers/read.d.ts +4 -6
- package/src/handlers/read.js +2 -2
- package/src/handlers/write.d.ts +15 -6
- package/src/handlers/write.js +78 -5
- package/src/notify.d.ts +2 -4
- package/src/notify.js +1 -1
- package/src/routes.d.ts +7 -6
- package/src/routes.js +4 -2
- package/src/service-auth.d.ts +4 -6
- package/src/service-auth.js +49 -27
- package/src/token.d.ts +33 -7
- package/src/token.js +80 -16
- package/src/writer.d.ts +11 -0
- package/src/writer.js +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pdsjs/spaces",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"types": "./src/index.d.ts",
|
|
@@ -20,10 +20,11 @@
|
|
|
20
20
|
"./car": "./src/car.js",
|
|
21
21
|
"./token": "./src/token.js",
|
|
22
22
|
"./authority": "./src/authority.js",
|
|
23
|
-
"./service-auth": "./src/service-auth.js"
|
|
23
|
+
"./service-auth": "./src/service-auth.js",
|
|
24
|
+
"./admin": "./src/admin.js"
|
|
24
25
|
},
|
|
25
26
|
"dependencies": {
|
|
26
|
-
"@pdsjs/core": "2.
|
|
27
|
+
"@pdsjs/core": "2.3.0"
|
|
27
28
|
},
|
|
28
29
|
"publishConfig": {
|
|
29
30
|
"access": "public"
|
package/src/admin.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {Object} ctx
|
|
3
|
+
* @param {import('@pdsjs/core/ports').SpaceStoragePort} ctx.spaceStorage
|
|
4
|
+
* @param {() => Promise<string|null>} ctx.getDid
|
|
5
|
+
* @returns {import('@pdsjs/core/ports').SpaceAdminPort}
|
|
6
|
+
*/
|
|
7
|
+
export declare function createSpaceAdmin({ spaceStorage, getDid }: {
|
|
8
|
+
spaceStorage: import('@pdsjs/core/ports').SpaceStoragePort;
|
|
9
|
+
getDid: () => Promise<string | null>;
|
|
10
|
+
}): import('@pdsjs/core/ports').SpaceAdminPort;
|
package/src/admin.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// @pdsjs/spaces/admin - space management for the account page.
|
|
2
|
+
//
|
|
3
|
+
// The account page runs behind the owner's own session, so these skip the
|
|
4
|
+
// scope checks the XRPC management endpoints apply to third-party callers.
|
|
5
|
+
// They mutate the same storage the same way com.atproto.simplespace.* does.
|
|
6
|
+
|
|
7
|
+
import { cborDecode, createTid } from '@pdsjs/core/repo';
|
|
8
|
+
import { LtHash } from './lthash.js';
|
|
9
|
+
import { makeSpaceRow } from './space-row.js';
|
|
10
|
+
import { formatSpaceUri } from './uri.js';
|
|
11
|
+
import { applyWrites } from './writer.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {Object} ctx
|
|
15
|
+
* @param {import('@pdsjs/core/ports').SpaceStoragePort} ctx.spaceStorage
|
|
16
|
+
* @param {() => Promise<string|null>} ctx.getDid
|
|
17
|
+
* @returns {import('@pdsjs/core/ports').SpaceAdminPort}
|
|
18
|
+
*/
|
|
19
|
+
export function createSpaceAdmin({ spaceStorage, getDid }) {
|
|
20
|
+
/** @returns {Promise<string>} */
|
|
21
|
+
async function requireDid() {
|
|
22
|
+
const did = await getDid();
|
|
23
|
+
if (!did) throw new Error('server not initialised');
|
|
24
|
+
return did;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} space
|
|
29
|
+
* @returns {Promise<void>}
|
|
30
|
+
*/
|
|
31
|
+
async function requireOwnedSpace(space) {
|
|
32
|
+
const row = await spaceStorage.getSpace(space);
|
|
33
|
+
if (!row?.isOwner || row.deletedAt) {
|
|
34
|
+
throw new Error(`no such space: ${space}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
async createSpace({ type, skey }) {
|
|
40
|
+
if (!type.includes('.')) {
|
|
41
|
+
throw new Error(`space type must be an NSID: ${type}`);
|
|
42
|
+
}
|
|
43
|
+
if (skey && skey.length > 512) {
|
|
44
|
+
throw new Error('skey is too long');
|
|
45
|
+
}
|
|
46
|
+
const did = await requireDid();
|
|
47
|
+
const uri = formatSpaceUri({
|
|
48
|
+
spaceDid: did,
|
|
49
|
+
spaceType: type,
|
|
50
|
+
skey: skey || createTid(),
|
|
51
|
+
});
|
|
52
|
+
if (await spaceStorage.getSpace(uri)) {
|
|
53
|
+
throw new Error(`space already exists: ${uri}`);
|
|
54
|
+
}
|
|
55
|
+
await spaceStorage.putSpace(makeSpaceRow(uri, { isOwner: true }));
|
|
56
|
+
// The owner joins their own space: a member-list space checks membership
|
|
57
|
+
// before it issues a credential, and that is the default policy.
|
|
58
|
+
await spaceStorage.addMember(uri, did);
|
|
59
|
+
return { uri };
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
async addMember(space, did) {
|
|
63
|
+
if (!did.startsWith('did:')) {
|
|
64
|
+
throw new Error(`not a DID: ${did}`);
|
|
65
|
+
}
|
|
66
|
+
await requireOwnedSpace(space);
|
|
67
|
+
await spaceStorage.addMember(space, did);
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
async removeMember(space, did) {
|
|
71
|
+
const owner = await requireDid();
|
|
72
|
+
if (did === owner) {
|
|
73
|
+
throw new Error('the owner cannot leave their own space');
|
|
74
|
+
}
|
|
75
|
+
await requireOwnedSpace(space);
|
|
76
|
+
await spaceStorage.removeMember(space, did);
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
async getRecord(space, collection, rkey) {
|
|
80
|
+
await requireOwnedSpace(space);
|
|
81
|
+
const row = await spaceStorage.getSpaceRecord(space, collection, rkey);
|
|
82
|
+
return row ? /** @type {Object} */ (cborDecode(row.value)) : null;
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
async createRecord(space, collection, rkey, record) {
|
|
86
|
+
await writeRecord(space, 'create', collection, rkey, record);
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
async putRecord(space, collection, rkey, record) {
|
|
90
|
+
await writeRecord(space, 'put', collection, rkey, record);
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {string} space
|
|
96
|
+
* @param {'create'|'put'} action
|
|
97
|
+
* @param {string} collection
|
|
98
|
+
* @param {string} rkey
|
|
99
|
+
* @param {Object} record
|
|
100
|
+
* @returns {Promise<void>}
|
|
101
|
+
*/
|
|
102
|
+
async function writeRecord(space, action, collection, rkey, record) {
|
|
103
|
+
const did = await requireDid();
|
|
104
|
+
await requireOwnedSpace(space);
|
|
105
|
+
const commit = await applyWrites(spaceStorage, {
|
|
106
|
+
space,
|
|
107
|
+
writes: [{ action, collection, rkey, record }],
|
|
108
|
+
});
|
|
109
|
+
// What fireNotifyWrite does after an XRPC write into the owner's own
|
|
110
|
+
// space: record the owner in the writer set, so listRepos knows this
|
|
111
|
+
// repo before its first push.
|
|
112
|
+
const hash = await new LtHash(commit.setHash).digest();
|
|
113
|
+
await spaceStorage.putSpaceWriter(space, did, commit.rev, hash);
|
|
114
|
+
}
|
|
115
|
+
}
|
package/src/authority.d.ts
CHANGED
|
@@ -51,7 +51,7 @@ export declare function spaceHostAudience(spaceDid: string): string;
|
|
|
51
51
|
/**
|
|
52
52
|
* Build the `getSigningKey` callback verifySpaceToken expects.
|
|
53
53
|
*
|
|
54
|
-
* @param {(
|
|
55
|
-
* @returns {(iss: string, kid?: string) => Promise<string>}
|
|
54
|
+
* @param {import('@pdsjs/core/ports').DidResolverPort} resolveDid
|
|
55
|
+
* @returns {(iss: string, kid?: string, forceRefresh?: boolean) => Promise<string>}
|
|
56
56
|
*/
|
|
57
|
-
export declare function didKeyResolver(resolveDid: (
|
|
57
|
+
export declare function didKeyResolver(resolveDid: import('@pdsjs/core/ports').DidResolverPort): (iss: string, kid?: string, forceRefresh?: boolean) => Promise<string>;
|
package/src/authority.js
CHANGED
|
@@ -150,12 +150,12 @@ export function spaceHostAudience(spaceDid) {
|
|
|
150
150
|
/**
|
|
151
151
|
* Build the `getSigningKey` callback verifySpaceToken expects.
|
|
152
152
|
*
|
|
153
|
-
* @param {(
|
|
154
|
-
* @returns {(iss: string, kid?: string) => Promise<string>}
|
|
153
|
+
* @param {import('@pdsjs/core/ports').DidResolverPort} resolveDid
|
|
154
|
+
* @returns {(iss: string, kid?: string, forceRefresh?: boolean) => Promise<string>}
|
|
155
155
|
*/
|
|
156
156
|
export function didKeyResolver(resolveDid) {
|
|
157
|
-
return async (iss, kid) => {
|
|
158
|
-
const didDoc = await resolveDid(iss);
|
|
157
|
+
return async (iss, kid, forceRefresh = false) => {
|
|
158
|
+
const didDoc = await resolveDid(iss, { forceRefresh });
|
|
159
159
|
if (!didDoc) {
|
|
160
160
|
throw new SpaceAuthorityError(`Could not resolve ${iss}`, 'DidNotFound');
|
|
161
161
|
}
|
package/src/dpop.d.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The key an application asks a credential to be bound to, read from the proof
|
|
3
|
+
* it presents when it asks.
|
|
4
|
+
*
|
|
5
|
+
* A thumbprint named in the request body is a claim: any holder of the
|
|
6
|
+
* delegation token can make it about a key somebody else controls. A proof
|
|
7
|
+
* demonstrates possession of the key it names. The proof carries no `ath`,
|
|
8
|
+
* because a delegation token is an authorization grant rather than an access
|
|
9
|
+
* token, so there is no bound token to hash.
|
|
10
|
+
*
|
|
11
|
+
* @param {Request} request
|
|
12
|
+
* @returns {Promise<string|null>} the proof's thumbprint, or null when the
|
|
13
|
+
* request carries no proof
|
|
14
|
+
* @throws {SpaceTokenError} when a proof is present but invalid or replayed
|
|
15
|
+
*/
|
|
16
|
+
export declare function credentialBindingFromProof(request: Request): Promise<string | null>;
|
|
1
17
|
/**
|
|
2
18
|
* Check the DPoP proof presented with a space credential.
|
|
3
19
|
*
|
package/src/dpop.js
CHANGED
|
@@ -43,6 +43,42 @@ function firstUse(jti, now) {
|
|
|
43
43
|
return true;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* The key an application asks a credential to be bound to, read from the proof
|
|
48
|
+
* it presents when it asks.
|
|
49
|
+
*
|
|
50
|
+
* A thumbprint named in the request body is a claim: any holder of the
|
|
51
|
+
* delegation token can make it about a key somebody else controls. A proof
|
|
52
|
+
* demonstrates possession of the key it names. The proof carries no `ath`,
|
|
53
|
+
* because a delegation token is an authorization grant rather than an access
|
|
54
|
+
* token, so there is no bound token to hash.
|
|
55
|
+
*
|
|
56
|
+
* @param {Request} request
|
|
57
|
+
* @returns {Promise<string|null>} the proof's thumbprint, or null when the
|
|
58
|
+
* request carries no proof
|
|
59
|
+
* @throws {SpaceTokenError} when a proof is present but invalid or replayed
|
|
60
|
+
*/
|
|
61
|
+
export async function credentialBindingFromProof(request) {
|
|
62
|
+
const proof = request.headers.get('dpop');
|
|
63
|
+
if (!proof) return null;
|
|
64
|
+
|
|
65
|
+
/** @type {import('@pdsjs/core/oauth').DpopProofResult} */
|
|
66
|
+
let parsed;
|
|
67
|
+
try {
|
|
68
|
+
parsed = await parseDpopProof(proof, request.method, request.url);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
throw new SpaceTokenError(
|
|
71
|
+
err instanceof Error ? err.message : String(err),
|
|
72
|
+
'BadDpopProof',
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!firstUse(parsed.jti, Date.now())) {
|
|
77
|
+
throw new SpaceTokenError('DPoP proof replayed', 'BadDpopProof');
|
|
78
|
+
}
|
|
79
|
+
return parsed.jkt;
|
|
80
|
+
}
|
|
81
|
+
|
|
46
82
|
/**
|
|
47
83
|
* Check the DPoP proof presented with a space credential.
|
|
48
84
|
*
|
package/src/handlers/auth.d.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* @param {Object} ctx
|
|
3
3
|
* @param {import('@pdsjs/core/ports').SpaceStoragePort} ctx.spaceStorage
|
|
4
4
|
* @param {() => Promise<string|null>} ctx.getDid
|
|
5
|
-
* @param {() => Promise<
|
|
6
|
-
* @param {(
|
|
5
|
+
* @param {() => Promise<import('../token.js').SpaceSigner>} ctx.getSigner
|
|
6
|
+
* @param {import('@pdsjs/core/ports').DidResolverPort} ctx.resolveDid
|
|
7
7
|
* @param {import('@pdsjs/core/ports').SignatureVerifierPort} ctx.verifier
|
|
8
8
|
* @param {typeof fetch} [ctx.fetch]
|
|
9
9
|
* @returns {import('@pdsjs/core/pds').Routes}
|
|
@@ -11,10 +11,8 @@
|
|
|
11
11
|
export declare function createAuthRoutes(ctx: {
|
|
12
12
|
spaceStorage: import('@pdsjs/core/ports').SpaceStoragePort;
|
|
13
13
|
getDid: () => Promise<string | null>;
|
|
14
|
-
getSigner: () => Promise<
|
|
15
|
-
|
|
16
|
-
}>;
|
|
17
|
-
resolveDid: (did: string) => Promise<any>;
|
|
14
|
+
getSigner: () => Promise<import('../token.js').SpaceSigner>;
|
|
15
|
+
resolveDid: import('@pdsjs/core/ports').DidResolverPort;
|
|
18
16
|
verifier: import('@pdsjs/core/ports').SignatureVerifierPort;
|
|
19
17
|
fetch?: typeof fetch;
|
|
20
18
|
}): import('@pdsjs/core/pds').Routes;
|
|
@@ -24,14 +22,14 @@ export declare function createAuthRoutes(ctx: {
|
|
|
24
22
|
*
|
|
25
23
|
* @param {Object} ctx
|
|
26
24
|
* @param {() => Promise<string|null>} ctx.getDid
|
|
27
|
-
* @param {(
|
|
25
|
+
* @param {import('@pdsjs/core/ports').DidResolverPort} ctx.resolveDid
|
|
28
26
|
* @param {import('@pdsjs/core/ports').SignatureVerifierPort} ctx.verifier
|
|
29
27
|
* @returns {(request: Request, space: string, auth: {did: string}|null) => Promise<Response|null>}
|
|
30
28
|
* a Response to return, or null to proceed
|
|
31
29
|
*/
|
|
32
30
|
export declare function createReadAuthorizer({ getDid, resolveDid, verifier }: {
|
|
33
31
|
getDid: () => Promise<string | null>;
|
|
34
|
-
resolveDid: (
|
|
32
|
+
resolveDid: import('@pdsjs/core/ports').DidResolverPort;
|
|
35
33
|
verifier: import('@pdsjs/core/ports').SignatureVerifierPort;
|
|
36
34
|
}): (request: Request, space: string, auth: {
|
|
37
35
|
did: string;
|
|
@@ -58,7 +56,7 @@ export declare function credentialFromRequest(request: Request): string | null;
|
|
|
58
56
|
* @param {Request} opts.request - the request the DPoP proof must cover
|
|
59
57
|
* @param {string} opts.credential - the raw JWT
|
|
60
58
|
* @param {string} opts.space - the space the request targets
|
|
61
|
-
* @param {(
|
|
59
|
+
* @param {import('@pdsjs/core/ports').DidResolverPort} opts.resolveDid
|
|
62
60
|
* @param {import('@pdsjs/core/ports').SignatureVerifierPort} opts.verifier
|
|
63
61
|
* @returns {Promise<{iss: string}>}
|
|
64
62
|
*/
|
|
@@ -66,7 +64,7 @@ export declare function verifyPresentedCredential({ request, credential, space,
|
|
|
66
64
|
request: Request;
|
|
67
65
|
credential: string;
|
|
68
66
|
space: string;
|
|
69
|
-
resolveDid: (
|
|
67
|
+
resolveDid: import('@pdsjs/core/ports').DidResolverPort;
|
|
70
68
|
verifier: import('@pdsjs/core/ports').SignatureVerifierPort;
|
|
71
69
|
}): Promise<{
|
|
72
70
|
iss: string;
|
|
@@ -77,7 +75,7 @@ export declare function verifyPresentedCredential({ request, credential, space,
|
|
|
77
75
|
* @param {Object} opts
|
|
78
76
|
* @param {string} opts.credential - the raw JWT
|
|
79
77
|
* @param {string} opts.space - the space the request targets
|
|
80
|
-
* @param {(
|
|
78
|
+
* @param {import('@pdsjs/core/ports').DidResolverPort} opts.resolveDid
|
|
81
79
|
* @param {import('@pdsjs/core/ports').SignatureVerifierPort} opts.verifier
|
|
82
80
|
* @returns {Promise<{iss: string, jkt: string}>} the authority, and the key the
|
|
83
81
|
* holder must prove possession of
|
|
@@ -85,7 +83,7 @@ export declare function verifyPresentedCredential({ request, credential, space,
|
|
|
85
83
|
export declare function verifySpaceCredential({ credential, space, resolveDid, verifier, }: {
|
|
86
84
|
credential: string;
|
|
87
85
|
space: string;
|
|
88
|
-
resolveDid: (
|
|
86
|
+
resolveDid: import('@pdsjs/core/ports').DidResolverPort;
|
|
89
87
|
verifier: import('@pdsjs/core/ports').SignatureVerifierPort;
|
|
90
88
|
}): Promise<{
|
|
91
89
|
iss: string;
|
|
@@ -103,8 +101,8 @@ export declare function verifySpaceCredential({ credential, space, resolveDid, v
|
|
|
103
101
|
* @param {string} opts.space
|
|
104
102
|
* @param {string} opts.userDid
|
|
105
103
|
* @param {string|undefined} opts.clientId
|
|
106
|
-
* @param {(
|
|
107
|
-
* @param {() => Promise<
|
|
104
|
+
* @param {import('@pdsjs/core/ports').DidResolverPort} opts.resolveDid
|
|
105
|
+
* @param {() => Promise<import('../token.js').SpaceSigner>} opts.getSigner
|
|
108
106
|
* @param {typeof fetch} [opts.fetch]
|
|
109
107
|
* @returns {Promise<boolean>}
|
|
110
108
|
*/
|
|
@@ -114,9 +112,7 @@ export declare function checkUserAccess({ managingApp, authorityDid, space, user
|
|
|
114
112
|
space: string;
|
|
115
113
|
userDid: string;
|
|
116
114
|
clientId: string | undefined;
|
|
117
|
-
resolveDid: (
|
|
118
|
-
getSigner: () => Promise<
|
|
119
|
-
sign: (bytes: Uint8Array) => Promise<Uint8Array>;
|
|
120
|
-
}>;
|
|
115
|
+
resolveDid: import('@pdsjs/core/ports').DidResolverPort;
|
|
116
|
+
getSigner: () => Promise<import('../token.js').SpaceSigner>;
|
|
121
117
|
fetch?: typeof fetch;
|
|
122
118
|
}): Promise<boolean>;
|
package/src/handlers/auth.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
spaceHostEndpoint,
|
|
16
16
|
spaceSigningKey,
|
|
17
17
|
} from '../authority.js';
|
|
18
|
-
import { verifyCredentialProof } from '../dpop.js';
|
|
18
|
+
import { credentialBindingFromProof, verifyCredentialProof } from '../dpop.js';
|
|
19
19
|
import { createServiceAuth } from '../service-auth.js';
|
|
20
20
|
import {
|
|
21
21
|
createSpaceToken,
|
|
@@ -62,8 +62,8 @@ async function readJson(request) {
|
|
|
62
62
|
* @param {Object} ctx
|
|
63
63
|
* @param {import('@pdsjs/core/ports').SpaceStoragePort} ctx.spaceStorage
|
|
64
64
|
* @param {() => Promise<string|null>} ctx.getDid
|
|
65
|
-
* @param {() => Promise<
|
|
66
|
-
* @param {(
|
|
65
|
+
* @param {() => Promise<import('../token.js').SpaceSigner>} ctx.getSigner
|
|
66
|
+
* @param {import('@pdsjs/core/ports').DidResolverPort} ctx.resolveDid
|
|
67
67
|
* @param {import('@pdsjs/core/ports').SignatureVerifierPort} ctx.verifier
|
|
68
68
|
* @param {typeof fetch} [ctx.fetch]
|
|
69
69
|
* @returns {import('@pdsjs/core/pds').Routes}
|
|
@@ -183,11 +183,35 @@ export function createAuthRoutes(ctx) {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
// Checked after the token, so a caller with no credentials at all reads
|
|
186
|
-
// 401 rather than a complaint about their
|
|
186
|
+
// 401 rather than a complaint about their request. Required, because an
|
|
187
187
|
// unbound credential is a bearer token for the whole space and every
|
|
188
188
|
// repo host in it would accept a replay of it.
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
//
|
|
190
|
+
// The key arrives two ways. A DPoP proof demonstrates it, which is what
|
|
191
|
+
// the Rust implementation sends; a `dpopJkt` body field asserts it,
|
|
192
|
+
// which is what the reference implementation reads. Both are accepted,
|
|
193
|
+
// and a caller sending both must agree with itself.
|
|
194
|
+
/** @type {string|null} */
|
|
195
|
+
let boundJkt;
|
|
196
|
+
try {
|
|
197
|
+
boundJkt = await credentialBindingFromProof(request);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
return tokenErrorResponse(err);
|
|
200
|
+
}
|
|
201
|
+
if (typeof dpopJkt === 'string' && dpopJkt) {
|
|
202
|
+
if (boundJkt && boundJkt !== dpopJkt) {
|
|
203
|
+
return errorResponse(
|
|
204
|
+
'InvalidRequest',
|
|
205
|
+
'dpopJkt names a different key than the DPoP proof proves',
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
boundJkt ??= dpopJkt;
|
|
209
|
+
}
|
|
210
|
+
if (!boundJkt) {
|
|
211
|
+
return errorResponse(
|
|
212
|
+
'InvalidRequest',
|
|
213
|
+
'A DPoP proof or a dpopJkt is required',
|
|
214
|
+
);
|
|
191
215
|
}
|
|
192
216
|
|
|
193
217
|
// Structural validation only. Full verification means resolving the
|
|
@@ -218,9 +242,12 @@ export function createAuthRoutes(ctx) {
|
|
|
218
242
|
return errorResponse('SpaceDeleted', 'Space has been deleted');
|
|
219
243
|
}
|
|
220
244
|
|
|
221
|
-
// User perimeter.
|
|
245
|
+
// User perimeter. The authority is the only party who can reconfigure
|
|
246
|
+
// the space, so no policy may lock it out of its own space.
|
|
222
247
|
let userAuthorized;
|
|
223
|
-
if (
|
|
248
|
+
if (userDid === authorityDid) {
|
|
249
|
+
userAuthorized = true;
|
|
250
|
+
} else if (spaceRow.policy === 'public') {
|
|
224
251
|
userAuthorized = true;
|
|
225
252
|
} else if (spaceRow.policy === 'member-list') {
|
|
226
253
|
userAuthorized = await spaceStorage.isMember(space, userDid);
|
|
@@ -262,7 +289,7 @@ export function createAuthRoutes(ctx) {
|
|
|
262
289
|
|
|
263
290
|
const credential = await createSpaceToken(
|
|
264
291
|
'credential',
|
|
265
|
-
{ iss: authorityDid, sub: space, dpopJkt },
|
|
292
|
+
{ iss: authorityDid, sub: space, dpopJkt: boundJkt },
|
|
266
293
|
await getSigner(),
|
|
267
294
|
);
|
|
268
295
|
return Response.json({ credential });
|
|
@@ -325,7 +352,7 @@ function toJsonBytes(bytes) {
|
|
|
325
352
|
*
|
|
326
353
|
* @param {Object} ctx
|
|
327
354
|
* @param {() => Promise<string|null>} ctx.getDid
|
|
328
|
-
* @param {(
|
|
355
|
+
* @param {import('@pdsjs/core/ports').DidResolverPort} ctx.resolveDid
|
|
329
356
|
* @param {import('@pdsjs/core/ports').SignatureVerifierPort} ctx.verifier
|
|
330
357
|
* @returns {(request: Request, space: string, auth: {did: string}|null) => Promise<Response|null>}
|
|
331
358
|
* a Response to return, or null to proceed
|
|
@@ -389,7 +416,7 @@ export function credentialFromRequest(request) {
|
|
|
389
416
|
* @param {Request} opts.request - the request the DPoP proof must cover
|
|
390
417
|
* @param {string} opts.credential - the raw JWT
|
|
391
418
|
* @param {string} opts.space - the space the request targets
|
|
392
|
-
* @param {(
|
|
419
|
+
* @param {import('@pdsjs/core/ports').DidResolverPort} opts.resolveDid
|
|
393
420
|
* @param {import('@pdsjs/core/ports').SignatureVerifierPort} opts.verifier
|
|
394
421
|
* @returns {Promise<{iss: string}>}
|
|
395
422
|
*/
|
|
@@ -416,7 +443,7 @@ export async function verifyPresentedCredential({
|
|
|
416
443
|
* @param {Object} opts
|
|
417
444
|
* @param {string} opts.credential - the raw JWT
|
|
418
445
|
* @param {string} opts.space - the space the request targets
|
|
419
|
-
* @param {(
|
|
446
|
+
* @param {import('@pdsjs/core/ports').DidResolverPort} opts.resolveDid
|
|
420
447
|
* @param {import('@pdsjs/core/ports').SignatureVerifierPort} opts.verifier
|
|
421
448
|
* @returns {Promise<{iss: string, jkt: string}>} the authority, and the key the
|
|
422
449
|
* holder must prove possession of
|
|
@@ -469,8 +496,8 @@ export async function verifySpaceCredential({
|
|
|
469
496
|
* @param {string} opts.space
|
|
470
497
|
* @param {string} opts.userDid
|
|
471
498
|
* @param {string|undefined} opts.clientId
|
|
472
|
-
* @param {(
|
|
473
|
-
* @param {() => Promise<
|
|
499
|
+
* @param {import('@pdsjs/core/ports').DidResolverPort} opts.resolveDid
|
|
500
|
+
* @param {() => Promise<import('../token.js').SpaceSigner>} opts.getSigner
|
|
474
501
|
* @param {typeof fetch} [opts.fetch]
|
|
475
502
|
* @returns {Promise<boolean>}
|
|
476
503
|
*/
|
package/src/handlers/manage.d.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* @param {Object} ctx
|
|
3
3
|
* @param {import('@pdsjs/core/ports').SpaceStoragePort} ctx.spaceStorage
|
|
4
4
|
* @param {() => Promise<string|null>} ctx.getDid
|
|
5
|
-
* @param {() => Promise<
|
|
6
|
-
* @param {(
|
|
5
|
+
* @param {() => Promise<import('../token.js').SpaceSigner>} ctx.getSigner
|
|
6
|
+
* @param {import('@pdsjs/core/ports').DidResolverPort} ctx.resolveDid
|
|
7
7
|
* @param {import('@pdsjs/core/ports').SignatureVerifierPort} ctx.verifier
|
|
8
8
|
* @param {typeof fetch} [ctx.fetch]
|
|
9
9
|
* @returns {import('@pdsjs/core/pds').Routes}
|
|
@@ -11,10 +11,8 @@
|
|
|
11
11
|
export declare function createManageRoutes(ctx: {
|
|
12
12
|
spaceStorage: import('@pdsjs/core/ports').SpaceStoragePort;
|
|
13
13
|
getDid: () => Promise<string | null>;
|
|
14
|
-
getSigner: () => Promise<
|
|
15
|
-
|
|
16
|
-
}>;
|
|
17
|
-
resolveDid: (did: string) => Promise<any>;
|
|
14
|
+
getSigner: () => Promise<import('../token.js').SpaceSigner>;
|
|
15
|
+
resolveDid: import('@pdsjs/core/ports').DidResolverPort;
|
|
18
16
|
verifier: import('@pdsjs/core/ports').SignatureVerifierPort;
|
|
19
17
|
fetch?: typeof fetch;
|
|
20
18
|
}): import('@pdsjs/core/pds').Routes;
|
package/src/handlers/manage.js
CHANGED
|
@@ -212,8 +212,8 @@ async function readJson(request) {
|
|
|
212
212
|
* @param {Object} ctx
|
|
213
213
|
* @param {import('@pdsjs/core/ports').SpaceStoragePort} ctx.spaceStorage
|
|
214
214
|
* @param {() => Promise<string|null>} ctx.getDid
|
|
215
|
-
* @param {() => Promise<
|
|
216
|
-
* @param {(
|
|
215
|
+
* @param {() => Promise<import('../token.js').SpaceSigner>} ctx.getSigner
|
|
216
|
+
* @param {import('@pdsjs/core/ports').DidResolverPort} ctx.resolveDid
|
|
217
217
|
* @param {import('@pdsjs/core/ports').SignatureVerifierPort} ctx.verifier
|
|
218
218
|
* @param {typeof fetch} [ctx.fetch]
|
|
219
219
|
* @returns {import('@pdsjs/core/pds').Routes}
|
|
@@ -745,7 +745,7 @@ export function createManageRoutes(ctx) {
|
|
|
745
745
|
* @param {Object} ctx
|
|
746
746
|
* @param {import('@pdsjs/core/ports').SpaceStoragePort} ctx.spaceStorage
|
|
747
747
|
* @param {() => Promise<string|null>} ctx.getDid
|
|
748
|
-
* @param {() => Promise<
|
|
748
|
+
* @param {() => Promise<import('../token.js').SpaceSigner>} ctx.getSigner
|
|
749
749
|
* @param {typeof fetch} [ctx.fetch]
|
|
750
750
|
* @param {string} space
|
|
751
751
|
* @returns {Promise<void>}
|
package/src/handlers/read.d.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* @param {import('@pdsjs/core/ports').SpaceStoragePort} ctx.spaceStorage
|
|
4
4
|
* @param {import('@pdsjs/core/ports').BlobPort} ctx.blobs
|
|
5
5
|
* @param {() => Promise<string|null>} ctx.getDid
|
|
6
|
-
* @param {() => Promise<
|
|
7
|
-
* @param {(
|
|
6
|
+
* @param {() => Promise<import('../token.js').SpaceSigner>} ctx.getSigner
|
|
7
|
+
* @param {import('@pdsjs/core/ports').DidResolverPort} ctx.resolveDid
|
|
8
8
|
* @param {import('@pdsjs/core/ports').SignatureVerifierPort} ctx.verifier
|
|
9
9
|
* @returns {import('@pdsjs/core/pds').Routes}
|
|
10
10
|
*/
|
|
@@ -12,9 +12,7 @@ export declare function createReadRoutes(ctx: {
|
|
|
12
12
|
spaceStorage: import('@pdsjs/core/ports').SpaceStoragePort;
|
|
13
13
|
blobs: import('@pdsjs/core/ports').BlobPort;
|
|
14
14
|
getDid: () => Promise<string | null>;
|
|
15
|
-
getSigner: () => Promise<
|
|
16
|
-
|
|
17
|
-
}>;
|
|
18
|
-
resolveDid: (did: string) => Promise<any>;
|
|
15
|
+
getSigner: () => Promise<import('../token.js').SpaceSigner>;
|
|
16
|
+
resolveDid: import('@pdsjs/core/ports').DidResolverPort;
|
|
19
17
|
verifier: import('@pdsjs/core/ports').SignatureVerifierPort;
|
|
20
18
|
}): import('@pdsjs/core/pds').Routes;
|
package/src/handlers/read.js
CHANGED
|
@@ -141,8 +141,8 @@ async function spaceBlobRefs(spaceStorage, space) {
|
|
|
141
141
|
* @param {import('@pdsjs/core/ports').SpaceStoragePort} ctx.spaceStorage
|
|
142
142
|
* @param {import('@pdsjs/core/ports').BlobPort} ctx.blobs
|
|
143
143
|
* @param {() => Promise<string|null>} ctx.getDid
|
|
144
|
-
* @param {() => Promise<
|
|
145
|
-
* @param {(
|
|
144
|
+
* @param {() => Promise<import('../token.js').SpaceSigner>} ctx.getSigner
|
|
145
|
+
* @param {import('@pdsjs/core/ports').DidResolverPort} ctx.resolveDid
|
|
146
146
|
* @param {import('@pdsjs/core/ports').SignatureVerifierPort} ctx.verifier
|
|
147
147
|
* @returns {import('@pdsjs/core/pds').Routes}
|
|
148
148
|
*/
|
package/src/handlers/write.d.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* @param {(blobCid: string, recordUri: string, recordTime: number|null) => Promise<void>} ctx.linkBlob
|
|
5
5
|
* @param {(recordUri: string) => Promise<void>} ctx.unlinkBlobs
|
|
6
6
|
* @param {() => Promise<string|null>} ctx.getDid
|
|
7
|
-
* @param {() => Promise<
|
|
8
|
-
* @param {(
|
|
7
|
+
* @param {() => Promise<import('../token.js').SpaceSigner>} ctx.getSigner
|
|
8
|
+
* @param {import('@pdsjs/core/ports').DidResolverPort} ctx.resolveDid
|
|
9
|
+
* @param {{collections: string[], check: (write: {space: string|null, collection: string, rkey: string, prevValue: unknown|null, nextValue: unknown|null}) => Promise<void>}} [ctx.recordGuard]
|
|
9
10
|
* @param {typeof fetch} [ctx.fetch]
|
|
10
11
|
* @returns {import('@pdsjs/core/pds').Routes}
|
|
11
12
|
*/
|
|
@@ -14,9 +15,17 @@ export declare function createWriteRoutes(ctx: {
|
|
|
14
15
|
linkBlob: (blobCid: string, recordUri: string, recordTime: number | null) => Promise<void>;
|
|
15
16
|
unlinkBlobs: (recordUri: string) => Promise<void>;
|
|
16
17
|
getDid: () => Promise<string | null>;
|
|
17
|
-
getSigner: () => Promise<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
getSigner: () => Promise<import('../token.js').SpaceSigner>;
|
|
19
|
+
resolveDid: import('@pdsjs/core/ports').DidResolverPort;
|
|
20
|
+
recordGuard?: {
|
|
21
|
+
collections: string[];
|
|
22
|
+
check: (write: {
|
|
23
|
+
space: string | null;
|
|
24
|
+
collection: string;
|
|
25
|
+
rkey: string;
|
|
26
|
+
prevValue: unknown | null;
|
|
27
|
+
nextValue: unknown | null;
|
|
28
|
+
}) => Promise<void>;
|
|
29
|
+
};
|
|
21
30
|
fetch?: typeof fetch;
|
|
22
31
|
}): import('@pdsjs/core/pds').Routes;
|
package/src/handlers/write.js
CHANGED
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
// Authorization is two checks: the caller may only write its own repo, and the
|
|
8
8
|
// token's `space:` scope must cover the target space, action and collection.
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
cborDecode,
|
|
12
|
+
createTid,
|
|
13
|
+
findBlobRefs,
|
|
14
|
+
recordTimeOf,
|
|
15
|
+
} from '@pdsjs/core/repo';
|
|
11
16
|
import { ScopePermissions } from '@pdsjs/core/scope';
|
|
12
17
|
import { spaceHostEndpoint } from '../authority.js';
|
|
13
18
|
import { LtHash } from '../lthash.js';
|
|
@@ -170,14 +175,58 @@ function writeAction(write) {
|
|
|
170
175
|
* @param {(blobCid: string, recordUri: string, recordTime: number|null) => Promise<void>} ctx.linkBlob
|
|
171
176
|
* @param {(recordUri: string) => Promise<void>} ctx.unlinkBlobs
|
|
172
177
|
* @param {() => Promise<string|null>} ctx.getDid
|
|
173
|
-
* @param {() => Promise<
|
|
174
|
-
* @param {(
|
|
178
|
+
* @param {() => Promise<import('../token.js').SpaceSigner>} ctx.getSigner
|
|
179
|
+
* @param {import('@pdsjs/core/ports').DidResolverPort} ctx.resolveDid
|
|
180
|
+
* @param {{collections: string[], check: (write: {space: string|null, collection: string, rkey: string, prevValue: unknown|null, nextValue: unknown|null}) => Promise<void>}} [ctx.recordGuard]
|
|
175
181
|
* @param {typeof fetch} [ctx.fetch]
|
|
176
182
|
* @returns {import('@pdsjs/core/pds').Routes}
|
|
177
183
|
*/
|
|
178
184
|
export function createWriteRoutes(ctx) {
|
|
179
|
-
const {
|
|
180
|
-
|
|
185
|
+
const {
|
|
186
|
+
spaceStorage,
|
|
187
|
+
linkBlob,
|
|
188
|
+
unlinkBlobs,
|
|
189
|
+
getDid,
|
|
190
|
+
getSigner,
|
|
191
|
+
resolveDid,
|
|
192
|
+
recordGuard,
|
|
193
|
+
} = ctx;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Consult the write guard for one write in a collection it covers. Null to
|
|
197
|
+
* proceed, the refusal Response otherwise. Read-then-check races the write
|
|
198
|
+
* queue; the single account that writes here makes that a non-event.
|
|
199
|
+
* @param {string} space
|
|
200
|
+
* @param {import('../writer.js').SpaceWriteInput} write
|
|
201
|
+
* @returns {Promise<Response|null>}
|
|
202
|
+
*/
|
|
203
|
+
async function guardWrite(space, write) {
|
|
204
|
+
if (!recordGuard || !recordGuard.collections.includes(write.collection)) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
const prev = await spaceStorage.getSpaceRecord(
|
|
208
|
+
space,
|
|
209
|
+
write.collection,
|
|
210
|
+
write.rkey,
|
|
211
|
+
);
|
|
212
|
+
try {
|
|
213
|
+
await recordGuard.check({
|
|
214
|
+
space,
|
|
215
|
+
collection: write.collection,
|
|
216
|
+
rkey: write.rkey,
|
|
217
|
+
prevValue: prev ? cborDecode(prev.value) : null,
|
|
218
|
+
nextValue: write.action === 'delete' ? null : (write.record ?? null),
|
|
219
|
+
});
|
|
220
|
+
return null;
|
|
221
|
+
} catch (err) {
|
|
222
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
223
|
+
const code =
|
|
224
|
+
err instanceof Error && 'code' in err
|
|
225
|
+
? String(/** @type {{code: unknown}} */ (err).code)
|
|
226
|
+
: 'InvalidRequest';
|
|
227
|
+
return errorResponse(code, message);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
181
230
|
|
|
182
231
|
/**
|
|
183
232
|
* Record which blobs a space write leaves referenced.
|
|
@@ -317,6 +366,9 @@ export function createWriteRoutes(ctx) {
|
|
|
317
366
|
});
|
|
318
367
|
if (scopeError) return scopeError;
|
|
319
368
|
|
|
369
|
+
const refused = await guardWrite(body.space, write);
|
|
370
|
+
if (refused) return refused;
|
|
371
|
+
|
|
320
372
|
try {
|
|
321
373
|
const commit = await applyWrites(spaceStorage, {
|
|
322
374
|
space: body.space,
|
|
@@ -374,11 +426,28 @@ export function createWriteRoutes(ctx) {
|
|
|
374
426
|
if (body.record === undefined) {
|
|
375
427
|
throw new SpaceWriteError('record is required', 'InvalidRequest');
|
|
376
428
|
}
|
|
429
|
+
if (
|
|
430
|
+
body.swapRecord !== undefined &&
|
|
431
|
+
body.swapRecord !== null &&
|
|
432
|
+
typeof body.swapRecord !== 'string'
|
|
433
|
+
) {
|
|
434
|
+
throw new SpaceWriteError(
|
|
435
|
+
'swapRecord must be a CID string or null',
|
|
436
|
+
'InvalidRequest',
|
|
437
|
+
);
|
|
438
|
+
}
|
|
377
439
|
return {
|
|
378
440
|
action: 'put',
|
|
379
441
|
collection: body.collection,
|
|
380
442
|
rkey: body.rkey,
|
|
381
443
|
record: body.record,
|
|
444
|
+
// A pds.js extension the draft proposal does not have: null
|
|
445
|
+
// requires that the record does not exist, a CID requires that
|
|
446
|
+
// exact version, absent skips the check. A server without it
|
|
447
|
+
// ignores the field, so a client falls back to last-write-wins.
|
|
448
|
+
...(body.swapRecord !== undefined
|
|
449
|
+
? { swapCid: body.swapRecord }
|
|
450
|
+
: {}),
|
|
382
451
|
};
|
|
383
452
|
},
|
|
384
453
|
(body, result, did) => ({
|
|
@@ -464,6 +533,10 @@ export function createWriteRoutes(ctx) {
|
|
|
464
533
|
});
|
|
465
534
|
if (scopeError) return scopeError;
|
|
466
535
|
}
|
|
536
|
+
for (const w of writes) {
|
|
537
|
+
const refused = await guardWrite(body.space, w);
|
|
538
|
+
if (refused) return refused;
|
|
539
|
+
}
|
|
467
540
|
|
|
468
541
|
try {
|
|
469
542
|
const commit = await applyWrites(spaceStorage, {
|
package/src/notify.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* @param {Object} ctx
|
|
15
15
|
* @param {import('@pdsjs/core/ports').SpaceStoragePort} ctx.spaceStorage
|
|
16
|
-
* @param {() => Promise<
|
|
16
|
+
* @param {() => Promise<import('./token.js').SpaceSigner>} ctx.getSigner
|
|
17
17
|
* @param {typeof fetch} [ctx.fetch]
|
|
18
18
|
* @param {Object} notice
|
|
19
19
|
* @param {string} notice.authorityDid - the space's authority, which signs
|
|
@@ -25,9 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
export declare function forwardToSyncers(ctx: {
|
|
27
27
|
spaceStorage: import('@pdsjs/core/ports').SpaceStoragePort;
|
|
28
|
-
getSigner: () => Promise<
|
|
29
|
-
sign: (bytes: Uint8Array) => Promise<Uint8Array>;
|
|
30
|
-
}>;
|
|
28
|
+
getSigner: () => Promise<import('./token.js').SpaceSigner>;
|
|
31
29
|
fetch?: typeof fetch;
|
|
32
30
|
}, { authorityDid, space, repo, rev, hash }: {
|
|
33
31
|
authorityDid: string;
|
package/src/notify.js
CHANGED
|
@@ -23,7 +23,7 @@ import { createServiceAuth } from './service-auth.js';
|
|
|
23
23
|
*
|
|
24
24
|
* @param {Object} ctx
|
|
25
25
|
* @param {import('@pdsjs/core/ports').SpaceStoragePort} ctx.spaceStorage
|
|
26
|
-
* @param {() => Promise<
|
|
26
|
+
* @param {() => Promise<import('./token.js').SpaceSigner>} ctx.getSigner
|
|
27
27
|
* @param {typeof fetch} [ctx.fetch]
|
|
28
28
|
* @param {Object} notice
|
|
29
29
|
* @param {string} notice.authorityDid - the space's authority, which signs
|
package/src/routes.d.ts
CHANGED
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
* @param {(blobCid: string, recordUri: string, recordTime: number|null) => Promise<void>} ctx.linkBlob
|
|
7
7
|
* @param {(recordUri: string) => Promise<void>} ctx.unlinkBlobs
|
|
8
8
|
* @param {() => Promise<string|null>} ctx.getDid - the hosted account's DID
|
|
9
|
-
* @param {() => Promise<
|
|
9
|
+
* @param {() => Promise<import('./token.js').SpaceSigner>} ctx.getSigner
|
|
10
10
|
* - the account's signing key, used to sign repo commits and space tokens
|
|
11
|
-
* @param {(
|
|
11
|
+
* @param {import('@pdsjs/core/ports').DidResolverPort} ctx.resolveDid - DID document lookup,
|
|
12
12
|
* injected so this package stays dependency-free
|
|
13
13
|
* @param {import('@pdsjs/core/ports').SignatureVerifierPort} ctx.verifier
|
|
14
|
+
* @param {import('@pdsjs/core/ports').RecordWriteGuardPort} [ctx.recordGuard]
|
|
15
|
+
* - a veto over writes in its named collections, e.g. git branch protection
|
|
14
16
|
* @param {typeof fetch} [ctx.fetch] - injected for the outbound notification
|
|
15
17
|
* calls, so tests can observe them without a network
|
|
16
18
|
* @returns {import('@pdsjs/core/pds').Routes}
|
|
@@ -21,10 +23,9 @@ export declare function createSpaceRoutes(ctx: {
|
|
|
21
23
|
linkBlob: (blobCid: string, recordUri: string, recordTime: number | null) => Promise<void>;
|
|
22
24
|
unlinkBlobs: (recordUri: string) => Promise<void>;
|
|
23
25
|
getDid: () => Promise<string | null>;
|
|
24
|
-
getSigner: () => Promise<
|
|
25
|
-
|
|
26
|
-
}>;
|
|
27
|
-
resolveDid: (did: string) => Promise<any>;
|
|
26
|
+
getSigner: () => Promise<import('./token.js').SpaceSigner>;
|
|
27
|
+
resolveDid: import('@pdsjs/core/ports').DidResolverPort;
|
|
28
28
|
verifier: import('@pdsjs/core/ports').SignatureVerifierPort;
|
|
29
|
+
recordGuard?: import('@pdsjs/core/ports').RecordWriteGuardPort;
|
|
29
30
|
fetch?: typeof fetch;
|
|
30
31
|
}): import('@pdsjs/core/pds').Routes;
|
package/src/routes.js
CHANGED
|
@@ -18,11 +18,13 @@ import { createWriteRoutes } from './handlers/write.js';
|
|
|
18
18
|
* @param {(blobCid: string, recordUri: string, recordTime: number|null) => Promise<void>} ctx.linkBlob
|
|
19
19
|
* @param {(recordUri: string) => Promise<void>} ctx.unlinkBlobs
|
|
20
20
|
* @param {() => Promise<string|null>} ctx.getDid - the hosted account's DID
|
|
21
|
-
* @param {() => Promise<
|
|
21
|
+
* @param {() => Promise<import('./token.js').SpaceSigner>} ctx.getSigner
|
|
22
22
|
* - the account's signing key, used to sign repo commits and space tokens
|
|
23
|
-
* @param {(
|
|
23
|
+
* @param {import('@pdsjs/core/ports').DidResolverPort} ctx.resolveDid - DID document lookup,
|
|
24
24
|
* injected so this package stays dependency-free
|
|
25
25
|
* @param {import('@pdsjs/core/ports').SignatureVerifierPort} ctx.verifier
|
|
26
|
+
* @param {import('@pdsjs/core/ports').RecordWriteGuardPort} [ctx.recordGuard]
|
|
27
|
+
* - a veto over writes in its named collections, e.g. git branch protection
|
|
26
28
|
* @param {typeof fetch} [ctx.fetch] - injected for the outbound notification
|
|
27
29
|
* calls, so tests can observe them without a network
|
|
28
30
|
* @returns {import('@pdsjs/core/pds').Routes}
|
package/src/service-auth.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @param {string} opts.jwt
|
|
6
6
|
* @param {string} opts.aud - our own DID; the token must be addressed to us
|
|
7
7
|
* @param {string} opts.lxm - the method being called
|
|
8
|
-
* @param {(
|
|
8
|
+
* @param {import('@pdsjs/core/ports').DidResolverPort} opts.resolveDid
|
|
9
9
|
* @param {import('@pdsjs/core/ports').SignatureVerifierPort} opts.verifier
|
|
10
10
|
* @returns {Promise<{iss: string}>}
|
|
11
11
|
*/
|
|
@@ -13,7 +13,7 @@ export declare function verifyServiceAuth({ jwt, aud, lxm, resolveDid, verifier,
|
|
|
13
13
|
jwt: string;
|
|
14
14
|
aud: string;
|
|
15
15
|
lxm: string;
|
|
16
|
-
resolveDid: (
|
|
16
|
+
resolveDid: import('@pdsjs/core/ports').DidResolverPort;
|
|
17
17
|
verifier: import('@pdsjs/core/ports').SignatureVerifierPort;
|
|
18
18
|
}): Promise<{
|
|
19
19
|
iss: string;
|
|
@@ -29,14 +29,12 @@ export declare function verifyServiceAuth({ jwt, aud, lxm, resolveDid, verifier,
|
|
|
29
29
|
* @param {string} opts.iss - our DID
|
|
30
30
|
* @param {string} opts.aud - the service we are calling
|
|
31
31
|
* @param {string} opts.lxm - the method, so the token cannot be replayed elsewhere
|
|
32
|
-
* @param {
|
|
32
|
+
* @param {import('./token.js').SpaceSigner} opts.signer
|
|
33
33
|
* @returns {Promise<string>}
|
|
34
34
|
*/
|
|
35
35
|
export declare function createServiceAuth({ iss, aud, lxm, signer }: {
|
|
36
36
|
iss: string;
|
|
37
37
|
aud: string;
|
|
38
38
|
lxm: string;
|
|
39
|
-
signer:
|
|
40
|
-
sign: (bytes: Uint8Array) => Promise<Uint8Array>;
|
|
41
|
-
};
|
|
39
|
+
signer: import('./token.js').SpaceSigner;
|
|
42
40
|
}): Promise<string>;
|
package/src/service-auth.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { base64UrlEncode, bytesToHex } from '@pdsjs/core/crypto';
|
|
9
9
|
import { atprotoSigningKey } from './authority.js';
|
|
10
|
-
import { SpaceTokenError } from './token.js';
|
|
10
|
+
import { assertKeyAlg, SpaceTokenError, signerAlg } from './token.js';
|
|
11
11
|
|
|
12
12
|
const CLOCK_SKEW_SEC = 5;
|
|
13
13
|
|
|
@@ -43,7 +43,7 @@ function decodeSig(b64) {
|
|
|
43
43
|
* @param {string} opts.jwt
|
|
44
44
|
* @param {string} opts.aud - our own DID; the token must be addressed to us
|
|
45
45
|
* @param {string} opts.lxm - the method being called
|
|
46
|
-
* @param {(
|
|
46
|
+
* @param {import('@pdsjs/core/ports').DidResolverPort} opts.resolveDid
|
|
47
47
|
* @param {import('@pdsjs/core/ports').SignatureVerifierPort} opts.verifier
|
|
48
48
|
* @returns {Promise<{iss: string}>}
|
|
49
49
|
*/
|
|
@@ -60,14 +60,19 @@ export async function verifyServiceAuth({
|
|
|
60
60
|
}
|
|
61
61
|
const [headerB64, payloadB64, sigB64] = parts;
|
|
62
62
|
|
|
63
|
+
/** @type {any} */
|
|
64
|
+
let header;
|
|
63
65
|
/** @type {any} */
|
|
64
66
|
let payload;
|
|
65
67
|
try {
|
|
66
|
-
decodePart(headerB64);
|
|
68
|
+
header = decodePart(headerB64);
|
|
67
69
|
payload = decodePart(payloadB64);
|
|
68
70
|
} catch {
|
|
69
71
|
throw new SpaceTokenError('could not parse service token', 'BadJwt');
|
|
70
72
|
}
|
|
73
|
+
if (typeof header?.alg !== 'string' || !header.alg) {
|
|
74
|
+
throw new SpaceTokenError('missing service token "alg"', 'BadJwt');
|
|
75
|
+
}
|
|
71
76
|
|
|
72
77
|
const now = Math.floor(Date.now() / 1000);
|
|
73
78
|
if (typeof payload.exp !== 'number' || now - CLOCK_SKEW_SEC >= payload.exp) {
|
|
@@ -91,30 +96,47 @@ export async function verifyServiceAuth({
|
|
|
91
96
|
);
|
|
92
97
|
}
|
|
93
98
|
|
|
94
|
-
const didDoc = await resolveDid(payload.iss);
|
|
95
|
-
if (!didDoc) {
|
|
96
|
-
throw new SpaceTokenError(
|
|
97
|
-
`could not resolve ${payload.iss}`,
|
|
98
|
-
'DidNotFound',
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
const didKey = atprotoSigningKey(didDoc, payload.iss);
|
|
102
99
|
const signingInput = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
|
|
100
|
+
const sig = decodeSig(sigB64);
|
|
103
101
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
);
|
|
102
|
+
/**
|
|
103
|
+
* @param {boolean} forceRefresh
|
|
104
|
+
* @returns {Promise<{didKey: string, valid: boolean}>}
|
|
105
|
+
*/
|
|
106
|
+
const matchesSignature = async (forceRefresh) => {
|
|
107
|
+
const didDoc = await resolveDid(payload.iss, { forceRefresh });
|
|
108
|
+
if (!didDoc) {
|
|
109
|
+
throw new SpaceTokenError(
|
|
110
|
+
`could not resolve ${payload.iss}`,
|
|
111
|
+
'DidNotFound',
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const didKey = atprotoSigningKey(didDoc, payload.iss);
|
|
115
|
+
assertKeyAlg(didKey, header.alg);
|
|
116
|
+
try {
|
|
117
|
+
return {
|
|
118
|
+
didKey,
|
|
119
|
+
valid: await verifier.verify(didKey, signingInput, sig),
|
|
120
|
+
};
|
|
121
|
+
} catch (err) {
|
|
122
|
+
throw new SpaceTokenError(
|
|
123
|
+
`could not verify service token: ${err instanceof Error ? err.message : String(err)}`,
|
|
124
|
+
'BadJwtSignature',
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const first = await matchesSignature(false);
|
|
130
|
+
if (!first.valid) {
|
|
131
|
+
// A resolver that holds its answers can name a key the caller has rotated
|
|
132
|
+
// away from, and the token is signed by the current one.
|
|
133
|
+
const fresh = await matchesSignature(true);
|
|
134
|
+
if (fresh.didKey === first.didKey || !fresh.valid) {
|
|
135
|
+
throw new SpaceTokenError(
|
|
136
|
+
'invalid service token signature',
|
|
137
|
+
'BadJwtSignature',
|
|
138
|
+
);
|
|
139
|
+
}
|
|
118
140
|
}
|
|
119
141
|
|
|
120
142
|
return { iss: payload.iss };
|
|
@@ -131,12 +153,12 @@ export async function verifyServiceAuth({
|
|
|
131
153
|
* @param {string} opts.iss - our DID
|
|
132
154
|
* @param {string} opts.aud - the service we are calling
|
|
133
155
|
* @param {string} opts.lxm - the method, so the token cannot be replayed elsewhere
|
|
134
|
-
* @param {
|
|
156
|
+
* @param {import('./token.js').SpaceSigner} opts.signer
|
|
135
157
|
* @returns {Promise<string>}
|
|
136
158
|
*/
|
|
137
159
|
export async function createServiceAuth({ iss, aud, lxm, signer }) {
|
|
138
160
|
const now = Math.floor(Date.now() / 1000);
|
|
139
|
-
const header = { typ: 'JWT', alg:
|
|
161
|
+
const header = { typ: 'JWT', alg: signerAlg(signer) };
|
|
140
162
|
const payload = {
|
|
141
163
|
iss,
|
|
142
164
|
aud,
|
package/src/token.d.ts
CHANGED
|
@@ -21,6 +21,21 @@ export declare const SPACE_TOKEN_TYPES: {
|
|
|
21
21
|
requireCnf: boolean;
|
|
22
22
|
};
|
|
23
23
|
};
|
|
24
|
+
export type SpaceSigner = {
|
|
25
|
+
sign: (bytes: Uint8Array) => Promise<Uint8Array>;
|
|
26
|
+
/**
|
|
27
|
+
* - p256 when absent
|
|
28
|
+
*/
|
|
29
|
+
curve?: 'p256' | 'secp256k1';
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* The JWT `alg` for a signer's curve. A verifier resolves the issuer's key,
|
|
33
|
+
* reads its type, and refuses a header that names another algorithm before it
|
|
34
|
+
* checks the signature. So a secp256k1 key must stamp ES256K.
|
|
35
|
+
* @param {SpaceSigner} signer
|
|
36
|
+
* @returns {string}
|
|
37
|
+
*/
|
|
38
|
+
export declare function signerAlg(signer: SpaceSigner): string;
|
|
24
39
|
export declare class SpaceTokenError extends Error {
|
|
25
40
|
code: string;
|
|
26
41
|
/**
|
|
@@ -29,6 +44,18 @@ export declare class SpaceTokenError extends Error {
|
|
|
29
44
|
*/
|
|
30
45
|
constructor(message: string, code?: string);
|
|
31
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Refuse a token whose header names an algorithm the issuer's key cannot
|
|
49
|
+
* produce. The signature would verify anyway, since the curve comes from the
|
|
50
|
+
* resolved key rather than the header. The reference implementation rejects
|
|
51
|
+
* the mismatch, so a token this server accepts has to be one the rest of the
|
|
52
|
+
* ecosystem accepts too.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} didKey
|
|
55
|
+
* @param {string} alg - the token header's `alg`
|
|
56
|
+
* @throws {SpaceTokenError}
|
|
57
|
+
*/
|
|
58
|
+
export declare function assertKeyAlg(didKey: string, alg: string): void;
|
|
32
59
|
export type SpaceTokenPayload = {
|
|
33
60
|
iss: string;
|
|
34
61
|
/**
|
|
@@ -54,7 +81,7 @@ export type SpaceTokenHeader = {
|
|
|
54
81
|
/**
|
|
55
82
|
* @param {keyof typeof SPACE_TOKEN_TYPES} type
|
|
56
83
|
* @param {{iss: string, sub: string, aud?: string, dpopJkt?: string, expiresInSec?: number, kid?: string, alg?: string}} opts
|
|
57
|
-
* @param {
|
|
84
|
+
* @param {SpaceSigner} signer
|
|
58
85
|
* @returns {Promise<string>}
|
|
59
86
|
*/
|
|
60
87
|
export declare function createSpaceToken(type: keyof typeof SPACE_TOKEN_TYPES, opts: {
|
|
@@ -65,9 +92,7 @@ export declare function createSpaceToken(type: keyof typeof SPACE_TOKEN_TYPES, o
|
|
|
65
92
|
expiresInSec?: number;
|
|
66
93
|
kid?: string;
|
|
67
94
|
alg?: string;
|
|
68
|
-
}, signer:
|
|
69
|
-
sign: (bytes: Uint8Array) => Promise<Uint8Array>;
|
|
70
|
-
}): Promise<string>;
|
|
95
|
+
}, signer: SpaceSigner): Promise<string>;
|
|
71
96
|
/**
|
|
72
97
|
* Structural validation only, no signature check. This is as far as we go for
|
|
73
98
|
* client attestations, whose key comes from the client's JWKS rather than a DID
|
|
@@ -87,15 +112,16 @@ export declare function parseSpaceToken(type: keyof typeof SPACE_TOKEN_TYPES, jw
|
|
|
87
112
|
* @param {keyof typeof SPACE_TOKEN_TYPES} type
|
|
88
113
|
* @param {string} jwt
|
|
89
114
|
* @param {Object} opts
|
|
90
|
-
* @param {(iss: string, kid?: string) => Promise<string>} opts.getSigningKey
|
|
91
|
-
* - resolves the issuer to a did:key. Given `kid` so it can honour the key
|
|
115
|
+
* @param {(iss: string, kid?: string, forceRefresh?: boolean) => Promise<string>} opts.getSigningKey
|
|
116
|
+
* - resolves the issuer to a did:key. Given `kid` so it can honour the key
|
|
117
|
+
* id, and `forceRefresh` to read past a held answer after a signature fails.
|
|
92
118
|
* @param {import('@pdsjs/core/ports').SignatureVerifierPort} opts.verifier
|
|
93
119
|
* @param {string} [opts.aud] - required audience, when the caller knows it
|
|
94
120
|
* @param {string} [opts.sub] - required subject, when the caller knows it
|
|
95
121
|
* @returns {Promise<{header: SpaceTokenHeader, payload: SpaceTokenPayload}>}
|
|
96
122
|
*/
|
|
97
123
|
export declare function verifySpaceToken(type: keyof typeof SPACE_TOKEN_TYPES, jwt: string, opts: {
|
|
98
|
-
getSigningKey: (iss: string, kid?: string) => Promise<string>;
|
|
124
|
+
getSigningKey: (iss: string, kid?: string, forceRefresh?: boolean) => Promise<string>;
|
|
99
125
|
verifier: import('@pdsjs/core/ports').SignatureVerifierPort;
|
|
100
126
|
aud?: string;
|
|
101
127
|
sub?: string;
|
package/src/token.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
base64UrlEncode,
|
|
13
13
|
bytesToHex,
|
|
14
14
|
} from '@pdsjs/core/crypto';
|
|
15
|
+
import { parseDidKey } from '@pdsjs/core/verify';
|
|
15
16
|
|
|
16
17
|
export const SPACE_TOKEN_TYPES = {
|
|
17
18
|
delegation: {
|
|
@@ -44,6 +45,27 @@ export const SPACE_TOKEN_TYPES = {
|
|
|
44
45
|
|
|
45
46
|
const CLOCK_SKEW_SEC = 5;
|
|
46
47
|
|
|
48
|
+
/**
|
|
49
|
+
* The account key that signs space tokens and repo commits.
|
|
50
|
+
* @typedef {Object} SpaceSigner
|
|
51
|
+
* @property {(bytes: Uint8Array) => Promise<Uint8Array>} sign
|
|
52
|
+
* @property {'p256'|'secp256k1'} [curve] - p256 when absent
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/** The JWT `alg` each curve signs under. */
|
|
56
|
+
const JWT_ALG = { p256: 'ES256', secp256k1: 'ES256K' };
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The JWT `alg` for a signer's curve. A verifier resolves the issuer's key,
|
|
60
|
+
* reads its type, and refuses a header that names another algorithm before it
|
|
61
|
+
* checks the signature. So a secp256k1 key must stamp ES256K.
|
|
62
|
+
* @param {SpaceSigner} signer
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
export function signerAlg(signer) {
|
|
66
|
+
return JWT_ALG[signer.curve ?? 'p256'];
|
|
67
|
+
}
|
|
68
|
+
|
|
47
69
|
export class SpaceTokenError extends Error {
|
|
48
70
|
/**
|
|
49
71
|
* @param {string} message
|
|
@@ -56,6 +78,35 @@ export class SpaceTokenError extends Error {
|
|
|
56
78
|
}
|
|
57
79
|
}
|
|
58
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Refuse a token whose header names an algorithm the issuer's key cannot
|
|
83
|
+
* produce. The signature would verify anyway, since the curve comes from the
|
|
84
|
+
* resolved key rather than the header. The reference implementation rejects
|
|
85
|
+
* the mismatch, so a token this server accepts has to be one the rest of the
|
|
86
|
+
* ecosystem accepts too.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} didKey
|
|
89
|
+
* @param {string} alg - the token header's `alg`
|
|
90
|
+
* @throws {SpaceTokenError}
|
|
91
|
+
*/
|
|
92
|
+
export function assertKeyAlg(didKey, alg) {
|
|
93
|
+
let expected;
|
|
94
|
+
try {
|
|
95
|
+
expected = JWT_ALG[parseDidKey(didKey).curve];
|
|
96
|
+
} catch (err) {
|
|
97
|
+
throw new SpaceTokenError(
|
|
98
|
+
`could not read the issuer key: ${err instanceof Error ? err.message : String(err)}`,
|
|
99
|
+
'BadJwtSignature',
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if (alg !== expected) {
|
|
103
|
+
throw new SpaceTokenError(
|
|
104
|
+
`token alg ${alg} does not match the issuer key, which signs ${expected}`,
|
|
105
|
+
'BadJwtSignature',
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
59
110
|
/**
|
|
60
111
|
* @typedef {Object} SpaceTokenPayload
|
|
61
112
|
* @property {string} iss
|
|
@@ -101,7 +152,7 @@ function decodeJsonPart(b64, part) {
|
|
|
101
152
|
/**
|
|
102
153
|
* @param {keyof typeof SPACE_TOKEN_TYPES} type
|
|
103
154
|
* @param {{iss: string, sub: string, aud?: string, dpopJkt?: string, expiresInSec?: number, kid?: string, alg?: string}} opts
|
|
104
|
-
* @param {
|
|
155
|
+
* @param {SpaceSigner} signer
|
|
105
156
|
* @returns {Promise<string>}
|
|
106
157
|
*/
|
|
107
158
|
export async function createSpaceToken(type, opts, signer) {
|
|
@@ -115,7 +166,7 @@ export async function createSpaceToken(type, opts, signer) {
|
|
|
115
166
|
|
|
116
167
|
const iat = Math.floor(Date.now() / 1000);
|
|
117
168
|
/** @type {SpaceTokenHeader} */
|
|
118
|
-
const header = { alg: opts.alg ??
|
|
169
|
+
const header = { alg: opts.alg ?? signerAlg(signer), typ: spec.typ };
|
|
119
170
|
const kid = opts.kid ?? spec.kid;
|
|
120
171
|
if (kid) header.kid = kid;
|
|
121
172
|
|
|
@@ -197,8 +248,9 @@ export function parseSpaceToken(type, jwt) {
|
|
|
197
248
|
* @param {keyof typeof SPACE_TOKEN_TYPES} type
|
|
198
249
|
* @param {string} jwt
|
|
199
250
|
* @param {Object} opts
|
|
200
|
-
* @param {(iss: string, kid?: string) => Promise<string>} opts.getSigningKey
|
|
201
|
-
* - resolves the issuer to a did:key. Given `kid` so it can honour the key
|
|
251
|
+
* @param {(iss: string, kid?: string, forceRefresh?: boolean) => Promise<string>} opts.getSigningKey
|
|
252
|
+
* - resolves the issuer to a did:key. Given `kid` so it can honour the key
|
|
253
|
+
* id, and `forceRefresh` to read past a held answer after a signature fails.
|
|
202
254
|
* @param {import('@pdsjs/core/ports').SignatureVerifierPort} opts.verifier
|
|
203
255
|
* @param {string} [opts.aud] - required audience, when the caller knows it
|
|
204
256
|
* @param {string} [opts.sub] - required subject, when the caller knows it
|
|
@@ -224,19 +276,31 @@ export async function verifySpaceToken(type, jwt, opts) {
|
|
|
224
276
|
);
|
|
225
277
|
}
|
|
226
278
|
|
|
279
|
+
/**
|
|
280
|
+
* @param {string} didKey
|
|
281
|
+
* @returns {Promise<boolean>}
|
|
282
|
+
*/
|
|
283
|
+
const matchesSignature = async (didKey) => {
|
|
284
|
+
assertKeyAlg(didKey, header.alg);
|
|
285
|
+
try {
|
|
286
|
+
return await opts.verifier.verify(didKey, signingInput, sig);
|
|
287
|
+
} catch (err) {
|
|
288
|
+
throw new SpaceTokenError(
|
|
289
|
+
`could not verify token signature: ${err instanceof Error ? err.message : String(err)}`,
|
|
290
|
+
'BadJwtSignature',
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
227
295
|
const didKey = await opts.getSigningKey(payload.iss, header.kid);
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
if (!valid) {
|
|
238
|
-
throw new SpaceTokenError('invalid token signature', 'BadJwtSignature');
|
|
296
|
+
if (await matchesSignature(didKey)) return { header, payload };
|
|
297
|
+
|
|
298
|
+
// A resolver that holds its answers can name a key the issuer has rotated
|
|
299
|
+
// away from, and the token is signed by the current one.
|
|
300
|
+
const freshDidKey = await opts.getSigningKey(payload.iss, header.kid, true);
|
|
301
|
+
if (freshDidKey !== didKey && (await matchesSignature(freshDidKey))) {
|
|
302
|
+
return { header, payload };
|
|
239
303
|
}
|
|
240
304
|
|
|
241
|
-
|
|
305
|
+
throw new SpaceTokenError('invalid token signature', 'BadJwtSignature');
|
|
242
306
|
}
|
package/src/writer.d.ts
CHANGED
|
@@ -23,6 +23,13 @@ export type SpaceWriteInput = {
|
|
|
23
23
|
* - required for everything but delete
|
|
24
24
|
*/
|
|
25
25
|
record?: Object;
|
|
26
|
+
/**
|
|
27
|
+
* - compare-and-swap: the CID the record must
|
|
28
|
+
* currently have, or null to require that it does not exist. Absent means no
|
|
29
|
+
* check. Checked inside the per-space queue, so the comparison and the commit
|
|
30
|
+
* cannot interleave with another write.
|
|
31
|
+
*/
|
|
32
|
+
swapCid?: string | null;
|
|
26
33
|
};
|
|
27
34
|
export type SpaceWriteResult = {
|
|
28
35
|
action: 'create' | 'update' | 'delete';
|
|
@@ -43,6 +50,10 @@ export type SpaceWriteResult = {
|
|
|
43
50
|
* @property {string} collection
|
|
44
51
|
* @property {string} rkey
|
|
45
52
|
* @property {Object} [record] - required for everything but delete
|
|
53
|
+
* @property {string|null} [swapCid] - compare-and-swap: the CID the record must
|
|
54
|
+
* currently have, or null to require that it does not exist. Absent means no
|
|
55
|
+
* check. Checked inside the per-space queue, so the comparison and the commit
|
|
56
|
+
* cannot interleave with another write.
|
|
46
57
|
*/
|
|
47
58
|
/**
|
|
48
59
|
* @typedef {Object} SpaceWriteResult
|
package/src/writer.js
CHANGED
|
@@ -88,6 +88,10 @@ function serialize(storage, space, fn) {
|
|
|
88
88
|
* @property {string} collection
|
|
89
89
|
* @property {string} rkey
|
|
90
90
|
* @property {Object} [record] - required for everything but delete
|
|
91
|
+
* @property {string|null} [swapCid] - compare-and-swap: the CID the record must
|
|
92
|
+
* currently have, or null to require that it does not exist. Absent means no
|
|
93
|
+
* check. Checked inside the per-space queue, so the comparison and the commit
|
|
94
|
+
* cannot interleave with another write.
|
|
91
95
|
*/
|
|
92
96
|
|
|
93
97
|
/**
|
|
@@ -150,6 +154,12 @@ export async function applyWrites(storage, { space, writes, now }) {
|
|
|
150
154
|
? /** @type {string|null} */ (staged.get(key))
|
|
151
155
|
: await storage.getSpaceRecordCid(space, collection, rkey);
|
|
152
156
|
|
|
157
|
+
if (write.swapCid !== undefined && write.swapCid !== prev) {
|
|
158
|
+
throw new SpaceWriteError(
|
|
159
|
+
`Record ${collection}/${rkey} is at ${prev ?? 'no version'}, expected ${write.swapCid ?? 'no record'}`,
|
|
160
|
+
'InvalidSwap',
|
|
161
|
+
);
|
|
162
|
+
}
|
|
153
163
|
if (write.action === 'create' && prev) {
|
|
154
164
|
throw new SpaceRecordAlreadyExistsError(collection, rkey);
|
|
155
165
|
}
|