@atproto/space 0.0.0-spaces-alpha-20260818022935

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.
Files changed (106) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE.txt +7 -0
  3. package/dist/commit.d.ts +25 -0
  4. package/dist/commit.d.ts.map +1 -0
  5. package/dist/commit.js +80 -0
  6. package/dist/commit.js.map +1 -0
  7. package/dist/credential.d.ts +77 -0
  8. package/dist/credential.d.ts.map +1 -0
  9. package/dist/credential.js +160 -0
  10. package/dist/credential.js.map +1 -0
  11. package/dist/dpop.d.ts +40 -0
  12. package/dist/dpop.d.ts.map +1 -0
  13. package/dist/dpop.js +96 -0
  14. package/dist/dpop.js.map +1 -0
  15. package/dist/error.d.ts +12 -0
  16. package/dist/error.d.ts.map +1 -0
  17. package/dist/error.js +21 -0
  18. package/dist/error.js.map +1 -0
  19. package/dist/index.d.ts +9 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +9 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/lthash.d.ts +18 -0
  24. package/dist/lthash.d.ts.map +1 -0
  25. package/dist/lthash.js +68 -0
  26. package/dist/lthash.js.map +1 -0
  27. package/dist/repo-commit.d.ts +51 -0
  28. package/dist/repo-commit.d.ts.map +1 -0
  29. package/dist/repo-commit.js +137 -0
  30. package/dist/repo-commit.js.map +1 -0
  31. package/dist/space-members.d.ts +27 -0
  32. package/dist/space-members.d.ts.map +1 -0
  33. package/dist/space-members.js +111 -0
  34. package/dist/space-members.js.map +1 -0
  35. package/dist/space-repo.d.ts +33 -0
  36. package/dist/space-repo.d.ts.map +1 -0
  37. package/dist/space-repo.js +136 -0
  38. package/dist/space-repo.js.map +1 -0
  39. package/dist/storage/index.d.ts +3 -0
  40. package/dist/storage/index.d.ts.map +1 -0
  41. package/dist/storage/index.js +19 -0
  42. package/dist/storage/index.js.map +1 -0
  43. package/dist/storage/memory-members-storage.d.ts +12 -0
  44. package/dist/storage/memory-members-storage.d.ts.map +1 -0
  45. package/dist/storage/memory-members-storage.js +36 -0
  46. package/dist/storage/memory-members-storage.js.map +1 -0
  47. package/dist/storage/memory-repo-storage.d.ts +17 -0
  48. package/dist/storage/memory-repo-storage.d.ts.map +1 -0
  49. package/dist/storage/memory-repo-storage.js +56 -0
  50. package/dist/storage/memory-repo-storage.js.map +1 -0
  51. package/dist/storage/types.d.ts +14 -0
  52. package/dist/storage/types.d.ts.map +1 -0
  53. package/dist/storage/types.js +3 -0
  54. package/dist/storage/types.js.map +1 -0
  55. package/dist/sync/car.d.ts +13 -0
  56. package/dist/sync/car.d.ts.map +1 -0
  57. package/dist/sync/car.js +185 -0
  58. package/dist/sync/car.js.map +1 -0
  59. package/dist/sync/consumer.d.ts +39 -0
  60. package/dist/sync/consumer.d.ts.map +1 -0
  61. package/dist/sync/consumer.js +89 -0
  62. package/dist/sync/consumer.js.map +1 -0
  63. package/dist/sync/index.d.ts +3 -0
  64. package/dist/sync/index.d.ts.map +1 -0
  65. package/dist/sync/index.js +3 -0
  66. package/dist/sync/index.js.map +1 -0
  67. package/dist/sync/provider.d.ts +25 -0
  68. package/dist/sync/provider.d.ts.map +1 -0
  69. package/dist/sync/provider.js +60 -0
  70. package/dist/sync/provider.js.map +1 -0
  71. package/dist/sync/repo-index.d.ts +19 -0
  72. package/dist/sync/repo-index.d.ts.map +1 -0
  73. package/dist/sync/repo-index.js +58 -0
  74. package/dist/sync/repo-index.js.map +1 -0
  75. package/dist/types.d.ts +49 -0
  76. package/dist/types.d.ts.map +1 -0
  77. package/dist/types.js +35 -0
  78. package/dist/types.js.map +1 -0
  79. package/dist/util.d.ts +6 -0
  80. package/dist/util.d.ts.map +1 -0
  81. package/dist/util.js +14 -0
  82. package/dist/util.js.map +1 -0
  83. package/package.json +44 -0
  84. package/src/credential.ts +262 -0
  85. package/src/dpop.ts +166 -0
  86. package/src/error.ts +38 -0
  87. package/src/index.ts +8 -0
  88. package/src/lthash.ts +80 -0
  89. package/src/repo-commit.ts +171 -0
  90. package/src/sync/consumer.ts +149 -0
  91. package/src/sync/index.ts +2 -0
  92. package/src/sync/provider.ts +82 -0
  93. package/src/types.ts +62 -0
  94. package/src/util.ts +21 -0
  95. package/tests/credential.test.ts +323 -0
  96. package/tests/dpop.test.ts +234 -0
  97. package/tests/lthash.test.ts +97 -0
  98. package/tests/repo-commit.test.ts +281 -0
  99. package/tests/sync.test.ts +385 -0
  100. package/tsconfig.build.json +16 -0
  101. package/tsconfig.build.tsbuildinfo +1 -0
  102. package/tsconfig.json +7 -0
  103. package/tsconfig.test.json +11 -0
  104. package/tsconfig.test.tsbuildinfo +1 -0
  105. package/tsconfig.tests.tsbuildinfo +1 -0
  106. package/vitest.config.ts +5 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consumer.d.ts","sourceRoot":"","sources":["../../src/sync/consumer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,GAAG,EAAU,MAAM,mBAAmB,CAAA;AAEpD,OAAO,EAAE,UAAU,EAAgB,MAAM,mBAAmB,CAAA;AAC5D,OAAO,EAEL,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,WAAW,EAEjB,MAAM,aAAa,CAAA;AAGpB,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IAEd,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,GAAG,CAAA;IACR,MAAM,EAAE,WAAW,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,YAAY,CAAA;IACpB,KAAK,EAAE,SAAS,CAAA;IAChB,IAAI,EAAE,UAAU,CAAA;IAChB,OAAO,EAAE,cAAc,CAAC,cAAc,CAAC,CAAA;CACxC,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,QACnB,aAAa,CAAC,UAAU,CAAC,GAAG,QAAQ,CAAC,UAAU,CAAC,UAC7C,gBAAgB,KACvB,OAAO,CAAC,YAAY,CAwCtB,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,iBAAiB,QACvB,aAAa,CAAC,UAAU,CAAC,GAAG,QAAQ,CAAC,UAAU,CAAC,UAC7C,gBAAgB,KACvB,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,SAAS,CAAC,GAAG;IAAE,OAAO,EAAE,cAAc,EAAE,CAAA;CAAE,CAOvE,CAAA"}
@@ -0,0 +1,89 @@
1
+ import { readCarStream } from '@atproto/common';
2
+ import { decode } from '@atproto/lex-cbor';
3
+ import { RepoVerificationError } from '../error.js';
4
+ import { RepoCommit, verifyCommit } from '../repo-commit.js';
5
+ import { def, } from '../types.js';
6
+ import { parseRecordPath } from '../util.js';
7
+ /**
8
+ * Verify a serialized repo, streaming out its records.
9
+ *
10
+ * The three stages follow the CAR's layout, so nothing needs buffering: the
11
+ * commit, then the index against the commit's hash (which authenticates every
12
+ * path/cid pair without reading a record), then each block against its index
13
+ * entry. That last stage runs as `records` is drained, so it must be consumed to
14
+ * know the repo was complete — {@link verifyRepoCarFull} does that.
15
+ */
16
+ export const verifyRepoCar = async (car, params) => {
17
+ // The reader hashes every block against its cid as it streams.
18
+ const { roots, blocks } = await readCarStream(car);
19
+ if (roots.length !== 2) {
20
+ throw new RepoVerificationError(`expected 2 car roots (commit, index), got ${roots.length}`);
21
+ }
22
+ const [commitRoot, indexRoot] = roots;
23
+ const commitBlock = await blocks.next();
24
+ if (commitBlock.done || !commitBlock.value.cid.equals(commitRoot)) {
25
+ throw new RepoVerificationError('expected the commit block to lead the car');
26
+ }
27
+ const commit = parseBlock(commitBlock.value.bytes, def.signedCommit);
28
+ const ctx = { ...params, rev: commit.rev };
29
+ if (!(await verifyCommit(commit, ctx, params.didKey))) {
30
+ throw new RepoVerificationError('commit failed verification');
31
+ }
32
+ const indexBlock = await blocks.next();
33
+ if (indexBlock.done || !indexBlock.value.cid.equals(indexRoot)) {
34
+ throw new RepoVerificationError('expected the index block to follow the commit');
35
+ }
36
+ const index = parseBlock(indexBlock.value.bytes, def.repoIndex);
37
+ const repo = RepoCommit.fromIndex(index);
38
+ if (!repo.matches(commit)) {
39
+ throw new RepoVerificationError('index does not match the commit hash');
40
+ }
41
+ return {
42
+ commit,
43
+ index,
44
+ repo,
45
+ records: verifyRecords(blocks, index, params.expectValues !== false),
46
+ };
47
+ };
48
+ /**
49
+ * Verify a serialized repo and collect its records. Prefer {@link verifyRepoCar}
50
+ * for large repos, where streaming avoids holding every record at once.
51
+ */
52
+ export const verifyRepoCarFull = async (car, params) => {
53
+ const { records, ...rest } = await verifyRepoCar(car, params);
54
+ const collected = [];
55
+ for await (const record of records) {
56
+ collected.push(record);
57
+ }
58
+ return { ...rest, records: collected };
59
+ };
60
+ // The car holds one record block per index entry, in the index's order.
61
+ async function* verifyRecords(blocks, index, expectValues) {
62
+ const paths = Object.keys(index);
63
+ let i = 0;
64
+ for await (const block of blocks) {
65
+ if (i >= paths.length) {
66
+ throw new RepoVerificationError('car has more blocks than index entries');
67
+ }
68
+ const path = paths[i];
69
+ const cid = index[path];
70
+ i++;
71
+ if (!block.cid.equals(cid)) {
72
+ throw new RepoVerificationError(`expected block ${cid} at ${path}, got ${block.cid}`);
73
+ }
74
+ const { collection, rkey } = parseRecordPath(path);
75
+ yield { collection, rkey, cid, record: decode(block.bytes) };
76
+ }
77
+ const isIndexOnly = !expectValues && i === 0;
78
+ if (i < paths.length && !isIndexOnly) {
79
+ throw new RepoVerificationError(`car is missing ${paths.length - i} record(s) named in the index`);
80
+ }
81
+ }
82
+ const parseBlock = (bytes, def) => {
83
+ const parsed = def.schema.safeParse(decode(bytes));
84
+ if (!parsed.success) {
85
+ throw new RepoVerificationError(`invalid ${def.name}: ${parsed.error.message}`);
86
+ }
87
+ return parsed.data;
88
+ };
89
+ //# sourceMappingURL=consumer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consumer.js","sourceRoot":"","sources":["../../src/sync/consumer.ts"],"names":[],"mappings":"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","sourcesContent":["import { type check, readCarStream } from '@atproto/common'\nimport { decode } from '@atproto/lex-cbor'\nimport type { Cid, LexMap } from '@atproto/lex-data'\nimport { RepoVerificationError } from '../error.js'\nimport { RepoCommit, verifyCommit } from '../repo-commit.js'\nimport {\n type CommitCtx,\n type RepoIndex,\n type SignedCommit,\n type SpaceRecord,\n def,\n} from '../types.js'\nimport { parseRecordPath } from '../util.js'\n\nexport type VerifyRepoParams = {\n space: string\n author: string\n didKey: string\n // False for an index-only car, which carries no record blocks. Defaults to true.\n expectValues?: boolean\n}\n\nexport type VerifiedRecord = {\n collection: string\n rkey: string\n cid: Cid\n record: SpaceRecord\n}\n\nexport type VerifiedRepo = {\n commit: SignedCommit\n index: RepoIndex\n repo: RepoCommit\n records: AsyncGenerator<VerifiedRecord>\n}\n\n/**\n * Verify a serialized repo, streaming out its records.\n *\n * The three stages follow the CAR's layout, so nothing needs buffering: the\n * commit, then the index against the commit's hash (which authenticates every\n * path/cid pair without reading a record), then each block against its index\n * entry. That last stage runs as `records` is drained, so it must be consumed to\n * know the repo was complete — {@link verifyRepoCarFull} does that.\n */\nexport const verifyRepoCar = async (\n car: AsyncIterable<Uint8Array> | Iterable<Uint8Array>,\n params: VerifyRepoParams,\n): Promise<VerifiedRepo> => {\n // The reader hashes every block against its cid as it streams.\n const { roots, blocks } = await readCarStream(car)\n if (roots.length !== 2) {\n throw new RepoVerificationError(\n `expected 2 car roots (commit, index), got ${roots.length}`,\n )\n }\n const [commitRoot, indexRoot] = roots\n\n const commitBlock = await blocks.next()\n if (commitBlock.done || !commitBlock.value.cid.equals(commitRoot)) {\n throw new RepoVerificationError('expected the commit block to lead the car')\n }\n const commit = parseBlock(commitBlock.value.bytes, def.signedCommit)\n\n const ctx: CommitCtx = { ...params, rev: commit.rev }\n if (!(await verifyCommit(commit, ctx, params.didKey))) {\n throw new RepoVerificationError('commit failed verification')\n }\n\n const indexBlock = await blocks.next()\n if (indexBlock.done || !indexBlock.value.cid.equals(indexRoot)) {\n throw new RepoVerificationError(\n 'expected the index block to follow the commit',\n )\n }\n const index = parseBlock(indexBlock.value.bytes, def.repoIndex)\n\n const repo = RepoCommit.fromIndex(index)\n if (!repo.matches(commit)) {\n throw new RepoVerificationError('index does not match the commit hash')\n }\n\n return {\n commit,\n index,\n repo,\n records: verifyRecords(blocks, index, params.expectValues !== false),\n }\n}\n\n/**\n * Verify a serialized repo and collect its records. Prefer {@link verifyRepoCar}\n * for large repos, where streaming avoids holding every record at once.\n */\nexport const verifyRepoCarFull = async (\n car: AsyncIterable<Uint8Array> | Iterable<Uint8Array>,\n params: VerifyRepoParams,\n): Promise<Omit<VerifiedRepo, 'records'> & { records: VerifiedRecord[] }> => {\n const { records, ...rest } = await verifyRepoCar(car, params)\n const collected: VerifiedRecord[] = []\n for await (const record of records) {\n collected.push(record)\n }\n return { ...rest, records: collected }\n}\n\n// The car holds one record block per index entry, in the index's order.\nasync function* verifyRecords(\n blocks: AsyncIterable<{ cid: Cid; bytes: Uint8Array }>,\n index: RepoIndex,\n expectValues: boolean,\n): AsyncGenerator<VerifiedRecord> {\n const paths = Object.keys(index)\n let i = 0\n\n for await (const block of blocks) {\n if (i >= paths.length) {\n throw new RepoVerificationError('car has more blocks than index entries')\n }\n const path = paths[i]\n const cid = index[path]\n i++\n\n if (!block.cid.equals(cid)) {\n throw new RepoVerificationError(\n `expected block ${cid} at ${path}, got ${block.cid}`,\n )\n }\n const { collection, rkey } = parseRecordPath(path)\n yield { collection, rkey, cid, record: decode(block.bytes) as LexMap }\n }\n\n const isIndexOnly = !expectValues && i === 0\n if (i < paths.length && !isIndexOnly) {\n throw new RepoVerificationError(\n `car is missing ${paths.length - i} record(s) named in the index`,\n )\n }\n}\n\nconst parseBlock = <T>(bytes: Uint8Array, def: check.Def<T>): T => {\n const parsed = def.schema.safeParse(decode(bytes))\n if (!parsed.success) {\n throw new RepoVerificationError(\n `invalid ${def.name}: ${parsed.error.message}`,\n )\n }\n return parsed.data\n}\n"]}
@@ -0,0 +1,3 @@
1
+ export * from './consumer.js';
2
+ export * from './provider.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sync/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA"}
@@ -0,0 +1,3 @@
1
+ export * from './consumer.js';
2
+ export * from './provider.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/sync/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA","sourcesContent":["export * from './consumer.js'\nexport * from './provider.js'\n"]}
@@ -0,0 +1,25 @@
1
+ import { type Cid } from '@atproto/lex-data';
2
+ import type { SignedCommit, SpaceRecord } from '../types.js';
3
+ export type SerializedRecord = {
4
+ collection: string;
5
+ rkey: string;
6
+ cid: Cid;
7
+ bytes: Uint8Array;
8
+ };
9
+ /**
10
+ * Serialize a repo as a car: two roots in order — the signed commit, then the
11
+ * index — followed by one block per index entry, in the index's order. Blobs are
12
+ * excluded.
13
+ *
14
+ * Records are collected up front because the index has to precede the blocks it
15
+ * describes.
16
+ *
17
+ * With `excludeValues` only the two roots are written. The index still
18
+ * authenticates against the commit, since the set hash is folded from the index's
19
+ * entries rather than from the blocks.
20
+ */
21
+ export declare function serializeRepo(commit: SignedCommit, records: AsyncIterable<SerializedRecord> | Iterable<SerializedRecord>, opts?: {
22
+ excludeValues?: boolean;
23
+ }): AsyncIterable<Uint8Array>;
24
+ export declare const serializeRecord: (collection: string, rkey: string, record: SpaceRecord) => Promise<SerializedRecord>;
25
+ //# sourceMappingURL=provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/sync/provider.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,GAAG,EAAc,MAAM,mBAAmB,CAAA;AACxD,OAAO,KAAK,EAAa,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAGvE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,GAAG,CAAA;IACR,KAAK,EAAE,UAAU,CAAA;CAClB,CAAA;AAED;;;;;;;;;;;GAWG;AACH,wBAAuB,aAAa,CAClC,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,aAAa,CAAC,gBAAgB,CAAC,GAAG,QAAQ,CAAC,gBAAgB,CAAC,EACrE,IAAI,GAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAA;CAAO,GACrC,aAAa,CAAC,UAAU,CAAC,CAiC3B;AAQD,eAAO,MAAM,eAAe,eACd,MAAM,QACZ,MAAM,UACJ,WAAW,KAClB,OAAO,CAAC,gBAAgB,CAO1B,CAAA"}
@@ -0,0 +1,60 @@
1
+ import { writeCarStream } from '@atproto/common';
2
+ import { cidForLex, encode } from '@atproto/lex-cbor';
3
+ import { cidForCbor } from '@atproto/lex-data';
4
+ import { formatRecordPath } from '../util.js';
5
+ /**
6
+ * Serialize a repo as a car: two roots in order — the signed commit, then the
7
+ * index — followed by one block per index entry, in the index's order. Blobs are
8
+ * excluded.
9
+ *
10
+ * Records are collected up front because the index has to precede the blocks it
11
+ * describes.
12
+ *
13
+ * With `excludeValues` only the two roots are written. The index still
14
+ * authenticates against the commit, since the set hash is folded from the index's
15
+ * entries rather than from the blocks.
16
+ */
17
+ export async function* serializeRepo(commit, records, opts = {}) {
18
+ const byPath = new Map();
19
+ for await (const record of records) {
20
+ byPath.set(formatRecordPath(record.collection, record.rkey), record);
21
+ }
22
+ // A consumer walks the index as the cbor encoder ordered its keys, so blocks
23
+ // have to follow that order too.
24
+ const paths = [...byPath.keys()].sort(byCanonicalKey);
25
+ const index = {};
26
+ for (const path of paths) {
27
+ index[path] = byPath.get(path).cid;
28
+ }
29
+ const commitBytes = encode(commit);
30
+ const indexBytes = encode(index);
31
+ const [commitRoot, indexRoot] = await Promise.all([
32
+ cidForCbor(commitBytes),
33
+ cidForCbor(indexBytes),
34
+ ]);
35
+ yield* writeCarStream([commitRoot, indexRoot], (function* () {
36
+ yield { cid: commitRoot, bytes: commitBytes };
37
+ yield { cid: indexRoot, bytes: indexBytes };
38
+ if (opts.excludeValues)
39
+ return;
40
+ for (const path of paths) {
41
+ const record = byPath.get(path);
42
+ yield { cid: record.cid, bytes: record.bytes };
43
+ }
44
+ })());
45
+ }
46
+ // Canonical dag-cbor map key order: shortest first, then bytewise.
47
+ const byCanonicalKey = (a, b) => {
48
+ if (a.length !== b.length)
49
+ return a.length - b.length;
50
+ return a < b ? -1 : a > b ? 1 : 0;
51
+ };
52
+ export const serializeRecord = async (collection, rkey, record) => {
53
+ return {
54
+ collection,
55
+ rkey,
56
+ cid: await cidForLex(record),
57
+ bytes: encode(record),
58
+ };
59
+ };
60
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/sync/provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAC/D,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,EAAY,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAExD,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAS7C;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,aAAa,CAClC,MAAoB,EACpB,OAAqE,EACrE,IAAI,GAAgC,EAAE;IAEtC,MAAM,MAAM,GAAG,IAAI,GAAG,EAA4B,CAAA;IAClD,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QACnC,MAAM,CAAC,GAAG,CAAC,gBAAgB,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAA;IACtE,CAAC;IACD,6EAA6E;IAC7E,iCAAiC;IACjC,MAAM,KAAK,GAAG,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAA;IAErD,MAAM,KAAK,GAAc,EAAE,CAAA;IAC3B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAE,CAAC,GAAG,CAAA;IACrC,CAAC;IAED,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,CAAA;IAClC,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAChC,MAAM,CAAC,UAAU,EAAE,SAAS,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAChD,UAAU,CAAC,WAAW,CAAC;QACvB,UAAU,CAAC,UAAU,CAAC;KACvB,CAAC,CAAA;IAEF,KAAK,CAAC,CAAC,cAAc,CACnB,CAAC,UAAU,EAAE,SAAS,CAAC,EACvB,CAAC,QAAQ,CAAC;QACR,MAAM,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,WAAW,EAAE,CAAA;QAC7C,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,UAAU,EAAE,CAAA;QAC3C,IAAI,IAAI,CAAC,aAAa;YAAE,OAAM;QAC9B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAE,CAAA;YAChC,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAA;QAChD,CAAC;IACH,CAAC,CAAC,EAAE,CACL,CAAA;AACH,CAAC;AAED,mEAAmE;AACnE,MAAM,cAAc,GAAG,CAAC,CAAS,EAAE,CAAS,EAAU,EAAE;IACtD,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAA;IACrD,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AACnC,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,EAClC,UAAkB,EAClB,IAAY,EACZ,MAAmB,EACQ,EAAE;IAC7B,OAAO;QACL,UAAU;QACV,IAAI;QACJ,GAAG,EAAE,MAAM,SAAS,CAAC,MAAM,CAAC;QAC5B,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC;KACtB,CAAA;AACH,CAAC,CAAA","sourcesContent":["import { type CarBlock, writeCarStream } from '@atproto/common'\nimport { cidForLex, encode } from '@atproto/lex-cbor'\nimport { type Cid, cidForCbor } from '@atproto/lex-data'\nimport type { RepoIndex, SignedCommit, SpaceRecord } from '../types.js'\nimport { formatRecordPath } from '../util.js'\n\nexport type SerializedRecord = {\n collection: string\n rkey: string\n cid: Cid\n bytes: Uint8Array\n}\n\n/**\n * Serialize a repo as a car: two roots in order — the signed commit, then the\n * index — followed by one block per index entry, in the index's order. Blobs are\n * excluded.\n *\n * Records are collected up front because the index has to precede the blocks it\n * describes.\n *\n * With `excludeValues` only the two roots are written. The index still\n * authenticates against the commit, since the set hash is folded from the index's\n * entries rather than from the blocks.\n */\nexport async function* serializeRepo(\n commit: SignedCommit,\n records: AsyncIterable<SerializedRecord> | Iterable<SerializedRecord>,\n opts: { excludeValues?: boolean } = {},\n): AsyncIterable<Uint8Array> {\n const byPath = new Map<string, SerializedRecord>()\n for await (const record of records) {\n byPath.set(formatRecordPath(record.collection, record.rkey), record)\n }\n // A consumer walks the index as the cbor encoder ordered its keys, so blocks\n // have to follow that order too.\n const paths = [...byPath.keys()].sort(byCanonicalKey)\n\n const index: RepoIndex = {}\n for (const path of paths) {\n index[path] = byPath.get(path)!.cid\n }\n\n const commitBytes = encode(commit)\n const indexBytes = encode(index)\n const [commitRoot, indexRoot] = await Promise.all([\n cidForCbor(commitBytes),\n cidForCbor(indexBytes),\n ])\n\n yield* writeCarStream(\n [commitRoot, indexRoot],\n (function* (): Generator<CarBlock> {\n yield { cid: commitRoot, bytes: commitBytes }\n yield { cid: indexRoot, bytes: indexBytes }\n if (opts.excludeValues) return\n for (const path of paths) {\n const record = byPath.get(path)!\n yield { cid: record.cid, bytes: record.bytes }\n }\n })(),\n )\n}\n\n// Canonical dag-cbor map key order: shortest first, then bytewise.\nconst byCanonicalKey = (a: string, b: string): number => {\n if (a.length !== b.length) return a.length - b.length\n return a < b ? -1 : a > b ? 1 : 0\n}\n\nexport const serializeRecord = async (\n collection: string,\n rkey: string,\n record: SpaceRecord,\n): Promise<SerializedRecord> => {\n return {\n collection,\n rkey,\n cid: await cidForLex(record),\n bytes: encode(record),\n }\n}\n"]}
@@ -0,0 +1,19 @@
1
+ import { Cid } from '@atproto/lex-data';
2
+ /**
3
+ * The index of a serialized repo: `"{collection}/{rkey}"` to the record's cid, in
4
+ * the order record blocks follow it.
5
+ *
6
+ * Folding an index into a `RepoCommit` and comparing against a verified commit
7
+ * authenticates every path/cid pair without reading a single record.
8
+ */
9
+ export type RepoIndex = Map<string, Cid>;
10
+ export type RepoIndexEntry = {
11
+ collection: string;
12
+ rkey: string;
13
+ cid: Cid;
14
+ };
15
+ export declare const repoIndex: (entries: Iterable<RepoIndexEntry>) => RepoIndex;
16
+ export declare const repoIndexEntries: (index: RepoIndex) => RepoIndexEntry[];
17
+ export declare const encodeRepoIndex: (index: RepoIndex) => Uint8Array;
18
+ export declare const decodeRepoIndex: (bytes: Uint8Array) => RepoIndex;
19
+ //# sourceMappingURL=repo-index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo-index.d.ts","sourceRoot":"","sources":["../../src/sync/repo-index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,GAAG,EAAS,MAAM,mBAAmB,CAAA;AAI9C;;;;;;GAMG;AACH,MAAM,MAAM,SAAS,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;AAExC,MAAM,MAAM,cAAc,GAAG;IAC3B,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,GAAG,CAAA;CACT,CAAA;AAED,eAAO,MAAM,SAAS,YACX,QAAQ,CAAC,cAAc,CAAC,KAChC,SAMF,CAAA;AAED,eAAO,MAAM,gBAAgB,UAAW,SAAS,KAAG,cAAc,EACI,CAAA;AAEtE,eAAO,MAAM,eAAe,UAAW,SAAS,KAAG,UAChB,CAAA;AAEnC,eAAO,MAAM,eAAe,UAAW,UAAU,KAAG,SAwBnD,CAAA"}
@@ -0,0 +1,58 @@
1
+ import { decode, encode } from '@atproto/lex-cbor';
2
+ import { ifCid } from '@atproto/lex-data';
3
+ import { RepoVerificationError } from '../error.js';
4
+ import { formatRecordPath, parseRecordPath } from '../util.js';
5
+ export const repoIndex = (entries) => {
6
+ const index = new Map();
7
+ for (const { collection, rkey, cid } of entries) {
8
+ index.set(formatRecordPath(collection, rkey), cid);
9
+ }
10
+ return canonicalize(index);
11
+ };
12
+ export const repoIndexEntries = (index) => [...index].map(([path, cid]) => ({ ...parseRecordPath(path), cid }));
13
+ export const encodeRepoIndex = (index) => encode(Object.fromEntries(index));
14
+ export const decodeRepoIndex = (bytes) => {
15
+ const decoded = decode(bytes);
16
+ if (decoded === null ||
17
+ typeof decoded !== 'object' ||
18
+ Array.isArray(decoded)) {
19
+ throw new RepoVerificationError('Repo index must be a map');
20
+ }
21
+ const index = new Map();
22
+ for (const [path, value] of Object.entries(decoded)) {
23
+ try {
24
+ parseRecordPath(path);
25
+ }
26
+ catch {
27
+ throw new RepoVerificationError(`Invalid path in repo index: ${path}`);
28
+ }
29
+ const cid = ifCid(value);
30
+ if (!cid) {
31
+ throw new RepoVerificationError(`Invalid cid in repo index at: ${path}`);
32
+ }
33
+ index.set(path, cid);
34
+ }
35
+ return canonicalize(index);
36
+ };
37
+ // Canonical DAG-CBOR key order: length first, then bytewise — not lexicographic.
38
+ // Sorting on construction means an index built from db rows iterates the same way
39
+ // as one decoded from a block, so provider and consumer always agree.
40
+ const canonicalize = (index) => {
41
+ const encoder = new TextEncoder();
42
+ const keyed = [...index.keys()].map((path) => ({
43
+ path,
44
+ bytes: encoder.encode(path),
45
+ }));
46
+ keyed.sort((a, b) => {
47
+ if (a.bytes.length !== b.bytes.length) {
48
+ return a.bytes.length - b.bytes.length;
49
+ }
50
+ for (let i = 0; i < a.bytes.length; i++) {
51
+ if (a.bytes[i] !== b.bytes[i])
52
+ return a.bytes[i] - b.bytes[i];
53
+ }
54
+ return 0;
55
+ });
56
+ return new Map(keyed.map(({ path }) => [path, index.get(path)]));
57
+ };
58
+ //# sourceMappingURL=repo-index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo-index.js","sourceRoot":"","sources":["../../src/sync/repo-index.ts"],"names":[],"mappings":"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","sourcesContent":["import { decode, encode } from '@atproto/lex-cbor'\nimport { Cid, ifCid } from '@atproto/lex-data'\nimport { RepoVerificationError } from '../error.js'\nimport { formatRecordPath, parseRecordPath } from '../util.js'\n\n/**\n * The index of a serialized repo: `\"{collection}/{rkey}\"` to the record's cid, in\n * the order record blocks follow it.\n *\n * Folding an index into a `RepoCommit` and comparing against a verified commit\n * authenticates every path/cid pair without reading a single record.\n */\nexport type RepoIndex = Map<string, Cid>\n\nexport type RepoIndexEntry = {\n collection: string\n rkey: string\n cid: Cid\n}\n\nexport const repoIndex = (\n entries: Iterable<RepoIndexEntry>,\n): RepoIndex => {\n const index: RepoIndex = new Map()\n for (const { collection, rkey, cid } of entries) {\n index.set(formatRecordPath(collection, rkey), cid)\n }\n return canonicalize(index)\n}\n\nexport const repoIndexEntries = (index: RepoIndex): RepoIndexEntry[] =>\n [...index].map(([path, cid]) => ({ ...parseRecordPath(path), cid }))\n\nexport const encodeRepoIndex = (index: RepoIndex): Uint8Array =>\n encode(Object.fromEntries(index))\n\nexport const decodeRepoIndex = (bytes: Uint8Array): RepoIndex => {\n const decoded = decode(bytes)\n if (\n decoded === null ||\n typeof decoded !== 'object' ||\n Array.isArray(decoded)\n ) {\n throw new RepoVerificationError('Repo index must be a map')\n }\n\n const index: RepoIndex = new Map()\n for (const [path, value] of Object.entries(decoded)) {\n try {\n parseRecordPath(path)\n } catch {\n throw new RepoVerificationError(`Invalid path in repo index: ${path}`)\n }\n const cid = ifCid(value)\n if (!cid) {\n throw new RepoVerificationError(`Invalid cid in repo index at: ${path}`)\n }\n index.set(path, cid)\n }\n return canonicalize(index)\n}\n\n// Canonical DAG-CBOR key order: length first, then bytewise — not lexicographic.\n// Sorting on construction means an index built from db rows iterates the same way\n// as one decoded from a block, so provider and consumer always agree.\nconst canonicalize = (index: RepoIndex): RepoIndex => {\n const encoder = new TextEncoder()\n const keyed = [...index.keys()].map((path) => ({\n path,\n bytes: encoder.encode(path),\n }))\n keyed.sort((a, b) => {\n if (a.bytes.length !== b.bytes.length) {\n return a.bytes.length - b.bytes.length\n }\n for (let i = 0; i < a.bytes.length; i++) {\n if (a.bytes[i] !== b.bytes[i]) return a.bytes[i] - b.bytes[i]\n }\n return 0\n })\n return new Map(keyed.map(({ path }) => [path, index.get(path)!]))\n}\n"]}
@@ -0,0 +1,49 @@
1
+ import { z } from 'zod';
2
+ import type { check } from '@atproto/common';
3
+ import { type Cid, type LexMap } from '@atproto/lex-data';
4
+ export type SpaceRecord = LexMap;
5
+ export type RecordPath = {
6
+ collection: string;
7
+ rkey: string;
8
+ };
9
+ export type RepoOp = RecordPath & {
10
+ cid: Cid | null;
11
+ prev: Cid | null;
12
+ };
13
+ export declare const COMMIT_VERSION = 1;
14
+ export type CommitCtx = {
15
+ space: string;
16
+ author: string;
17
+ rev: string;
18
+ };
19
+ declare const signedCommit: z.ZodObject<{
20
+ ver: z.ZodLiteral<1>;
21
+ hash: z.ZodType<Uint8Array<ArrayBufferLike>, z.ZodTypeDef, Uint8Array<ArrayBufferLike>>;
22
+ ikm: z.ZodType<Uint8Array<ArrayBufferLike>, z.ZodTypeDef, Uint8Array<ArrayBufferLike>>;
23
+ sig: z.ZodType<Uint8Array<ArrayBufferLike>, z.ZodTypeDef, Uint8Array<ArrayBufferLike>>;
24
+ mac: z.ZodType<Uint8Array<ArrayBufferLike>, z.ZodTypeDef, Uint8Array<ArrayBufferLike>>;
25
+ rev: z.ZodString;
26
+ }, "strip", z.ZodTypeAny, {
27
+ ver: 1;
28
+ hash: Uint8Array<ArrayBufferLike>;
29
+ ikm: Uint8Array<ArrayBufferLike>;
30
+ sig: Uint8Array<ArrayBufferLike>;
31
+ mac: Uint8Array<ArrayBufferLike>;
32
+ rev: string;
33
+ }, {
34
+ ver: 1;
35
+ hash: Uint8Array<ArrayBufferLike>;
36
+ ikm: Uint8Array<ArrayBufferLike>;
37
+ sig: Uint8Array<ArrayBufferLike>;
38
+ mac: Uint8Array<ArrayBufferLike>;
39
+ rev: string;
40
+ }>;
41
+ export type SignedCommit = z.infer<typeof signedCommit>;
42
+ declare const repoIndex: z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodUnknown, Cid<0 | 1, number, number>, unknown>>;
43
+ export type RepoIndex = z.infer<typeof repoIndex>;
44
+ export declare const def: {
45
+ signedCommit: check.Def<SignedCommit>;
46
+ repoIndex: check.Def<RepoIndex>;
47
+ };
48
+ export {};
49
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAA;AAC5C,OAAO,EAAE,KAAK,GAAG,EAAE,KAAK,MAAM,EAAS,MAAM,mBAAmB,CAAA;AAEhE,MAAM,MAAM,WAAW,GAAG,MAAM,CAAA;AAEhC,MAAM,MAAM,UAAU,GAAG;IACvB,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAGD,MAAM,MAAM,MAAM,GAAG,UAAU,GAAG;IAChC,GAAG,EAAE,GAAG,GAAG,IAAI,CAAA;IACf,IAAI,EAAE,GAAG,GAAG,IAAI,CAAA;CACjB,CAAA;AAED,eAAO,MAAM,cAAc,IAAI,CAAA;AAE/B,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAeD,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;EAOhB,CAAA;AACF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAA;AAGvD,QAAA,MAAM,SAAS,2FAAkC,CAAA;AACjD,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAA;AAEjD,eAAO,MAAM,GAAG;IACd,YAAY,EAGP,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC;IAC5B,SAAS,EAGJ,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC;CAC1B,CAAA"}
package/dist/types.js ADDED
@@ -0,0 +1,35 @@
1
+ import { z } from 'zod';
2
+ import { ifCid } from '@atproto/lex-data';
3
+ export const COMMIT_VERSION = 1;
4
+ const cidSchema = z.unknown().transform((input, ctx) => {
5
+ const cid = ifCid(input);
6
+ if (cid)
7
+ return cid;
8
+ ctx.addIssue({
9
+ code: z.ZodIssueCode.custom,
10
+ message: 'Not a valid CID',
11
+ });
12
+ return z.NEVER;
13
+ });
14
+ const bytes = z.instanceof(Uint8Array);
15
+ const signedCommit = z.object({
16
+ ver: z.literal(COMMIT_VERSION),
17
+ hash: bytes,
18
+ ikm: bytes,
19
+ sig: bytes,
20
+ mac: bytes,
21
+ rev: z.string(),
22
+ });
23
+ // `{collection}/{rkey}` to the record's cid.
24
+ const repoIndex = z.record(z.string(), cidSchema);
25
+ export const def = {
26
+ signedCommit: {
27
+ name: 'signed commit',
28
+ schema: signedCommit,
29
+ },
30
+ repoIndex: {
31
+ name: 'repo index',
32
+ schema: repoIndex,
33
+ },
34
+ };
35
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,OAAO,EAAyB,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAehE,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAA;AAQ/B,MAAM,SAAS,GAAG,CAAC,CAAC,OAAO,EAAE,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,GAAG,EAAO,EAAE;IAC1D,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,CAAA;IACxB,IAAI,GAAG;QAAE,OAAO,GAAG,CAAA;IAEnB,GAAG,CAAC,QAAQ,CAAC;QACX,IAAI,EAAE,CAAC,CAAC,YAAY,CAAC,MAAM;QAC3B,OAAO,EAAE,iBAAiB;KAC3B,CAAC,CAAA;IACF,OAAO,CAAC,CAAC,KAAK,CAAA;AAChB,CAAC,CAAC,CAAA;AAEF,MAAM,KAAK,GAAG,CAAC,CAAC,UAAU,CAAC,UAA2B,CAAC,CAAA;AAEvD,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5B,GAAG,EAAE,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC;IAC9B,IAAI,EAAE,KAAK;IACX,GAAG,EAAE,KAAK;IACV,GAAG,EAAE,KAAK;IACV,GAAG,EAAE,KAAK;IACV,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;CAChB,CAAC,CAAA;AAGF,6CAA6C;AAC7C,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,SAAS,CAAC,CAAA;AAGjD,MAAM,CAAC,MAAM,GAAG,GAAG;IACjB,YAAY,EAAE;QACZ,IAAI,EAAE,eAAe;QACrB,MAAM,EAAE,YAAY;KACM;IAC5B,SAAS,EAAE;QACT,IAAI,EAAE,YAAY;QAClB,MAAM,EAAE,SAAS;KACM;CAC1B,CAAA","sourcesContent":["import { z } from 'zod'\nimport type { check } from '@atproto/common'\nimport { type Cid, type LexMap, ifCid } from '@atproto/lex-data'\n\nexport type SpaceRecord = LexMap\n\nexport type RecordPath = {\n collection: string\n rkey: string\n}\n\n// A create has no `prev`, a delete no `cid`, an update both.\nexport type RepoOp = RecordPath & {\n cid: Cid | null\n prev: Cid | null\n}\n\nexport const COMMIT_VERSION = 1\n\nexport type CommitCtx = {\n space: string\n author: string\n rev: string\n}\n\nconst cidSchema = z.unknown().transform((input, ctx): Cid => {\n const cid = ifCid(input)\n if (cid) return cid\n\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: 'Not a valid CID',\n })\n return z.NEVER\n})\n\nconst bytes = z.instanceof(Uint8Array<ArrayBufferLike>)\n\nconst signedCommit = z.object({\n ver: z.literal(COMMIT_VERSION),\n hash: bytes,\n ikm: bytes,\n sig: bytes,\n mac: bytes,\n rev: z.string(),\n})\nexport type SignedCommit = z.infer<typeof signedCommit>\n\n// `{collection}/{rkey}` to the record's cid.\nconst repoIndex = z.record(z.string(), cidSchema)\nexport type RepoIndex = z.infer<typeof repoIndex>\n\nexport const def = {\n signedCommit: {\n name: 'signed commit',\n schema: signedCommit,\n } as check.Def<SignedCommit>,\n repoIndex: {\n name: 'repo index',\n schema: repoIndex,\n } as check.Def<RepoIndex>,\n}\n"]}
package/dist/util.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { Cid } from '@atproto/lex-data';
2
+ import type { RecordPath } from './types.js';
3
+ export declare const formatRecordPath: (collection: string, rkey: string) => string;
4
+ export declare const parseRecordPath: (path: string) => RecordPath;
5
+ export declare const formatSetHashElement: (collection: string, rkey: string, cid: Cid) => string;
6
+ //# sourceMappingURL=util.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAA;AAC5C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C,eAAO,MAAM,gBAAgB,eAAgB,MAAM,QAAQ,MAAM,KAAG,MAEnE,CAAA;AAED,eAAO,MAAM,eAAe,SAAU,MAAM,KAAG,UAI9C,CAAA;AAGD,eAAO,MAAM,oBAAoB,eACnB,MAAM,QACZ,MAAM,OACP,GAAG,KACP,MAEF,CAAA"}
package/dist/util.js ADDED
@@ -0,0 +1,14 @@
1
+ export const formatRecordPath = (collection, rkey) => {
2
+ return collection + '/' + rkey;
3
+ };
4
+ export const parseRecordPath = (path) => {
5
+ const { length, 0: collection, 1: rkey } = path.split('/');
6
+ if (length !== 2)
7
+ throw new Error(`invalid record path: ${path}`);
8
+ return { collection, rkey };
9
+ };
10
+ // The element a record contributes to a repo's set hash.
11
+ export const formatSetHashElement = (collection, rkey, cid) => {
12
+ return `${collection}/${rkey}/${cid.toString()}`;
13
+ };
14
+ //# sourceMappingURL=util.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,UAAkB,EAAE,IAAY,EAAU,EAAE;IAC3E,OAAO,UAAU,GAAG,GAAG,GAAG,IAAI,CAAA;AAChC,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,IAAY,EAAc,EAAE;IAC1D,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC1D,IAAI,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,IAAI,EAAE,CAAC,CAAA;IACjE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,CAAA;AAC7B,CAAC,CAAA;AAED,yDAAyD;AACzD,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,UAAkB,EAClB,IAAY,EACZ,GAAQ,EACA,EAAE;IACV,OAAO,GAAG,UAAU,IAAI,IAAI,IAAI,GAAG,CAAC,QAAQ,EAAE,EAAE,CAAA;AAClD,CAAC,CAAA","sourcesContent":["import type { Cid } from '@atproto/lex-data'\nimport type { RecordPath } from './types.js'\n\nexport const formatRecordPath = (collection: string, rkey: string): string => {\n return collection + '/' + rkey\n}\n\nexport const parseRecordPath = (path: string): RecordPath => {\n const { length, 0: collection, 1: rkey } = path.split('/')\n if (length !== 2) throw new Error(`invalid record path: ${path}`)\n return { collection, rkey }\n}\n\n// The element a record contributes to a repo's set hash.\nexport const formatSetHashElement = (\n collection: string,\n rkey: string,\n cid: Cid,\n): string => {\n return `${collection}/${rkey}/${cid.toString()}`\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@atproto/space",
3
+ "version": "0.0.0-spaces-alpha-20260818022935",
4
+ "license": "MIT",
5
+ "description": "atproto permissioned data protocol",
6
+ "keywords": [
7
+ "atproto",
8
+ "space"
9
+ ],
10
+ "engines": {
11
+ "node": ">=22"
12
+ },
13
+ "homepage": "https://atproto.com",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/bluesky-social/atproto",
17
+ "directory": "packages/space"
18
+ },
19
+ "dependencies": {
20
+ "@noble/hashes": "^1.6.1",
21
+ "jose": "^5.2.0",
22
+ "zod": "^3.23.8",
23
+ "@atproto/common": "^0.0.0-spaces-alpha-20260818022935",
24
+ "@atproto/jwk": "^0.7.4",
25
+ "@atproto/crypto": "^0.0.0-spaces-alpha-20260818022935",
26
+ "@atproto/lex-data": "^0.1.7",
27
+ "@atproto/lex-cbor": "^0.1.6"
28
+ },
29
+ "devDependencies": {
30
+ "vitest": "^4.1.10",
31
+ "@atproto/jwk-jose": "^0.2.4"
32
+ },
33
+ "type": "module",
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.ts",
37
+ "default": "./dist/index.js"
38
+ }
39
+ },
40
+ "scripts": {
41
+ "test": "vitest run",
42
+ "build": "tsc --build tsconfig.build.json"
43
+ }
44
+ }