@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.
- package/LICENSE +21 -0
- package/package.json +40 -0
- package/src/authority.d.ts +47 -0
- package/src/authority.js +148 -0
- package/src/blake3.d.ts +14 -0
- package/src/blake3.js +272 -0
- package/src/car.d.ts +41 -0
- package/src/car.js +98 -0
- package/src/commit.d.ts +109 -0
- package/src/commit.js +152 -0
- package/src/handlers/auth.d.ts +67 -0
- package/src/handlers/auth.js +436 -0
- package/src/handlers/manage.d.ts +20 -0
- package/src/handlers/manage.js +508 -0
- package/src/handlers/read.d.ts +18 -0
- package/src/handlers/read.js +394 -0
- package/src/handlers/write.d.ts +18 -0
- package/src/handlers/write.js +462 -0
- package/src/index.d.ts +15 -0
- package/src/index.js +56 -0
- package/src/lthash.d.ts +24 -0
- package/src/lthash.js +86 -0
- package/src/mac.d.ts +50 -0
- package/src/mac.js +107 -0
- package/src/memory-storage.d.ts +4 -0
- package/src/memory-storage.js +244 -0
- package/src/path.d.ts +22 -0
- package/src/path.js +33 -0
- package/src/routes.d.ts +23 -0
- package/src/routes.js +42 -0
- package/src/service-auth.d.ts +42 -0
- package/src/service-auth.js +153 -0
- package/src/space-row.d.ts +15 -0
- package/src/space-row.js +33 -0
- package/src/token.d.ts +95 -0
- package/src/token.js +223 -0
- package/src/uri.d.ts +32 -0
- package/src/uri.js +89 -0
- package/src/verifier.d.ts +15 -0
- package/src/verifier.js +74 -0
- package/src/writer.d.ts +77 -0
- package/src/writer.js +195 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Chad Miller
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pdsjs/spaces",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./src/index.js",
|
|
6
|
+
"types": "./src/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./blake3": "./src/blake3.js",
|
|
10
|
+
"./lthash": "./src/lthash.js",
|
|
11
|
+
"./path": "./src/path.js",
|
|
12
|
+
"./uri": "./src/uri.js",
|
|
13
|
+
"./mac": "./src/mac.js",
|
|
14
|
+
"./verifier": "./src/verifier.js",
|
|
15
|
+
"./commit": "./src/commit.js",
|
|
16
|
+
"./memory-storage": "./src/memory-storage.js",
|
|
17
|
+
"./routes": "./src/routes.js",
|
|
18
|
+
"./space-row": "./src/space-row.js",
|
|
19
|
+
"./writer": "./src/writer.js",
|
|
20
|
+
"./car": "./src/car.js",
|
|
21
|
+
"./token": "./src/token.js",
|
|
22
|
+
"./authority": "./src/authority.js",
|
|
23
|
+
"./service-auth": "./src/service-auth.js"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@pdsjs/core": "1.0.0"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://tangled.org/chadtmiller.com/pds.js",
|
|
35
|
+
"directory": "packages/spaces"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"src"
|
|
39
|
+
]
|
|
40
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export declare const SPACE_KEY_ID = "#atproto_space";
|
|
2
|
+
export declare const SPACE_HOST_ID = "#atproto_space_host";
|
|
3
|
+
export declare const ATPROTO_KEY_ID = "#atproto";
|
|
4
|
+
export declare const ATPROTO_PDS_ID = "#atproto_pds";
|
|
5
|
+
export declare class SpaceAuthorityError extends Error {
|
|
6
|
+
code: string;
|
|
7
|
+
/** @param {string} message @param {string} [code] */
|
|
8
|
+
constructor(message: string, code?: string);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The key that verifies a space authority's credentials.
|
|
12
|
+
*
|
|
13
|
+
* @param {any} didDoc
|
|
14
|
+
* @param {string} did
|
|
15
|
+
* @returns {string} did:key
|
|
16
|
+
*/
|
|
17
|
+
export declare function spaceSigningKey(didDoc: any, did: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* The key that verifies an ordinary account's signatures — delegation tokens and
|
|
20
|
+
* repo commits. Always the account's `#atproto` key.
|
|
21
|
+
*
|
|
22
|
+
* @param {any} didDoc
|
|
23
|
+
* @param {string} did
|
|
24
|
+
* @returns {string} did:key
|
|
25
|
+
*/
|
|
26
|
+
export declare function atprotoSigningKey(didDoc: any, did: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Where a space's authority serves its host endpoints.
|
|
29
|
+
*
|
|
30
|
+
* @param {any} didDoc
|
|
31
|
+
* @param {string} did
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
export declare function spaceHostEndpoint(didDoc: any, did: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* The `aud` a delegation token must carry for a given authority.
|
|
37
|
+
* @param {string} spaceDid
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
export declare function spaceHostAudience(spaceDid: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Build the `getSigningKey` callback verifySpaceToken expects.
|
|
43
|
+
*
|
|
44
|
+
* @param {(did: string) => Promise<any>} resolveDid
|
|
45
|
+
* @returns {(iss: string, kid?: string) => Promise<string>}
|
|
46
|
+
*/
|
|
47
|
+
export declare function didKeyResolver(resolveDid: (did: string) => Promise<any>): (iss: string, kid?: string) => Promise<string>;
|
package/src/authority.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// @pdsjs/spaces/authority - resolving a space authority from its DID document.
|
|
2
|
+
//
|
|
3
|
+
// Proposal 0016 gives a space authority two optional DID document entries, each
|
|
4
|
+
// with a fallback to the account's ordinary atproto entry:
|
|
5
|
+
//
|
|
6
|
+
// verification method #atproto_space -> falls back to #atproto
|
|
7
|
+
// service #atproto_space_host -> falls back to #atproto_pds
|
|
8
|
+
//
|
|
9
|
+
// The service fallback is why a space-unaware PDS still receives space requests:
|
|
10
|
+
// clients resolve the space host to its ordinary PDS endpoint.
|
|
11
|
+
|
|
12
|
+
export const SPACE_KEY_ID = '#atproto_space';
|
|
13
|
+
export const SPACE_HOST_ID = '#atproto_space_host';
|
|
14
|
+
export const ATPROTO_KEY_ID = '#atproto';
|
|
15
|
+
export const ATPROTO_PDS_ID = '#atproto_pds';
|
|
16
|
+
|
|
17
|
+
export class SpaceAuthorityError extends Error {
|
|
18
|
+
/** @param {string} message @param {string} [code] */
|
|
19
|
+
constructor(message, code = 'SpaceAuthorityNotFound') {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = 'SpaceAuthorityError';
|
|
22
|
+
this.code = code;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {any} didDoc
|
|
28
|
+
* @param {string} did
|
|
29
|
+
* @param {string} id - fragment, e.g. '#atproto'
|
|
30
|
+
* @returns {string|null} did:key, or null when absent
|
|
31
|
+
*/
|
|
32
|
+
function verificationMethod(didDoc, did, id) {
|
|
33
|
+
const methods = didDoc?.verificationMethod ?? [];
|
|
34
|
+
for (const method of methods) {
|
|
35
|
+
// A DID document may write the id either fully-qualified or as a bare
|
|
36
|
+
// fragment; accept both.
|
|
37
|
+
if (method?.id === id || method?.id === `${did}${id}`) {
|
|
38
|
+
return method.publicKeyMultibase
|
|
39
|
+
? `did:key:${method.publicKeyMultibase}`
|
|
40
|
+
: null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {any} didDoc
|
|
48
|
+
* @param {string} did
|
|
49
|
+
* @param {string} id
|
|
50
|
+
* @returns {string|null}
|
|
51
|
+
*/
|
|
52
|
+
function serviceEndpoint(didDoc, did, id) {
|
|
53
|
+
const services = didDoc?.service ?? [];
|
|
54
|
+
for (const service of services) {
|
|
55
|
+
if (service?.id === id || service?.id === `${did}${id}`) {
|
|
56
|
+
return typeof service.serviceEndpoint === 'string'
|
|
57
|
+
? service.serviceEndpoint
|
|
58
|
+
: null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The key that verifies a space authority's credentials.
|
|
66
|
+
*
|
|
67
|
+
* @param {any} didDoc
|
|
68
|
+
* @param {string} did
|
|
69
|
+
* @returns {string} did:key
|
|
70
|
+
*/
|
|
71
|
+
export function spaceSigningKey(didDoc, did) {
|
|
72
|
+
const key =
|
|
73
|
+
verificationMethod(didDoc, did, SPACE_KEY_ID) ??
|
|
74
|
+
verificationMethod(didDoc, did, ATPROTO_KEY_ID);
|
|
75
|
+
if (!key) {
|
|
76
|
+
throw new SpaceAuthorityError(
|
|
77
|
+
`No space signing key for ${did}`,
|
|
78
|
+
'SpaceKeyNotFound',
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return key;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The key that verifies an ordinary account's signatures — delegation tokens and
|
|
86
|
+
* repo commits. Always the account's `#atproto` key.
|
|
87
|
+
*
|
|
88
|
+
* @param {any} didDoc
|
|
89
|
+
* @param {string} did
|
|
90
|
+
* @returns {string} did:key
|
|
91
|
+
*/
|
|
92
|
+
export function atprotoSigningKey(didDoc, did) {
|
|
93
|
+
const key = verificationMethod(didDoc, did, ATPROTO_KEY_ID);
|
|
94
|
+
if (!key) {
|
|
95
|
+
throw new SpaceAuthorityError(
|
|
96
|
+
`No atproto signing key for ${did}`,
|
|
97
|
+
'SigningKeyNotFound',
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return key;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Where a space's authority serves its host endpoints.
|
|
105
|
+
*
|
|
106
|
+
* @param {any} didDoc
|
|
107
|
+
* @param {string} did
|
|
108
|
+
* @returns {string}
|
|
109
|
+
*/
|
|
110
|
+
export function spaceHostEndpoint(didDoc, did) {
|
|
111
|
+
const endpoint =
|
|
112
|
+
serviceEndpoint(didDoc, did, SPACE_HOST_ID) ??
|
|
113
|
+
serviceEndpoint(didDoc, did, ATPROTO_PDS_ID);
|
|
114
|
+
if (!endpoint) {
|
|
115
|
+
throw new SpaceAuthorityError(
|
|
116
|
+
`No space host for ${did}`,
|
|
117
|
+
'SpaceHostNotFound',
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return endpoint;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The `aud` a delegation token must carry for a given authority.
|
|
125
|
+
* @param {string} spaceDid
|
|
126
|
+
* @returns {string}
|
|
127
|
+
*/
|
|
128
|
+
export function spaceHostAudience(spaceDid) {
|
|
129
|
+
return `${spaceDid}${SPACE_HOST_ID}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Build the `getSigningKey` callback verifySpaceToken expects.
|
|
134
|
+
*
|
|
135
|
+
* @param {(did: string) => Promise<any>} resolveDid
|
|
136
|
+
* @returns {(iss: string, kid?: string) => Promise<string>}
|
|
137
|
+
*/
|
|
138
|
+
export function didKeyResolver(resolveDid) {
|
|
139
|
+
return async (iss, kid) => {
|
|
140
|
+
const didDoc = await resolveDid(iss);
|
|
141
|
+
if (!didDoc) {
|
|
142
|
+
throw new SpaceAuthorityError(`Could not resolve ${iss}`, 'DidNotFound');
|
|
143
|
+
}
|
|
144
|
+
return kid === SPACE_KEY_ID
|
|
145
|
+
? spaceSigningKey(didDoc, iss)
|
|
146
|
+
: atprotoSigningKey(didDoc, iss);
|
|
147
|
+
};
|
|
148
|
+
}
|
package/src/blake3.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type Output = {
|
|
2
|
+
cv: Uint32Array;
|
|
3
|
+
block: Uint32Array;
|
|
4
|
+
counter: number;
|
|
5
|
+
blockLen: number;
|
|
6
|
+
flags: number;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Unkeyed BLAKE3 with extendable output.
|
|
10
|
+
* @param {Uint8Array} input
|
|
11
|
+
* @param {number} [outLen] - output length in bytes, default 32
|
|
12
|
+
* @returns {Uint8Array}
|
|
13
|
+
*/
|
|
14
|
+
export declare function blake3(input: Uint8Array, outLen?: number): Uint8Array;
|
package/src/blake3.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// @pdsjs/spaces/blake3 - vendored BLAKE3 with extendable output.
|
|
2
|
+
//
|
|
3
|
+
// This is the only hand-written cryptographic primitive in pds.js. WebCrypto
|
|
4
|
+
// has no BLAKE3, and LtHash (proposal 0016) is defined in terms of it. Verified
|
|
5
|
+
// against the official BLAKE3 test vectors in test/fixtures/blake3-vectors.json;
|
|
6
|
+
// do not modify without re-running packages/spaces/test/blake3.test.js.
|
|
7
|
+
//
|
|
8
|
+
// Transliterated from the BLAKE3 reference implementation
|
|
9
|
+
// (https://github.com/BLAKE3-team/BLAKE3, reference_impl/reference_impl.rs),
|
|
10
|
+
// per the spec at https://github.com/BLAKE3-team/BLAKE3-specs. Only the
|
|
11
|
+
// unkeyed, extendable-output mode is implemented — no keyed hash, no
|
|
12
|
+
// derive-key mode.
|
|
13
|
+
|
|
14
|
+
const IV = new Uint32Array([
|
|
15
|
+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c,
|
|
16
|
+
0x1f83d9ab, 0x5be0cd19,
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const MSG_PERMUTATION = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8];
|
|
20
|
+
|
|
21
|
+
const CHUNK_START = 1 << 0;
|
|
22
|
+
const CHUNK_END = 1 << 1;
|
|
23
|
+
const PARENT = 1 << 2;
|
|
24
|
+
const ROOT = 1 << 3;
|
|
25
|
+
|
|
26
|
+
const BLOCK_LEN = 64;
|
|
27
|
+
const CHUNK_LEN = 1024;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A deferred compression: the final block of a chunk or parent, not yet
|
|
31
|
+
* compressed, so the ROOT flag can still be applied to the last one.
|
|
32
|
+
* @typedef {Object} Output
|
|
33
|
+
* @property {Uint32Array} cv
|
|
34
|
+
* @property {Uint32Array} block
|
|
35
|
+
* @property {number} counter
|
|
36
|
+
* @property {number} blockLen
|
|
37
|
+
* @property {number} flags
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {number} x
|
|
42
|
+
* @param {number} n
|
|
43
|
+
* @returns {number}
|
|
44
|
+
*/
|
|
45
|
+
const rotr = (x, n) => ((x >>> n) | (x << (32 - n))) >>> 0;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The quarter-round. Rotations are 16, 12, 8, 7.
|
|
49
|
+
* @param {Uint32Array} s - 16-word compression state, mutated in place
|
|
50
|
+
* @param {number} a
|
|
51
|
+
* @param {number} b
|
|
52
|
+
* @param {number} c
|
|
53
|
+
* @param {number} d
|
|
54
|
+
* @param {number} mx
|
|
55
|
+
* @param {number} my
|
|
56
|
+
* @returns {void}
|
|
57
|
+
*/
|
|
58
|
+
function g(s, a, b, c, d, mx, my) {
|
|
59
|
+
s[a] = (s[a] + s[b] + mx) >>> 0;
|
|
60
|
+
s[d] = rotr(s[d] ^ s[a], 16);
|
|
61
|
+
s[c] = (s[c] + s[d]) >>> 0;
|
|
62
|
+
s[b] = rotr(s[b] ^ s[c], 12);
|
|
63
|
+
s[a] = (s[a] + s[b] + my) >>> 0;
|
|
64
|
+
s[d] = rotr(s[d] ^ s[a], 8);
|
|
65
|
+
s[c] = (s[c] + s[d]) >>> 0;
|
|
66
|
+
s[b] = rotr(s[b] ^ s[c], 7);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Four column mixes then four diagonal mixes.
|
|
71
|
+
* @param {Uint32Array} s - 16-word compression state, mutated in place
|
|
72
|
+
* @param {Uint32Array} m - 16 message words
|
|
73
|
+
* @returns {void}
|
|
74
|
+
*/
|
|
75
|
+
function round(s, m) {
|
|
76
|
+
g(s, 0, 4, 8, 12, m[0], m[1]);
|
|
77
|
+
g(s, 1, 5, 9, 13, m[2], m[3]);
|
|
78
|
+
g(s, 2, 6, 10, 14, m[4], m[5]);
|
|
79
|
+
g(s, 3, 7, 11, 15, m[6], m[7]);
|
|
80
|
+
g(s, 0, 5, 10, 15, m[8], m[9]);
|
|
81
|
+
g(s, 1, 6, 11, 12, m[10], m[11]);
|
|
82
|
+
g(s, 2, 7, 8, 13, m[12], m[13]);
|
|
83
|
+
g(s, 3, 4, 9, 14, m[14], m[15]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {Uint32Array} m - 16 message words
|
|
88
|
+
* @returns {Uint32Array} the message words reordered by MSG_PERMUTATION
|
|
89
|
+
*/
|
|
90
|
+
function permute(m) {
|
|
91
|
+
const out = new Uint32Array(16);
|
|
92
|
+
for (let i = 0; i < 16; i++) out[i] = m[MSG_PERMUTATION[i]];
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {Uint32Array} cv - 8-word chaining value
|
|
98
|
+
* @param {Uint32Array} block - 16 message words
|
|
99
|
+
* @param {number} counter
|
|
100
|
+
* @param {number} blockLen
|
|
101
|
+
* @param {number} flags
|
|
102
|
+
* @returns {Uint32Array} all 16 state words — the first 8 are the chaining
|
|
103
|
+
* value, all 16 are needed for extended output.
|
|
104
|
+
*/
|
|
105
|
+
function compress(cv, block, counter, blockLen, flags) {
|
|
106
|
+
const s = new Uint32Array(16);
|
|
107
|
+
s.set(cv, 0);
|
|
108
|
+
s[8] = IV[0];
|
|
109
|
+
s[9] = IV[1];
|
|
110
|
+
s[10] = IV[2];
|
|
111
|
+
s[11] = IV[3];
|
|
112
|
+
s[12] = counter >>> 0;
|
|
113
|
+
s[13] = Math.floor(counter / 2 ** 32) >>> 0;
|
|
114
|
+
s[14] = blockLen;
|
|
115
|
+
s[15] = flags;
|
|
116
|
+
|
|
117
|
+
let m = block;
|
|
118
|
+
for (let i = 0; i < 7; i++) {
|
|
119
|
+
round(s, m);
|
|
120
|
+
if (i < 6) m = permute(m);
|
|
121
|
+
}
|
|
122
|
+
for (let i = 0; i < 8; i++) {
|
|
123
|
+
s[i] = (s[i] ^ s[i + 8]) >>> 0;
|
|
124
|
+
s[i + 8] = (s[i + 8] ^ cv[i]) >>> 0;
|
|
125
|
+
}
|
|
126
|
+
return s;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Read 16 little-endian uint32s from a 64-byte block.
|
|
131
|
+
* @param {Uint8Array} bytes
|
|
132
|
+
* @param {number} offset
|
|
133
|
+
* @returns {Uint32Array}
|
|
134
|
+
*/
|
|
135
|
+
function wordsFromBlock(bytes, offset) {
|
|
136
|
+
const m = new Uint32Array(16);
|
|
137
|
+
for (let i = 0; i < 16; i++) {
|
|
138
|
+
const o = offset + i * 4;
|
|
139
|
+
m[i] =
|
|
140
|
+
(bytes[o] |
|
|
141
|
+
(bytes[o + 1] << 8) |
|
|
142
|
+
(bytes[o + 2] << 16) |
|
|
143
|
+
(bytes[o + 3] << 24)) >>>
|
|
144
|
+
0;
|
|
145
|
+
}
|
|
146
|
+
return m;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param {Output} o
|
|
151
|
+
* @returns {Uint32Array} the 8-word chaining value
|
|
152
|
+
*/
|
|
153
|
+
function outputChainingValue(o) {
|
|
154
|
+
return compress(o.cv, o.block, o.counter, o.blockLen, o.flags).slice(0, 8);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {Output} o
|
|
159
|
+
* @param {number} outLen - output length in bytes
|
|
160
|
+
* @returns {Uint8Array}
|
|
161
|
+
*/
|
|
162
|
+
function rootOutputBytes(o, outLen) {
|
|
163
|
+
const out = new Uint8Array(outLen);
|
|
164
|
+
let i = 0;
|
|
165
|
+
let counter = 0;
|
|
166
|
+
while (i < outLen) {
|
|
167
|
+
const words = compress(o.cv, o.block, counter, o.blockLen, o.flags | ROOT);
|
|
168
|
+
for (let w = 0; w < 16 && i < outLen; w++) {
|
|
169
|
+
for (let b = 0; b < 4 && i < outLen; b++) {
|
|
170
|
+
out[i++] = (words[w] >>> (8 * b)) & 0xff;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
counter++;
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @param {Uint8Array} input
|
|
180
|
+
* @param {number} start - byte offset of the chunk within input
|
|
181
|
+
* @param {number} len - chunk length in bytes
|
|
182
|
+
* @param {number} chunkCounter
|
|
183
|
+
* @returns {Output}
|
|
184
|
+
*/
|
|
185
|
+
function chunkOutput(input, start, len, chunkCounter) {
|
|
186
|
+
let cv = IV.slice(0, 8);
|
|
187
|
+
let blockFlags = CHUNK_START;
|
|
188
|
+
let pos = 0;
|
|
189
|
+
while (len - pos > BLOCK_LEN) {
|
|
190
|
+
const m = wordsFromBlock(input, start + pos);
|
|
191
|
+
cv = compress(cv, m, chunkCounter, BLOCK_LEN, blockFlags).slice(0, 8);
|
|
192
|
+
pos += BLOCK_LEN;
|
|
193
|
+
blockFlags = 0;
|
|
194
|
+
}
|
|
195
|
+
const blockLen = len - pos;
|
|
196
|
+
const padded = new Uint8Array(BLOCK_LEN);
|
|
197
|
+
padded.set(input.subarray(start + pos, start + len));
|
|
198
|
+
return {
|
|
199
|
+
cv,
|
|
200
|
+
block: wordsFromBlock(padded, 0),
|
|
201
|
+
counter: chunkCounter,
|
|
202
|
+
blockLen,
|
|
203
|
+
flags: blockFlags | CHUNK_END,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* @param {Uint32Array} leftCv - 8-word chaining value
|
|
209
|
+
* @param {Uint32Array} rightCv - 8-word chaining value
|
|
210
|
+
* @returns {Output}
|
|
211
|
+
*/
|
|
212
|
+
function parentOutput(leftCv, rightCv) {
|
|
213
|
+
const block = new Uint32Array(16);
|
|
214
|
+
block.set(leftCv, 0);
|
|
215
|
+
block.set(rightCv, 8);
|
|
216
|
+
return {
|
|
217
|
+
cv: IV.slice(0, 8),
|
|
218
|
+
block,
|
|
219
|
+
counter: 0,
|
|
220
|
+
blockLen: BLOCK_LEN,
|
|
221
|
+
flags: PARENT,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Unkeyed BLAKE3 with extendable output.
|
|
227
|
+
* @param {Uint8Array} input
|
|
228
|
+
* @param {number} [outLen] - output length in bytes, default 32
|
|
229
|
+
* @returns {Uint8Array}
|
|
230
|
+
*/
|
|
231
|
+
export function blake3(input, outLen = 32) {
|
|
232
|
+
const chunks = Math.max(1, Math.ceil(input.length / CHUNK_LEN));
|
|
233
|
+
/** @type {Uint32Array[]} */
|
|
234
|
+
const stack = [];
|
|
235
|
+
/** @type {Output|undefined} */
|
|
236
|
+
let output;
|
|
237
|
+
|
|
238
|
+
for (let i = 0; i < chunks; i++) {
|
|
239
|
+
const start = i * CHUNK_LEN;
|
|
240
|
+
const len = Math.min(CHUNK_LEN, Math.max(0, input.length - start));
|
|
241
|
+
const chunk = chunkOutput(input, start, len, i);
|
|
242
|
+
|
|
243
|
+
// The last chunk stays an Output so the ROOT flag can reach it.
|
|
244
|
+
if (i === chunks - 1) {
|
|
245
|
+
output = chunk;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Merge while the completed-chunk count has a trailing zero bit.
|
|
250
|
+
let cv = outputChainingValue(chunk);
|
|
251
|
+
let n = i + 1;
|
|
252
|
+
while ((n & 1) === 0) {
|
|
253
|
+
// Guaranteed non-empty: n's trailing zero count never exceeds the
|
|
254
|
+
// number of merges already pushed onto the stack.
|
|
255
|
+
cv = outputChainingValue(
|
|
256
|
+
parentOutput(/** @type {Uint32Array} */ (stack.pop()), cv),
|
|
257
|
+
);
|
|
258
|
+
n >>= 1;
|
|
259
|
+
}
|
|
260
|
+
stack.push(cv);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// At least one chunk always runs, so `output` is set by the time either
|
|
264
|
+
// loop below reads it.
|
|
265
|
+
while (stack.length > 0) {
|
|
266
|
+
output = parentOutput(
|
|
267
|
+
/** @type {Uint32Array} */ (stack.pop()),
|
|
268
|
+
outputChainingValue(/** @type {Output} */ (output)),
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
return rootOutputBytes(/** @type {Output} */ (output), outLen);
|
|
272
|
+
}
|
package/src/car.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical DAG-CBOR map key order: shortest key first, then bytewise.
|
|
3
|
+
*
|
|
4
|
+
* This is NOT plain lexicographic order — `a/b` sorts before `aa/b`. A consumer
|
|
5
|
+
* walks the index in the order the CBOR encoder emitted its keys and expects the
|
|
6
|
+
* record blocks to follow, so both have to use this comparison.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} a
|
|
9
|
+
* @param {string} b
|
|
10
|
+
* @returns {number}
|
|
11
|
+
*/
|
|
12
|
+
export declare function byCanonicalKey(a: string, b: string): number;
|
|
13
|
+
export type SerializedRecord = {
|
|
14
|
+
collection: string;
|
|
15
|
+
rkey: string;
|
|
16
|
+
cid: string;
|
|
17
|
+
/**
|
|
18
|
+
* - DAG-CBOR encoded record
|
|
19
|
+
*/
|
|
20
|
+
value: Uint8Array;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} SerializedRecord
|
|
24
|
+
* @property {string} collection
|
|
25
|
+
* @property {string} rkey
|
|
26
|
+
* @property {string} cid
|
|
27
|
+
* @property {Uint8Array} value - DAG-CBOR encoded record
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Serialize a repo as a CAR: two roots in order — the signed commit, then the
|
|
31
|
+
* index — followed by one block per index entry, in the index's order. Blobs are
|
|
32
|
+
* excluded.
|
|
33
|
+
*
|
|
34
|
+
* Records are collected up front because the index has to precede the blocks it
|
|
35
|
+
* describes.
|
|
36
|
+
*
|
|
37
|
+
* @param {import('./commit.js').SignedCommit} commit
|
|
38
|
+
* @param {Iterable<SerializedRecord>} records
|
|
39
|
+
* @returns {Promise<Uint8Array>}
|
|
40
|
+
*/
|
|
41
|
+
export declare function serializeRepo(commit: import('./commit.js').SignedCommit, records: Iterable<SerializedRecord>): Promise<Uint8Array>;
|
package/src/car.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// @pdsjs/spaces/car - permissioned repo CAR serialization.
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildCarFile,
|
|
5
|
+
CID,
|
|
6
|
+
cborEncodeDagCbor,
|
|
7
|
+
cidToBytes,
|
|
8
|
+
cidToString,
|
|
9
|
+
createCid,
|
|
10
|
+
} from '@pdsjs/core/repo';
|
|
11
|
+
import { formatRecordPath } from './path.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Canonical DAG-CBOR map key order: shortest key first, then bytewise.
|
|
15
|
+
*
|
|
16
|
+
* This is NOT plain lexicographic order — `a/b` sorts before `aa/b`. A consumer
|
|
17
|
+
* walks the index in the order the CBOR encoder emitted its keys and expects the
|
|
18
|
+
* record blocks to follow, so both have to use this comparison.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} a
|
|
21
|
+
* @param {string} b
|
|
22
|
+
* @returns {number}
|
|
23
|
+
*/
|
|
24
|
+
export function byCanonicalKey(a, b) {
|
|
25
|
+
if (a.length !== b.length) return a.length - b.length;
|
|
26
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {Object} SerializedRecord
|
|
31
|
+
* @property {string} collection
|
|
32
|
+
* @property {string} rkey
|
|
33
|
+
* @property {string} cid
|
|
34
|
+
* @property {Uint8Array} value - DAG-CBOR encoded record
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Serialize a repo as a CAR: two roots in order — the signed commit, then the
|
|
39
|
+
* index — followed by one block per index entry, in the index's order. Blobs are
|
|
40
|
+
* excluded.
|
|
41
|
+
*
|
|
42
|
+
* Records are collected up front because the index has to precede the blocks it
|
|
43
|
+
* describes.
|
|
44
|
+
*
|
|
45
|
+
* @param {import('./commit.js').SignedCommit} commit
|
|
46
|
+
* @param {Iterable<SerializedRecord>} records
|
|
47
|
+
* @returns {Promise<Uint8Array>}
|
|
48
|
+
*/
|
|
49
|
+
export async function serializeRepo(commit, records) {
|
|
50
|
+
/** @type {Map<string, SerializedRecord>} */
|
|
51
|
+
const byPath = new Map();
|
|
52
|
+
for (const record of records) {
|
|
53
|
+
byPath.set(formatRecordPath(record.collection, record.rkey), record);
|
|
54
|
+
}
|
|
55
|
+
const paths = [...byPath.keys()].sort(byCanonicalKey);
|
|
56
|
+
|
|
57
|
+
/** @type {Record<string, any>} */
|
|
58
|
+
const index = {};
|
|
59
|
+
for (const path of paths) {
|
|
60
|
+
// A CID instance is what the DAG-CBOR encoder emits as a tag-42 link.
|
|
61
|
+
index[path] = new CID(
|
|
62
|
+
cidToBytes(/** @type {SerializedRecord} */ (byPath.get(path)).cid),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const commitBytes = cborEncodeDagCbor(toCommitBlock(commit));
|
|
67
|
+
const indexBytes = cborEncodeDagCbor(index);
|
|
68
|
+
const commitRoot = cidToString(await createCid(commitBytes));
|
|
69
|
+
const indexRoot = cidToString(await createCid(indexBytes));
|
|
70
|
+
|
|
71
|
+
const blocks = [
|
|
72
|
+
{ cid: commitRoot, data: commitBytes },
|
|
73
|
+
{ cid: indexRoot, data: indexBytes },
|
|
74
|
+
...paths.map((path) => {
|
|
75
|
+
const record = /** @type {SerializedRecord} */ (byPath.get(path));
|
|
76
|
+
return { cid: record.cid, data: record.value };
|
|
77
|
+
}),
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
return buildCarFile([commitRoot, indexRoot], blocks);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The commit block is plain DAG-CBOR with byte fields, not the JSON `$bytes`
|
|
85
|
+
* shape used on the wire elsewhere.
|
|
86
|
+
* @param {import('./commit.js').SignedCommit} commit
|
|
87
|
+
* @returns {Object}
|
|
88
|
+
*/
|
|
89
|
+
function toCommitBlock(commit) {
|
|
90
|
+
return {
|
|
91
|
+
ver: commit.ver,
|
|
92
|
+
hash: commit.hash,
|
|
93
|
+
ikm: commit.ikm,
|
|
94
|
+
sig: commit.sig,
|
|
95
|
+
mac: commit.mac,
|
|
96
|
+
rev: commit.rev,
|
|
97
|
+
};
|
|
98
|
+
}
|