···11+---
22+'@atcute/car': minor
33+---
44+55+add `writeCarStream` for writing CAR archives
+5
.changeset/calm-moons-glow.md
···11+---
22+'@atcute/lexicon-resolver': patch
33+---
44+55+use `@atcute/repo` to verify records
+13
.changeset/ninety-beers-lie.md
···11+---
22+'@atcute/car': major
33+---
44+55+remove AT Protocol repository reader
66+77+the addition of `@atcute/mst` into the atcute family of packages has put `@atcute/car` in a weird
88+spot, we can't have `@atcute/car` depend on `@atcute/mst` because we've already done the reverse to
99+test `@atcute/mst`'s functionalities.
1010+1111+if you need this functionality back, use `@atcute/repo`.
1212+1313+this does mean that we are skipping the stable release of v4, and straight into v5.
+1-1
README.md
···5959| [`identity-resolver`](./packages/identity/identity-resolver): handle and DID document resolution |
6060| [`identity-resolver-node`](./packages/identity/identity-resolver-node): additional identity resolvers for Node.js |
6161| **Utility packages** |
6262-| [`car`](./packages/utilities/car): DASL CAR and atproto repository decoder |
6262+| [`car`](./packages/utilities/car): DASL CAR codec |
6363| [`cbor`](./packages/utilities/cbor): DASL dCBOR42 codec |
6464| [`cid`](./packages/utilities/cid): DASL CID codec |
6565| [`crypto`](./packages/utilities/crypto): cryptographic utilities |
···11-import * as CBOR from '@atcute/cbor';
22-import * as CID from '@atcute/cid';
11+import { CidLinkWrapper, type Cid, type CidLink } from '@atcute/cid';
3243export interface CarV1Header {
54 version: 1;
66- roots: CID.CidLink[];
55+ roots: CidLink[];
76}
8798export const isCarV1Header = (value: unknown): value is CarV1Header => {
···1211 }
13121413 const { version, roots } = value as CarV1Header;
1515- return version === 1 && Array.isArray(roots) && roots.every((root) => root instanceof CBOR.CidLinkWrapper);
1414+ return version === 1 && Array.isArray(roots) && roots.every((root) => root instanceof CidLinkWrapper);
1615};
17161817export interface CarHeader {
···2827 entryStart: number;
2928 entryEnd: number;
30293131- cid: CID.Cid;
3030+ cid: Cid;
3231 cidStart: number;
3332 cidEnd: number;
3433···3635 bytesStart: number;
3736 bytesEnd: number;
3837}
3838+3939+/**
4040+ * represents a block to be written to a CAR file
4141+ */
4242+export interface CarBlock {
4343+ /** the CID of the block (as bytes) */
4444+ cid: Uint8Array;
4545+ /** the block data */
4646+ data: Uint8Array;
4747+}
+6-2
packages/utilities/car/lib/index.ts
···11-export * as CarReader from './car-reader/index.js';
22-export * as RepoReader from './repo-reader/index.js';
11+export * from './reader.js';
22+export * from './streamed-reader.js';
33+44+export * from './writer.js';
55+66+export * from './types.js';
···11-import { describe, expect, it } from 'bun:test';
11+import { describe, expect, it } from 'vitest';
2233import { fromCidLink, toString } from '@atcute/cid';
44import { fromBase64 } from '@atcute/multibase';
5566-import { fromStream, repoEntryTransform } from './stream-repo-reader.js';
77-import { fromUint8Array } from './sync-repo-reader.js';
66+import { fromStream, fromUint8Array, repoEntryTransform } from './index.js';
8798describe('fromUint8Array', () => {
109 it('decodes atproto car files', () => {
-6
packages/utilities/car/lib/repo-reader/index.ts
···11-export * from './types.js';
22-33-export * from './mst.js';
44-export * from './stream-repo-reader.js';
55-export * from './sync-blockmap.js';
66-export * from './sync-repo-reader.js';
-110
packages/utilities/car/lib/repo-reader/mst.ts
···11-import * as CBOR from '@atcute/cbor';
22-import * as CID from '@atcute/cid';
33-44-const isCidLink = (value: unknown): value is CID.CidLink => {
55- if (value instanceof CID.CidLinkWrapper) {
66- return true;
77- }
88-99- if (value === null || typeof value !== 'object') {
1010- return false;
1111- }
1212-1313- return '$link' in value && typeof value.$link === 'string';
1414-};
1515-1616-const isBytes = (value: unknown): value is CBOR.Bytes => {
1717- if (value instanceof CBOR.BytesWrapper) {
1818- return true;
1919- }
2020-2121- if (value === null || typeof value !== 'object') {
2222- return false;
2323- }
2424-2525- return '$bytes' in value && typeof value.$bytes === 'string';
2626-};
2727-2828-/** commit object */
2929-export interface Commit {
3030- version: 3;
3131- did: string;
3232- data: CID.CidLink;
3333- rev: string;
3434- prev: CID.CidLink | null;
3535- sig: CBOR.Bytes;
3636-}
3737-3838-/**
3939- * checks if a value is a valid commit object
4040- * @param value the value to check
4141- * @returns true if the value is a valid commit object, false otherwise
4242- */
4343-export const isCommit = (value: unknown): value is Commit => {
4444- if (value === null || typeof value !== 'object') {
4545- return false;
4646- }
4747-4848- const obj = value as Record<string, unknown>;
4949-5050- return (
5151- obj.version === 3 &&
5252- typeof obj.did === 'string' &&
5353- isCidLink(obj.data) &&
5454- typeof obj.rev === 'string' &&
5555- (obj.prev === null || isCidLink(obj.prev)) &&
5656- isBytes(obj.sig)
5757- );
5858-};
5959-6060-/** mst tree entry object */
6161-export interface TreeEntry {
6262- /** count of bytes shared with previous TreeEntry in this Node (if any) */
6363- p: number;
6464- /** remainder of key for this TreeEntry, after "prefixlen" have been removed */
6565- k: CBOR.Bytes;
6666- /** link to a sub-tree Node at a lower level which has keys sorting after this TreeEntry's key (to the "right"), but before the next TreeEntry's key in this Node (if any) */
6767- v: CID.CidLink;
6868- /** next subtree (to the right of leaf) */
6969- t: CID.CidLink | null;
7070-}
7171-7272-/**
7373- * checks if a value is a valid mst tree entry object
7474- * @param value the value to check
7575- * @returns true if the value is a valid mst tree entry object, false otherwise
7676- */
7777-export const isTreeEntry = (value: unknown): value is TreeEntry => {
7878- if (value === null || typeof value !== 'object') {
7979- return false;
8080- }
8181-8282- const obj = value as Record<string, unknown>;
8383-8484- return (
8585- typeof obj.p === 'number' && isBytes(obj.k) && isCidLink(obj.v) && (obj.t === null || isCidLink(obj.t))
8686- );
8787-};
8888-8989-/** mst node object */
9090-export interface MstNode {
9191- /** link to sub-tree Node on a lower level and with all keys sorting before keys at this node */
9292- l: CID.CidLink | null;
9393- /** ordered list of TreeEntry objects */
9494- e: TreeEntry[];
9595-}
9696-9797-/**
9898- * checks if a value is a valid mst node object
9999- * @param value the value to check
100100- * @returns true if the value is a valid mst node object, false otherwise
101101- */
102102-export const isMstNode = (value: unknown): value is MstNode => {
103103- if (value === null || typeof value !== 'object') {
104104- return false;
105105- }
106106-107107- const obj = value as Record<string, unknown>;
108108-109109- return (obj.l === null || isCidLink(obj.l)) && Array.isArray(obj.e) && obj.e.every(isTreeEntry);
110110-};
···11-import * as CBOR from '@atcute/cbor';
22-import * as CID from '@atcute/cid';
33-import { decodeUtf8From } from '@atcute/uint8array';
44-55-import * as CarReader from '../car-reader/index.js';
66-import { assert } from '../utils.js';
77-88-import { isMstNode } from './mst.js';
99-1010-export type BlockMap = Map<string, CarReader.CarEntry>;
1111-1212-/**
1313- * collects entries from a CAR archive into a mapping of CID string -> actual bytes
1414- * @param iterator a generator that yields objects with a `cid` and `bytes` property
1515- * @returns a mapping of CID string -> actual bytes
1616- */
1717-export const collectBlock = (iterator: Iterable<CarReader.CarEntry>): BlockMap => {
1818- const blockmap: BlockMap = new Map();
1919- for (const entry of iterator) {
2020- blockmap.set(CID.toString(entry.cid), entry);
2121- }
2222-2323- return blockmap;
2424-};
2525-2626-/**
2727- * reads a block from the blockmap and validates it against the provided validation function
2828- * @param map a mapping of CID string -> actual bytes
2929- * @param link a CID link to read
3030- * @param validate a validation function to validate the decoded data
3131- * @returns the decoded and validated data
3232- */
3333-export const readBlock = <T>(
3434- map: BlockMap,
3535- link: CID.CidLink,
3636- validate: (value: unknown) => value is T,
3737-): T => {
3838- const cid = link.$link;
3939-4040- const entry = map.get(cid);
4141- assert(entry != null, `cid not found in blockmap; cid=${cid}`);
4242-4343- const data = CBOR.decode(entry.bytes);
4444- assert(validate(data), `validation failed for cid=${cid}`);
4545-4646- return data;
4747-};
4848-4949-/** node entry object */
5050-export interface NodeEntry {
5151- key: string;
5252- cid: CID.CidLink;
5353-}
5454-5555-/**
5656- * walks the entries of a Merkle Sorted Tree (MST) in a depth-first manner
5757- * @param map a mapping of CID string -> actual bytes
5858- * @param pointer a CID link to the root of the MST
5959- * @returns a generator that yields the entries of the MST
6060- */
6161-export function* walkMstEntries(map: BlockMap, pointer: CID.CidLink): Generator<NodeEntry> {
6262- const data = readBlock(map, pointer, isMstNode);
6363- const entries = data.e;
6464-6565- let lastKey = '';
6666-6767- if (data.l !== null) {
6868- yield* walkMstEntries(map, data.l);
6969- }
7070-7171- for (let i = 0, il = entries.length; i < il; i++) {
7272- const entry = entries[i];
7373-7474- const key_str = decodeUtf8From(CBOR.fromBytes(entry.k));
7575- const key = lastKey.slice(0, entry.p) + key_str;
7676-7777- lastKey = key;
7878-7979- yield { key: key, cid: entry.v };
8080-8181- if (entry.t !== null) {
8282- yield* walkMstEntries(map, entry.t);
8383- }
8484- }
8585-}
···11-import * as CarReader from '../car-reader/index.js';
22-import { assert } from '../utils.js';
33-44-import { isCommit } from './mst.js';
55-import { collectBlock, readBlock, walkMstEntries } from './sync-blockmap.js';
66-import { RepoEntry } from './types.js';
77-88-export function* fromUint8Array(buf: Uint8Array): Generator<RepoEntry> {
99- const car = CarReader.fromUint8Array(buf);
1010- const roots = car.roots;
1111-1212- assert(roots.length === 1, `expected only 1 root in the car archive; got=${roots.length}`);
1313-1414- const blockmap = collectBlock(car);
1515- assert(blockmap.size > 0, `expected at least 1 block in the archive; got=${blockmap.size}`);
1616-1717- const commit = readBlock(blockmap, roots[0], isCommit);
1818-1919- for (const { key, cid } of walkMstEntries(blockmap, commit.data)) {
2020- const [collection, rkey] = key.split('/');
2121-2222- const carEntry = blockmap.get(cid.$link);
2323- assert(carEntry != null, `cid not found in blockmap; cid=${cid}`);
2424-2525- yield new RepoEntry(collection, rkey, cid, carEntry);
2626- }
2727-}
-32
packages/utilities/car/lib/repo-reader/types.ts
···11-import * as CBOR from '@atcute/cbor';
22-import * as CID from '@atcute/cid';
33-44-import { type CarEntry } from '../car-reader/index.js';
55-66-export class RepoEntry {
77- /** @internal */
88- constructor(
99- /** the collection this record belongs to */
1010- public readonly collection: string,
1111- /** record key */
1212- public readonly rkey: string,
1313- /** CID of this record */
1414- public readonly cid: CID.CidLink,
1515- /** the associated CarEntry for this record */
1616- public readonly carEntry: CarEntry,
1717- ) {}
1818-1919- /**
2020- * raw contents of this record
2121- */
2222- get bytes(): Uint8Array {
2323- return this.carEntry.bytes;
2424- }
2525-2626- /**
2727- * decoded contents of this record
2828- */
2929- get record(): unknown {
3030- return CBOR.decode(this.bytes);
3131- }
3232-}
···11-import * as CBOR from '@atcute/cbor';
22-import * as CID from '@atcute/cid';
33-44-type BlockEntry = [cid: string, bytes: Uint8Array<ArrayBuffer>];
55-61/** a map from CID strings to their encoded block data */
72export type BlockMap = Map<string, Uint8Array<ArrayBuffer>>;
88-99-/**
1010- * encodes data as CBOR, computes its CID, and adds it to the map
1111- * @param map the block map to add to
1212- * @param data the data to encode and add
1313- */
1414-export const add = async (map: BlockMap, data: unknown): Promise<void> => {
1515- const encoded = CBOR.encode(data);
1616- const cid = await CID.create(0x71, encoded);
1717-1818- map.set(CID.toString(cid), encoded);
1919-};
2020-2121-/**
2222- * copies multiple blocks from an iterable into the map
2323- * @param map the block map to add to
2424- * @param entries the block entries to add
2525- */
2626-export const setMany = (map: BlockMap, entries: Iterable<Readonly<BlockEntry>>) => {
2727- for (const [cid, bytes] of entries) {
2828- map.set(cid, bytes);
2929- }
3030-};
3131-3232-/**
3333- * removes multiple blocks from the map by their CIDs
3434- * @param map the block map to remove from
3535- * @param cids the CID strings to remove
3636- */
3737-export const deleteMany = (map: BlockMap, cids: Iterable<string>) => {
3838- for (const cid of cids) {
3939- map.delete(cid);
4040- }
4141-};
···33import { allocUnsafe } from '@atcute/uint8array';
44import * as varint from '@atcute/varint';
5566+import type { CarBlock } from './types.js';
77+68/**
77- * Encodes a number as an unsigned varint (variable-length integer)
99+ * encodes a number as an unsigned varint (variable-length integer)
810 * @param n the number to encode
911 * @returns the varint-encoded bytes
1012 */
···1618};
17191820/**
1919- * Serializes a CAR v1 header
2121+ * serializes a CAR v1 header
2222+ * @internal
2023 * @param roots array of root CIDs (typically just one)
2124 * @returns the serialized header bytes
2225 */
···3639};
37403841/**
3939- * Serializes a single CAR entry (block)
4242+ * serializes a single CAR entry (block)
4343+ * @internal
4044 * @param cid the CID of the block (as bytes)
4145 * @param data the block data
4246 * @returns the serialized entry bytes
···5357};
54585559/**
5656- * Represents a block to be written to a CAR file
5757- */
5858-export interface CarBlock {
5959- /** the CID of the block (as bytes) */
6060- cid: Uint8Array;
6161- /** the block data */
6262- data: Uint8Array;
6363-}
6464-6565-/**
6666- * Creates an async generator that yields CAR file chunks
6767- * @param root the root CID for the CAR file
6060+ * creates an async generator that yields CAR file chunks
6161+ * @param root root CIDs for the CAR file
6862 * @param blocks async iterable of blocks to write
6963 * @yields Uint8Array chunks of the CAR file (header, then entries)
7064 *
···7670 * };
7771 *
7872 * // Stream chunks
7979- * for await (const chunk of createCarStream(rootCid, blocks())) {
7373+ * for await (const chunk of writeCarStream([rootCid], blocks())) {
8074 * stream.write(chunk);
8175 * }
8276 *
8377 * // Or collect into array (requires Array.fromAsync or polyfill)
8484- * const chunks = await Array.fromAsync(createCarStream(rootCid, blocks()));
7878+ * const chunks = await Array.fromAsync(writeCarStream([rootCid], blocks()));
8579 * ```
8680 */
8787-export async function* createCarStream(
8888- root: CidLink,
8181+export async function* writeCarStream(
8282+ roots: CidLink[],
8983 blocks: AsyncIterable<CarBlock> | Iterable<CarBlock>,
9084): AsyncGenerator<Uint8Array<ArrayBuffer>> {
9185 // Emit header first
9292- yield serializeCarHeader([root]);
8686+ yield serializeCarHeader(roots);
93879488 // Then emit each block entry
9589 for await (const block of blocks) {
+6-5
packages/utilities/mst/lib/diff.ts
···11import type { CidLink } from '@atcute/cid';
2233-import { MSTNode } from './node.js';
43import type { NodeStore } from './node-store.js';
54import { NodeWalker } from './node-walker.js';
55+import { MSTNode } from './node.js';
6677/**
88 * Type of change to a record
···164164 * @param rootB CID of second MST root
165165 * @returns tuple of [created nodes, deleted nodes]
166166 */
167167-export const mstDiff = async (ns: NodeStore, rootA: string, rootB: string): Promise<[Set<string>, Set<string>]> => {
167167+export const mstDiff = async (
168168+ ns: NodeStore,
169169+ rootA: string,
170170+ rootB: string,
171171+): Promise<[Set<string>, Set<string>]> => {
168172 const created = new Set<string>(); // nodes in B but not in A
169173 const deleted = new Set<string>(); // nodes in A but not in B
170174···265269 }
266270267271 // The rpaths now match, but the subtrees below us might not
268268- const aSubtree = a.subtree;
269269- const bSubtree = b.subtree;
270270-271272 // Recursively diff the subtrees
272273 const aSubWalker = await a.createSubtreeWalker();
273274 const bSubWalker = await b.createSubtreeWalker();
+10
packages/utilities/mst/lib/index.ts
···11+export * from './types.js';
22+export * from './node.js';
33+export * from './node-store.js';
44+export * from './node-wrangler.js';
55+export * from './node-walker.js';
66+export * from './diff.js';
77+export * from './proof.js';
88+export * from './errors.js';
99+export * from './blockmap.js';
1010+export * from './stores.js';
-1
packages/utilities/mst/lib/node-store.ts
···11import { MissingBlockError } from './errors.js';
22import { MSTNode } from './node.js';
33import type { BlockStore } from './stores.js';
44-54import LRUCache from './utils/lru.js';
6576/**
+1-1
packages/utilities/mst/lib/node-wrangler.ts
···11import type { CidLink } from '@atcute/cid';
2233-import { MSTNode, getKeyHeight } from './node.js';
43import { NodeStore } from './node-store.js';
44+import { MSTNode, getKeyHeight } from './node.js';
5566/**
77 * replaces element at index with a new value
+2-1
packages/utilities/mst/lib/stores.ts
···11-import { deleteMany, setMany, type BlockMap } from './blockmap.js';
11+import { type BlockMap } from './blockmap.js';
22+import { deleteMany, setMany } from './utils/blockmap.js';
2334/**
45 * a read-only interface for retrieving blocks by their CID
+3-3
packages/utilities/mst/lib/test-suite.test.ts
···44import * as fs from 'node:fs/promises';
55import * as path from 'node:path';
6677-import { CarReader } from '@atcute/car';
77+import * as CAR from '@atcute/car';
88import * as CID from '@atcute/cid';
991010-import { setMany } from './blockmap.js';
1110import { DeltaType, mstDiff, recordDiff } from './diff.js';
1211import { NodeStore } from './node-store.js';
1312import { NodeWrangler } from './node-wrangler.js';
···1817 OverlayBlockStore,
1918 ReadonlyMemoryBlockStore,
2019} from './stores.js';
2020+import { setMany } from './utils/blockmap.js';
21212222const mstDiffTestCaseSchema = v.object({
2323 $type: v.literal('mst-diff'),
···5252 const filename = path.join(testSuiteRoot, relname);
5353 const bytes = await fs.readFile(filename);
54545555- const car = CarReader.fromUint8Array(bytes);
5555+ const car = CAR.fromUint8Array(bytes);
5656 const store = new MemoryBlockStore();
57575858 for (const entry of car) {
+40
packages/utilities/mst/lib/utils/blockmap.ts
···11+import * as CBOR from '@atcute/cbor';
22+import * as CID from '@atcute/cid';
33+44+import type { BlockMap } from '../blockmap.js';
55+66+type BlockEntry = [cid: string, bytes: Uint8Array<ArrayBuffer>];
77+88+/**
99+ * encodes data as CBOR, computes its CID, and adds it to the map
1010+ * @param map the block map to add to
1111+ * @param data the data to encode and add
1212+ */
1313+export const add = async (map: BlockMap, data: unknown): Promise<void> => {
1414+ const encoded = CBOR.encode(data);
1515+ const cid = await CID.create(0x71, encoded);
1616+1717+ map.set(CID.toString(cid), encoded);
1818+};
1919+2020+/**
2121+ * copies multiple blocks from an iterable into the map
2222+ * @param map the block map to add to
2323+ * @param entries the block entries to add
2424+ */
2525+export const setMany = (map: BlockMap, entries: Iterable<Readonly<BlockEntry>>) => {
2626+ for (const [cid, bytes] of entries) {
2727+ map.set(cid, bytes);
2828+ }
2929+};
3030+3131+/**
3232+ * removes multiple blocks from the map by their CIDs
3333+ * @param map the block map to remove from
3434+ * @param cids the CID strings to remove
3535+ */
3636+export const deleteMany = (map: BlockMap, cids: Iterable<string>) => {
3737+ for (const cid of cids) {
3838+ map.delete(cid);
3939+ }
4040+};
···11+export type { RepoEntry } from './types.js';
22+33+export * from './streamed-reader.js';
44+export * from './reader.js';
55+export * from './verify.js';
+98
packages/utilities/repo/lib/reader.ts
···11+import type { CarEntry } from '@atcute/car';
22+import * as CAR from '@atcute/car';
33+import * as CBOR from '@atcute/cbor';
44+import type { CidLink } from '@atcute/cid';
55+import * as CID from '@atcute/cid';
66+import { isNodeData } from '@atcute/mst';
77+import { decodeUtf8From } from '@atcute/uint8array';
88+99+import { isCommit, RepoEntry } from './types.js';
1010+import { assert } from './utils.js';
1111+1212+/** @internal */
1313+type EntryMap = Map<string, CarEntry>;
1414+1515+/** node entry object */
1616+interface NodeEntry {
1717+ key: string;
1818+ cid: CidLink;
1919+}
2020+2121+export function* fromUint8Array(buf: Uint8Array): Generator<RepoEntry> {
2222+ const car = CAR.fromUint8Array(buf);
2323+ const roots = car.roots;
2424+2525+ assert(roots.length === 1, `expected only 1 root in the car archive; got=${roots.length}`);
2626+2727+ const map: EntryMap = new Map();
2828+ for (const entry of car) {
2929+ map.set(CID.toString(entry.cid), entry);
3030+ }
3131+3232+ // [commit, mst node, record?]
3333+ assert(map.size >= 2, `expected at least 2 blocks in the archive; got=${map.size}`);
3434+3535+ const commit = readEntry(map, roots[0], isCommit);
3636+3737+ for (const { key, cid } of walkMstEntries(map, commit.data)) {
3838+ const [collection, rkey] = key.split('/');
3939+4040+ const carEntry = map.get(cid.$link);
4141+ assert(carEntry != null, `cid not found in blockmap; cid=${cid}`);
4242+4343+ yield new RepoEntry(collection, rkey, cid, carEntry);
4444+ }
4545+}
4646+4747+/**
4848+ * reads a block from the blockmap and validates it against the provided validation function
4949+ * @internal
5050+ * @param map a mapping of CID string -> actual bytes
5151+ * @param link a CID link to read
5252+ * @param validate a validation function to validate the decoded data
5353+ * @returns the decoded and validated data
5454+ */
5555+export const readEntry = <T>(map: EntryMap, link: CidLink, validate: (value: unknown) => value is T): T => {
5656+ const cid = link.$link;
5757+5858+ const entry = map.get(cid);
5959+ assert(entry != null, `cid not found in blockmap; cid=${cid}`);
6060+6161+ const data = CBOR.decode(entry.bytes);
6262+ assert(validate(data), `validation failed for cid=${cid}`);
6363+6464+ return data;
6565+};
6666+6767+/**
6868+ * walks the entries of a Merkle Sorted Tree (MST) in a depth-first manner
6969+ * @internal
7070+ * @param map a mapping of CID string -> actual bytes
7171+ * @param pointer a CID link to the root of the MST
7272+ * @returns a generator that yields the entries of the MST
7373+ */
7474+export function* walkMstEntries(map: EntryMap, pointer: CidLink): Generator<NodeEntry> {
7575+ const data = readEntry(map, pointer, isNodeData);
7676+ const entries = data.e;
7777+7878+ let lastKey = '';
7979+8080+ if (data.l !== null) {
8181+ yield* walkMstEntries(map, data.l);
8282+ }
8383+8484+ for (let i = 0, il = entries.length; i < il; i++) {
8585+ const entry = entries[i];
8686+8787+ const key_str = decodeUtf8From(CBOR.fromBytes(entry.k));
8888+ const key = lastKey.slice(0, entry.p) + key_str;
8989+9090+ lastKey = key;
9191+9292+ yield { key: key, cid: entry.v };
9393+9494+ if (entry.t !== null) {
9595+ yield* walkMstEntries(map, entry.t);
9696+ }
9797+ }
9898+}
+64
packages/utilities/repo/lib/types.ts
···11+import type { CarEntry } from '@atcute/car';
22+import * as CBOR from '@atcute/cbor';
33+import { isBytes, type Bytes } from '@atcute/cbor';
44+import { isCidLink, type CidLink } from '@atcute/cid';
55+66+export class RepoEntry {
77+ /** @internal */
88+ constructor(
99+ /** the collection this record belongs to */
1010+ public readonly collection: string,
1111+ /** record key */
1212+ public readonly rkey: string,
1313+ /** CID of this record */
1414+ public readonly cid: CidLink,
1515+ /** the associated CarEntry for this record */
1616+ public readonly carEntry: CarEntry,
1717+ ) {}
1818+1919+ /**
2020+ * raw contents of this record
2121+ */
2222+ get bytes(): Uint8Array {
2323+ return this.carEntry.bytes;
2424+ }
2525+2626+ /**
2727+ * decoded contents of this record
2828+ */
2929+ get record(): unknown {
3030+ return CBOR.decode(this.bytes);
3131+ }
3232+}
3333+3434+/** commit object */
3535+export interface Commit {
3636+ version: 3;
3737+ did: string;
3838+ data: CidLink;
3939+ rev: string;
4040+ prev: CidLink | null;
4141+ sig: Bytes;
4242+}
4343+4444+/**
4545+ * checks if value is a valid commit object
4646+ * @param value value to check
4747+ * @returns true if the value is a valid commit object, false otherwise
4848+ */
4949+export const isCommit = (value: unknown): value is Commit => {
5050+ if (value === null || typeof value !== 'object') {
5151+ return false;
5252+ }
5353+5454+ const obj = value as Record<string, unknown>;
5555+5656+ return (
5757+ obj.version === 3 &&
5858+ typeof obj.did === 'string' &&
5959+ isCidLink(obj.data) &&
6060+ typeof obj.rev === 'string' &&
6161+ (obj.prev === null || isCidLink(obj.prev)) &&
6262+ isBytes(obj.sig)
6363+ );
6464+};