@pdsjs/spaces 1.0.0

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.
@@ -0,0 +1,33 @@
1
+ // @pdsjs/spaces/space-row - the one place a SpaceRow is constructed.
2
+
3
+ import { parseSpaceUri } from './uri.js';
4
+
5
+ /**
6
+ * Build a SpaceRow from a space URI.
7
+ *
8
+ * `SpaceRow` carries `spaceDid` and `spaceType` alongside `uri` so the storage
9
+ * adapters can filter on indexed columns without parsing AT-URIs themselves
10
+ * (parsing is domain logic and lives here). Those fields are therefore
11
+ * redundant with `uri`, and nothing downstream re-checks that they agree — so
12
+ * every construction site goes through this function rather than assembling the
13
+ * object by hand.
14
+ *
15
+ * @param {string} uri - space AT-URI
16
+ * @param {Partial<Omit<import('@pdsjs/core/ports').SpaceRow, 'uri'|'spaceDid'|'spaceType'>>} [fields]
17
+ * @returns {import('@pdsjs/core/ports').SpaceRow}
18
+ */
19
+ export function makeSpaceRow(uri, fields = {}) {
20
+ const { spaceDid, spaceType } = parseSpaceUri(uri);
21
+ return {
22
+ uri,
23
+ spaceDid,
24
+ spaceType,
25
+ isOwner: fields.isOwner ?? false,
26
+ policy: fields.policy ?? 'member-list',
27
+ managingApp: fields.managingApp ?? null,
28
+ appAccessType: fields.appAccessType ?? 'open',
29
+ appAllowed: fields.appAllowed ? [...fields.appAllowed] : [],
30
+ createdAt: fields.createdAt ?? new Date().toISOString(),
31
+ deletedAt: fields.deletedAt ?? null,
32
+ };
33
+ }
package/src/token.d.ts ADDED
@@ -0,0 +1,95 @@
1
+ export declare const SPACE_TOKEN_TYPES: {
2
+ delegation: {
3
+ typ: string;
4
+ kid: string;
5
+ expiresInSec: number;
6
+ requireAud: boolean;
7
+ };
8
+ credential: {
9
+ typ: string;
10
+ kid: string;
11
+ expiresInSec: number;
12
+ requireAud: boolean;
13
+ };
14
+ clientAttestation: {
15
+ typ: string;
16
+ kid: undefined;
17
+ expiresInSec: number;
18
+ requireAud: boolean;
19
+ };
20
+ };
21
+ export declare class SpaceTokenError extends Error {
22
+ code: string;
23
+ /**
24
+ * @param {string} message
25
+ * @param {string} [code] - machine-readable error name for the XRPC response
26
+ */
27
+ constructor(message: string, code?: string);
28
+ }
29
+ export type SpaceTokenPayload = {
30
+ iss: string;
31
+ /**
32
+ * - the space URI, or the client_id for an attestation
33
+ */
34
+ sub: string;
35
+ aud?: string;
36
+ iat: number;
37
+ exp: number;
38
+ jti: string;
39
+ };
40
+ export type SpaceTokenHeader = {
41
+ alg: string;
42
+ typ: string;
43
+ kid?: string;
44
+ };
45
+ /**
46
+ * @param {keyof typeof SPACE_TOKEN_TYPES} type
47
+ * @param {{iss: string, sub: string, aud?: string, expiresInSec?: number, kid?: string, alg?: string}} opts
48
+ * @param {{sign: (bytes: Uint8Array) => Promise<Uint8Array>}} signer
49
+ * @returns {Promise<string>}
50
+ */
51
+ export declare function createSpaceToken(type: keyof typeof SPACE_TOKEN_TYPES, opts: {
52
+ iss: string;
53
+ sub: string;
54
+ aud?: string;
55
+ expiresInSec?: number;
56
+ kid?: string;
57
+ alg?: string;
58
+ }, signer: {
59
+ sign: (bytes: Uint8Array) => Promise<Uint8Array>;
60
+ }): Promise<string>;
61
+ /**
62
+ * Structural validation only, no signature check. This is as far as we go for
63
+ * client attestations, whose key comes from the client's JWKS rather than a DID
64
+ * document.
65
+ *
66
+ * @param {keyof typeof SPACE_TOKEN_TYPES} type
67
+ * @param {string} jwt
68
+ * @returns {{header: SpaceTokenHeader, payload: SpaceTokenPayload, signingInput: Uint8Array, sig: Uint8Array}}
69
+ */
70
+ export declare function parseSpaceToken(type: keyof typeof SPACE_TOKEN_TYPES, jwt: string): {
71
+ header: SpaceTokenHeader;
72
+ payload: SpaceTokenPayload;
73
+ signingInput: Uint8Array;
74
+ sig: Uint8Array;
75
+ };
76
+ /**
77
+ * @param {keyof typeof SPACE_TOKEN_TYPES} type
78
+ * @param {string} jwt
79
+ * @param {Object} opts
80
+ * @param {(iss: string, kid?: string) => Promise<string>} opts.getSigningKey
81
+ * - resolves the issuer to a did:key. Given `kid` so it can honour the key id.
82
+ * @param {import('@pdsjs/core/ports').SignatureVerifierPort} opts.verifier
83
+ * @param {string} [opts.aud] - required audience, when the caller knows it
84
+ * @param {string} [opts.sub] - required subject, when the caller knows it
85
+ * @returns {Promise<{header: SpaceTokenHeader, payload: SpaceTokenPayload}>}
86
+ */
87
+ export declare function verifySpaceToken(type: keyof typeof SPACE_TOKEN_TYPES, jwt: string, opts: {
88
+ getSigningKey: (iss: string, kid?: string) => Promise<string>;
89
+ verifier: import('@pdsjs/core/ports').SignatureVerifierPort;
90
+ aud?: string;
91
+ sub?: string;
92
+ }): Promise<{
93
+ header: SpaceTokenHeader;
94
+ payload: SpaceTokenPayload;
95
+ }>;
package/src/token.js ADDED
@@ -0,0 +1,223 @@
1
+ // @pdsjs/spaces/token - the three space JWTs.
2
+ //
3
+ // A delegation token proves the user delegated to an app; a client attestation
4
+ // proves which app is acting; a space credential is what the authority issues in
5
+ // exchange, and is what repo hosts actually accept.
6
+ //
7
+ // They share a wire shape and differ only in who signs, who they address, and
8
+ // how long they live — so they are data, not three implementations.
9
+
10
+ import { base64UrlDecode, base64UrlEncode, bytesToHex } from '@pdsjs/core/crypto';
11
+
12
+ export const SPACE_TOKEN_TYPES = {
13
+ delegation: {
14
+ typ: 'atproto-space-delegation+jwt',
15
+ kid: '#atproto',
16
+ expiresInSec: 60,
17
+ requireAud: true,
18
+ },
19
+ credential: {
20
+ typ: 'atproto-space-credential+jwt',
21
+ kid: '#atproto_space',
22
+ // Multi-use across repo hosts until it expires, so it carries no aud.
23
+ expiresInSec: 7200,
24
+ requireAud: false,
25
+ },
26
+ clientAttestation: {
27
+ typ: 'atproto-client-attestation+jwt',
28
+ kid: undefined,
29
+ expiresInSec: 60,
30
+ requireAud: true,
31
+ },
32
+ };
33
+
34
+ const CLOCK_SKEW_SEC = 5;
35
+
36
+ export class SpaceTokenError extends Error {
37
+ /**
38
+ * @param {string} message
39
+ * @param {string} [code] - machine-readable error name for the XRPC response
40
+ */
41
+ constructor(message, code = 'InvalidToken') {
42
+ super(message);
43
+ this.name = 'SpaceTokenError';
44
+ this.code = code;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * @typedef {Object} SpaceTokenPayload
50
+ * @property {string} iss
51
+ * @property {string} sub - the space URI, or the client_id for an attestation
52
+ * @property {string} [aud]
53
+ * @property {number} iat
54
+ * @property {number} exp
55
+ * @property {string} jti
56
+ */
57
+
58
+ /**
59
+ * @typedef {Object} SpaceTokenHeader
60
+ * @property {string} alg
61
+ * @property {string} typ
62
+ * @property {string} [kid]
63
+ */
64
+
65
+ /**
66
+ * @param {Record<string, unknown>} json
67
+ * @returns {string}
68
+ */
69
+ function jsonToB64Url(json) {
70
+ return base64UrlEncode(new TextEncoder().encode(JSON.stringify(json)));
71
+ }
72
+
73
+ /**
74
+ * @param {string} b64
75
+ * @param {string} part
76
+ * @returns {any}
77
+ */
78
+ function decodeJsonPart(b64, part) {
79
+ try {
80
+ return JSON.parse(new TextDecoder().decode(base64UrlDecode(b64)));
81
+ } catch (err) {
82
+ throw new SpaceTokenError(
83
+ `could not parse token ${part}: ${err instanceof Error ? err.message : String(err)}`,
84
+ 'BadJwt',
85
+ );
86
+ }
87
+ }
88
+
89
+ /**
90
+ * @param {keyof typeof SPACE_TOKEN_TYPES} type
91
+ * @param {{iss: string, sub: string, aud?: string, expiresInSec?: number, kid?: string, alg?: string}} opts
92
+ * @param {{sign: (bytes: Uint8Array) => Promise<Uint8Array>}} signer
93
+ * @returns {Promise<string>}
94
+ */
95
+ export async function createSpaceToken(type, opts, signer) {
96
+ const spec = SPACE_TOKEN_TYPES[type];
97
+ if (spec.requireAud && !opts.aud) {
98
+ throw new SpaceTokenError(`a ${type} token requires an "aud"`);
99
+ }
100
+
101
+ const iat = Math.floor(Date.now() / 1000);
102
+ /** @type {SpaceTokenHeader} */
103
+ const header = { alg: opts.alg ?? 'ES256', typ: spec.typ };
104
+ const kid = opts.kid ?? spec.kid;
105
+ if (kid) header.kid = kid;
106
+
107
+ /** @type {SpaceTokenPayload} */
108
+ const payload = {
109
+ iss: opts.iss,
110
+ sub: opts.sub,
111
+ ...(opts.aud ? { aud: opts.aud } : undefined),
112
+ iat,
113
+ exp: iat + (opts.expiresInSec ?? spec.expiresInSec),
114
+ jti: bytesToHex(crypto.getRandomValues(new Uint8Array(16))),
115
+ };
116
+
117
+ const signingInput = `${jsonToB64Url(header)}.${jsonToB64Url(payload)}`;
118
+ const sig = await signer.sign(new TextEncoder().encode(signingInput));
119
+ return `${signingInput}.${base64UrlEncode(sig)}`;
120
+ }
121
+
122
+ /**
123
+ * Structural validation only, no signature check. This is as far as we go for
124
+ * client attestations, whose key comes from the client's JWKS rather than a DID
125
+ * document.
126
+ *
127
+ * @param {keyof typeof SPACE_TOKEN_TYPES} type
128
+ * @param {string} jwt
129
+ * @returns {{header: SpaceTokenHeader, payload: SpaceTokenPayload, signingInput: Uint8Array, sig: Uint8Array}}
130
+ */
131
+ export function parseSpaceToken(type, jwt) {
132
+ const spec = SPACE_TOKEN_TYPES[type];
133
+
134
+ const parts = typeof jwt === 'string' ? jwt.split('.') : [];
135
+ if (parts.length !== 3) {
136
+ throw new SpaceTokenError('malformed token: expected 3 parts', 'BadJwt');
137
+ }
138
+ const [headerB64, payloadB64, sigB64] = parts;
139
+
140
+ const header = decodeJsonPart(headerB64, 'header');
141
+ const payload = decodeJsonPart(payloadB64, 'payload');
142
+
143
+ if (header.typ !== spec.typ) {
144
+ throw new SpaceTokenError(
145
+ `wrong token type: expected "${spec.typ}", got "${header.typ}"`,
146
+ 'BadJwtType',
147
+ );
148
+ }
149
+ if (typeof header.alg !== 'string' || !header.alg) {
150
+ throw new SpaceTokenError('missing token "alg"', 'BadJwt');
151
+ }
152
+ if (!payload.iss)
153
+ throw new SpaceTokenError('missing token "iss"', 'BadJwtIss');
154
+ if (!payload.sub)
155
+ throw new SpaceTokenError('missing token "sub"', 'BadJwtSub');
156
+ if (typeof payload.exp !== 'number') {
157
+ throw new SpaceTokenError('missing token "exp"', 'BadJwt');
158
+ }
159
+ if (spec.requireAud && !payload.aud) {
160
+ throw new SpaceTokenError('missing token "aud"', 'BadJwtAudience');
161
+ }
162
+ if (type === 'clientAttestation' && payload.iss !== payload.sub) {
163
+ throw new SpaceTokenError(
164
+ 'client attestation "iss" and "sub" must both be the client_id',
165
+ 'BadJwtIss',
166
+ );
167
+ }
168
+
169
+ return {
170
+ header,
171
+ payload,
172
+ signingInput: new TextEncoder().encode(`${headerB64}.${payloadB64}`),
173
+ sig: base64UrlDecode(sigB64),
174
+ };
175
+ }
176
+
177
+ /**
178
+ * @param {keyof typeof SPACE_TOKEN_TYPES} type
179
+ * @param {string} jwt
180
+ * @param {Object} opts
181
+ * @param {(iss: string, kid?: string) => Promise<string>} opts.getSigningKey
182
+ * - resolves the issuer to a did:key. Given `kid` so it can honour the key id.
183
+ * @param {import('@pdsjs/core/ports').SignatureVerifierPort} opts.verifier
184
+ * @param {string} [opts.aud] - required audience, when the caller knows it
185
+ * @param {string} [opts.sub] - required subject, when the caller knows it
186
+ * @returns {Promise<{header: SpaceTokenHeader, payload: SpaceTokenPayload}>}
187
+ */
188
+ export async function verifySpaceToken(type, jwt, opts) {
189
+ const { header, payload, signingInput, sig } = parseSpaceToken(type, jwt);
190
+
191
+ const now = Math.floor(Date.now() / 1000);
192
+ if (now - CLOCK_SKEW_SEC >= payload.exp) {
193
+ throw new SpaceTokenError('token expired', 'JwtExpired');
194
+ }
195
+ if (opts.aud !== undefined && payload.aud !== opts.aud) {
196
+ throw new SpaceTokenError(
197
+ 'token audience does not match this service',
198
+ 'BadJwtAudience',
199
+ );
200
+ }
201
+ if (opts.sub !== undefined && payload.sub !== opts.sub) {
202
+ throw new SpaceTokenError(
203
+ 'token subject does not match the requested space',
204
+ 'BadJwtSub',
205
+ );
206
+ }
207
+
208
+ const didKey = await opts.getSigningKey(payload.iss, header.kid);
209
+ let valid;
210
+ try {
211
+ valid = await opts.verifier.verify(didKey, signingInput, sig);
212
+ } catch (err) {
213
+ throw new SpaceTokenError(
214
+ `could not verify token signature: ${err instanceof Error ? err.message : String(err)}`,
215
+ 'BadJwtSignature',
216
+ );
217
+ }
218
+ if (!valid) {
219
+ throw new SpaceTokenError('invalid token signature', 'BadJwtSignature');
220
+ }
221
+
222
+ return { header, payload };
223
+ }
package/src/uri.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ export declare const SPACE_MARKER = "space";
2
+ /**
3
+ * @param {{spaceDid: string, spaceType: string, skey: string,
4
+ * authorDid?: string|null, collection?: string|null, rkey?: string|null}} parts
5
+ * @returns {string}
6
+ */
7
+ export declare function formatSpaceUri(parts: {
8
+ spaceDid: string;
9
+ spaceType: string;
10
+ skey: string;
11
+ authorDid?: string | null;
12
+ collection?: string | null;
13
+ rkey?: string | null;
14
+ }): string;
15
+ /**
16
+ * @param {string} uri
17
+ * @returns {boolean}
18
+ */
19
+ export declare function isSpaceUri(uri: string): boolean;
20
+ /**
21
+ * @param {string} uri
22
+ * @returns {{spaceDid: string, spaceType: string, skey: string,
23
+ * authorDid: string|null, collection: string|null, rkey: string|null}}
24
+ */
25
+ export declare function parseSpaceUri(uri: string): {
26
+ spaceDid: string;
27
+ spaceType: string;
28
+ skey: string;
29
+ authorDid: string | null;
30
+ collection: string | null;
31
+ rkey: string | null;
32
+ };
package/src/uri.js ADDED
@@ -0,0 +1,89 @@
1
+ // @pdsjs/spaces/uri - permissioned space AT-URI addressing.
2
+ //
3
+ // at://{spaceDid}/space/{spaceType}/{skey}[/{authorDid}/{collection}/{rkey}]
4
+ //
5
+ // Space URIs are distinguished from public ones by the literal `space` segment,
6
+ // which contains no dots, whereas a collection NSID always contains at least two.
7
+
8
+ export const SPACE_MARKER = 'space';
9
+
10
+ /**
11
+ * @param {{spaceDid: string, spaceType: string, skey: string,
12
+ * authorDid?: string|null, collection?: string|null, rkey?: string|null}} parts
13
+ * @returns {string}
14
+ */
15
+ export function formatSpaceUri(parts) {
16
+ const { spaceDid, spaceType, skey, authorDid, collection, rkey } = parts;
17
+ const optionalCount = [authorDid, collection, rkey].filter(Boolean).length;
18
+ if (optionalCount !== 0 && optionalCount !== 3) {
19
+ throw new Error(
20
+ 'authorDid, collection, and rkey must be all present or all absent',
21
+ );
22
+ }
23
+ let uri = `at://${spaceDid}/${SPACE_MARKER}/${spaceType}/${skey}`;
24
+ if (authorDid) uri += `/${authorDid}`;
25
+ if (collection) uri += `/${collection}`;
26
+ if (rkey) uri += `/${rkey}`;
27
+ return uri;
28
+ }
29
+
30
+ /**
31
+ * @param {string} uri
32
+ * @returns {boolean}
33
+ */
34
+ export function isSpaceUri(uri) {
35
+ const segments = splitUri(uri);
36
+ return segments !== null && segments[1] === SPACE_MARKER;
37
+ }
38
+
39
+ /**
40
+ * @param {string} uri
41
+ * @returns {{spaceDid: string, spaceType: string, skey: string,
42
+ * authorDid: string|null, collection: string|null, rkey: string|null}}
43
+ */
44
+ export function parseSpaceUri(uri) {
45
+ const segments = splitUri(uri);
46
+ if (segments === null || segments[1] !== SPACE_MARKER) {
47
+ throw new Error(`not a space URI: ${uri}`);
48
+ }
49
+ const [spaceDid, , spaceType, skey, authorDid, collection, rkey] = segments;
50
+ if (segments.length !== 4 && segments.length !== 7) {
51
+ throw new Error(`invalid space URI: ${uri}`);
52
+ }
53
+ if (!spaceDid.startsWith('did:')) {
54
+ throw new Error(`space authority must be a DID: ${uri}`);
55
+ }
56
+ if (!spaceType.includes('.')) {
57
+ throw new Error(`space type must be an NSID: ${uri}`);
58
+ }
59
+ if (!skey) throw new Error(`invalid space URI: ${uri}`);
60
+ if (segments.length === 4) {
61
+ return {
62
+ spaceDid,
63
+ spaceType,
64
+ skey,
65
+ authorDid: null,
66
+ collection: null,
67
+ rkey: null,
68
+ };
69
+ }
70
+ if (!authorDid.startsWith('did:')) {
71
+ throw new Error(`space record author must be a DID: ${uri}`);
72
+ }
73
+ if (!collection.includes('.')) {
74
+ throw new Error(`collection must be an NSID: ${uri}`);
75
+ }
76
+ if (!rkey) throw new Error(`invalid space URI: ${uri}`);
77
+ return { spaceDid, spaceType, skey, authorDid, collection, rkey };
78
+ }
79
+
80
+ /**
81
+ * @param {string} uri
82
+ * @returns {string[]|null} host followed by path segments, or null if not an at:// URI
83
+ */
84
+ function splitUri(uri) {
85
+ if (!uri.startsWith('at://')) return null;
86
+ const segments = uri.slice('at://'.length).split('/');
87
+ if (segments.length < 2 || segments.some((s) => s === '')) return null;
88
+ return segments;
89
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @param {string} didKey - did:key:z… (the did: prefix is optional)
3
+ * @returns {{curve: 'p256'|'secp256k1', publicKey: Uint8Array}}
4
+ */
5
+ export declare function parseDidKey(didKey: string): {
6
+ curve: 'p256' | 'secp256k1';
7
+ publicKey: Uint8Array;
8
+ };
9
+ /**
10
+ * @param {{secp256k1?: (publicKey: Uint8Array, data: Uint8Array, sig: Uint8Array) => Promise<boolean>}} [opts]
11
+ * @returns {import('@pdsjs/core/ports').SignatureVerifierPort}
12
+ */
13
+ export declare function createVerifier(opts?: {
14
+ secp256k1?: (publicKey: Uint8Array, data: Uint8Array, sig: Uint8Array) => Promise<boolean>;
15
+ }): import('@pdsjs/core/ports').SignatureVerifierPort;
@@ -0,0 +1,74 @@
1
+ // @pdsjs/spaces/verifier - signature verification port.
2
+ //
3
+ // P-256 ships built in on crypto.subtle. secp256k1 is injected: hand-rolling
4
+ // ECDSA verification is a different risk class from hand-rolling a hash, since
5
+ // a subtle bug means accepting a forged credential rather than a failing test.
6
+ // Without an injected verifier, spaces interoperate only with P-256 accounts.
7
+
8
+ import { base58btcDecode, P256_MULTICODEC } from '@pdsjs/core/plc';
9
+
10
+ const SECP256K1_MULTICODEC = [0xe7, 0x01];
11
+
12
+ /**
13
+ * @param {string} didKey - did:key:z… (the did: prefix is optional)
14
+ * @returns {{curve: 'p256'|'secp256k1', publicKey: Uint8Array}}
15
+ */
16
+ export function parseDidKey(didKey) {
17
+ const value = didKey.startsWith('did:key:') ? didKey.slice(8) : didKey;
18
+ if (!value.startsWith('z')) {
19
+ throw new Error(`expected a did:key multibase value, got: ${didKey}`);
20
+ }
21
+ const bytes = base58btcDecode(value.slice(1));
22
+ if (bytes[0] === P256_MULTICODEC[0] && bytes[1] === P256_MULTICODEC[1]) {
23
+ return { curve: 'p256', publicKey: bytes.slice(2) };
24
+ }
25
+ if (
26
+ bytes[0] === SECP256K1_MULTICODEC[0] &&
27
+ bytes[1] === SECP256K1_MULTICODEC[1]
28
+ ) {
29
+ return { curve: 'secp256k1', publicKey: bytes.slice(2) };
30
+ }
31
+ throw new Error(`unsupported did:key multicodec: ${didKey}`);
32
+ }
33
+
34
+ /**
35
+ * @param {{secp256k1?: (publicKey: Uint8Array, data: Uint8Array, sig: Uint8Array) => Promise<boolean>}} [opts]
36
+ * @returns {import('@pdsjs/core/ports').SignatureVerifierPort}
37
+ */
38
+ export function createVerifier(opts = {}) {
39
+ return {
40
+ async verify(didKey, data, sig) {
41
+ const { curve, publicKey } = parseDidKey(didKey);
42
+ if (curve === 'p256') return verifyP256(publicKey, data, sig);
43
+ if (!opts.secp256k1) {
44
+ throw new Error(
45
+ 'secp256k1 verification requires an injected verifier; ' +
46
+ 'this deployment supports P-256 keys only',
47
+ );
48
+ }
49
+ return opts.secp256k1(publicKey, data, sig);
50
+ },
51
+ };
52
+ }
53
+
54
+ /**
55
+ * @param {Uint8Array} publicKey - 33-byte compressed point
56
+ * @param {Uint8Array} data
57
+ * @param {Uint8Array} sig - 64-byte raw r||s
58
+ * @returns {Promise<boolean>}
59
+ */
60
+ async function verifyP256(publicKey, data, sig) {
61
+ const key = await crypto.subtle.importKey(
62
+ 'raw',
63
+ /** @type {BufferSource} */ (publicKey),
64
+ { name: 'ECDSA', namedCurve: 'P-256' },
65
+ false,
66
+ ['verify'],
67
+ );
68
+ return crypto.subtle.verify(
69
+ { name: 'ECDSA', hash: 'SHA-256' },
70
+ key,
71
+ /** @type {BufferSource} */ (sig),
72
+ /** @type {BufferSource} */ (data),
73
+ );
74
+ }
@@ -0,0 +1,77 @@
1
+ export declare const MAX_WRITES_PER_COMMIT = 200;
2
+ export declare class SpaceWriteError extends Error {
3
+ code: string;
4
+ /**
5
+ * @param {string} message
6
+ * @param {string} code - machine-readable error name for the XRPC response
7
+ */
8
+ constructor(message: string, code: string);
9
+ }
10
+ export declare class SpaceRecordNotFoundError extends SpaceWriteError {
11
+ /** @param {string} collection @param {string} rkey */
12
+ constructor(collection: string, rkey: string);
13
+ }
14
+ export declare class SpaceRecordAlreadyExistsError extends SpaceWriteError {
15
+ /** @param {string} collection @param {string} rkey */
16
+ constructor(collection: string, rkey: string);
17
+ }
18
+ export type SpaceWriteInput = {
19
+ action: 'create' | 'update' | 'put' | 'delete';
20
+ collection: string;
21
+ rkey: string;
22
+ /**
23
+ * - required for everything but delete
24
+ */
25
+ record?: Object;
26
+ };
27
+ export type SpaceWriteResult = {
28
+ action: 'create' | 'update' | 'delete';
29
+ collection: string;
30
+ rkey: string;
31
+ /**
32
+ * - null for deletes
33
+ */
34
+ cid: string | null;
35
+ /**
36
+ * - null for creates
37
+ */
38
+ prev: string | null;
39
+ };
40
+ /**
41
+ * @typedef {Object} SpaceWriteInput
42
+ * @property {'create'|'update'|'put'|'delete'} action
43
+ * @property {string} collection
44
+ * @property {string} rkey
45
+ * @property {Object} [record] - required for everything but delete
46
+ */
47
+ /**
48
+ * @typedef {Object} SpaceWriteResult
49
+ * @property {'create'|'update'|'delete'} action
50
+ * @property {string} collection
51
+ * @property {string} rkey
52
+ * @property {string|null} cid - null for deletes
53
+ * @property {string|null} prev - null for creates
54
+ */
55
+ /**
56
+ * Apply a batch of writes to a permissioned repo as one commit.
57
+ *
58
+ * Writes resolve in order against the state the batch has built up, not just
59
+ * what was stored on entry, so a batch can create then update a record and a
60
+ * repeated create is still caught rather than double-counted in the set hash.
61
+ *
62
+ * @param {import('@pdsjs/core/ports').SpaceStoragePort} storage
63
+ * @param {Object} opts
64
+ * @param {string} opts.space - space AT-URI
65
+ * @param {SpaceWriteInput[]} opts.writes
66
+ * @param {string} [opts.now] - ISO timestamp, for deterministic tests
67
+ * @returns {Promise<{rev: string, setHash: Uint8Array, results: SpaceWriteResult[]}>}
68
+ */
69
+ export declare function applyWrites(storage: import('@pdsjs/core/ports').SpaceStoragePort, { space, writes, now }: {
70
+ space: string;
71
+ writes: SpaceWriteInput[];
72
+ now?: string;
73
+ }): Promise<{
74
+ rev: string;
75
+ setHash: Uint8Array;
76
+ results: SpaceWriteResult[];
77
+ }>;