@misofm/platform 0.19.0 → 0.21.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.
Files changed (65) hide show
  1. package/README.md +15 -15
  2. package/dist/client.d.ts +1 -1
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/contracts.d.ts +1 -0
  5. package/dist/contracts.d.ts.map +1 -1
  6. package/dist/contracts.js +1 -0
  7. package/dist/contracts.js.map +1 -1
  8. package/dist/cover.d.ts +5 -8
  9. package/dist/cover.d.ts.map +1 -1
  10. package/dist/cover.js +5 -17
  11. package/dist/cover.js.map +1 -1
  12. package/dist/deployments.d.ts +7 -3
  13. package/dist/deployments.d.ts.map +1 -1
  14. package/dist/deployments.js +3 -1
  15. package/dist/deployments.js.map +1 -1
  16. package/dist/index.d.ts +1 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1 -2
  19. package/dist/index.js.map +1 -1
  20. package/dist/internal.d.ts +11 -0
  21. package/dist/internal.d.ts.map +1 -1
  22. package/dist/internal.js +18 -1
  23. package/dist/internal.js.map +1 -1
  24. package/dist/publication.d.ts +14 -0
  25. package/dist/publication.d.ts.map +1 -1
  26. package/dist/publication.js +17 -5
  27. package/dist/publication.js.map +1 -1
  28. package/dist/read/catalog.d.ts +1 -1
  29. package/dist/read/catalog.d.ts.map +1 -1
  30. package/dist/read/catalog.js +6 -23
  31. package/dist/read/catalog.js.map +1 -1
  32. package/dist/read/config.d.ts +0 -2
  33. package/dist/read/config.d.ts.map +1 -1
  34. package/dist/read/config.js.map +1 -1
  35. package/dist/read/internal/walrus.js +1 -1
  36. package/dist/read/internal/walrus.js.map +1 -1
  37. package/dist/read/types.d.ts +0 -2
  38. package/dist/read/types.d.ts.map +1 -1
  39. package/dist/recording-extensions.d.ts +49 -3
  40. package/dist/recording-extensions.d.ts.map +1 -1
  41. package/dist/recording-extensions.js +126 -10
  42. package/dist/recording-extensions.js.map +1 -1
  43. package/dist/walrus-ids.d.ts +11 -0
  44. package/dist/walrus-ids.d.ts.map +1 -0
  45. package/dist/walrus-ids.js +96 -0
  46. package/dist/walrus-ids.js.map +1 -0
  47. package/package.json +2 -8
  48. package/src/client.ts +1 -1
  49. package/src/contracts.ts +1 -0
  50. package/src/cover.ts +21 -41
  51. package/src/deployments.ts +9 -4
  52. package/src/index.ts +1 -2
  53. package/src/internal.ts +25 -0
  54. package/src/publication.ts +36 -4
  55. package/src/read/catalog.ts +4 -29
  56. package/src/read/config.ts +0 -2
  57. package/src/read/internal/walrus.ts +1 -1
  58. package/src/read/types.ts +0 -2
  59. package/src/recording-extensions.ts +177 -18
  60. package/src/walrus-ids.ts +102 -0
  61. package/dist/mix.d.ts +0 -144
  62. package/dist/mix.d.ts.map +0 -1
  63. package/dist/mix.js +0 -656
  64. package/dist/mix.js.map +0 -1
  65. package/src/mix.ts +0 -910
@@ -48,11 +48,13 @@ import {
48
48
  } from "./credits.ts";
49
49
  import {
50
50
  setRecordingAdvisory,
51
+ setRecordingEngineSession,
51
52
  setRecordingInstrumental,
52
53
  setRecordingLanguages,
53
54
  setRecordingMasterReference,
54
55
  setRecordingStreamingTranscode,
55
56
  } from "./recording-extensions.ts";
57
+ import { unencryptedWalrusBlob } from "./internal.ts";
56
58
  import {
57
59
  setReleaseDescription,
58
60
  setReleaseDspLinks,
@@ -148,6 +150,20 @@ type PublicationRecordingParent =
148
150
  | { readonly parentCompositionIndex: number; readonly parentCompositionId?: never }
149
151
  | { readonly parentCompositionIndex?: never; readonly parentCompositionId: string };
150
152
 
153
+ /** One stem a Session V1 document references, paired with its Walrus blob. */
154
+ export interface PublicationEngineSessionStem {
155
+ /** 32-byte SHA-256 of the stem's canonical PCM (engine STEM_IDENTITY_V1); bytes or 64 hex chars. */
156
+ readonly digest: Uint8Array | string;
157
+ /** Unencrypted Walrus blob holding the stem's FLAC, as its on-chain `u256`. */
158
+ readonly blobId: bigint | string;
159
+ }
160
+
161
+ export interface PublicationEngineSession {
162
+ /** Unencrypted Walrus blob holding the canonical Session V1 JSON, as its on-chain `u256`. */
163
+ readonly sessionBlobId: bigint | string;
164
+ readonly stems: readonly PublicationEngineSessionStem[];
165
+ }
166
+
151
167
  export type PublicationRecordingLanguages =
152
168
  | { readonly kind: "instrumental" }
153
169
  | { readonly kind: "languages"; readonly codes: string[] };
@@ -171,6 +187,8 @@ export type PublicationRecording = PublicationRecordingParent & {
171
187
  readonly masterReferenceBlobId?: bigint | string;
172
188
  /** Complete Walrus Quilt ID containing this Recording's streaming transcodes. */
173
189
  readonly streamingTranscodeQuiltId?: bigint | string;
190
+ /** This Recording's Miso Engine session: the Session V1 blob and every stem it plays. */
191
+ readonly engineSession?: PublicationEngineSession;
174
192
  };
175
193
 
176
194
  export interface PublicationFreshTrack {
@@ -332,10 +350,7 @@ function languageVector(tx: Transaction, p: AtomicPublicationParams, codes: stri
332
350
  }
333
351
 
334
352
  function walrusBlob(tx: Transaction, p: AtomicPublicationParams, blobId: bigint | string) {
335
- return tx.moveCall({
336
- target: `${p.deployment.packages.ori}::walrus_data::new_blob`,
337
- arguments: [tx.pure.u256(blobId)],
338
- });
353
+ return unencryptedWalrusBlob(tx, p.deployment.packages.ori, blobId);
339
354
  }
340
355
 
341
356
  function listingPrice(tx: Transaction, p: AtomicPublicationParams, price: ListingPrice) {
@@ -652,6 +667,14 @@ export function publishAtomicCatalog(p: AtomicPublicationParams): TxThunk {
652
667
  "A recording streaming transcode requires deployment.packages.recordingStreamingTranscode",
653
668
  );
654
669
  }
670
+ if (
671
+ p.recordings.some((node) => node.engineSession !== undefined) &&
672
+ !p.deployment.packages.recordingEngineSession
673
+ ) {
674
+ throw new Error(
675
+ "A recording engine session requires deployment.packages.recordingEngineSession",
676
+ );
677
+ }
655
678
  if (publicationUsesVault(p)) {
656
679
  requireOperationsDeployment(p.deployment.operations);
657
680
  }
@@ -869,6 +892,15 @@ export function publishAtomicCatalog(p: AtomicPublicationParams): TxThunk {
869
892
  oriPackageId: p.deployment.packages.ori,
870
893
  quiltId: node.streamingTranscodeQuiltId,
871
894
  })(tx);
895
+ if (node.engineSession !== undefined) setRecordingEngineSession({
896
+ recordingId: parts.work, authority,
897
+ recordingShareType: node.shareType,
898
+ compositionShareType: node.compositionShareType,
899
+ recordingEngineSessionPackageId: p.deployment.packages.recordingEngineSession!,
900
+ oriPackageId: p.deployment.packages.ori,
901
+ sessionBlobId: node.engineSession.sessionBlobId,
902
+ stems: node.engineSession.stems,
903
+ })(tx);
872
904
  });
873
905
 
874
906
  if (p.release && releaseObject && releaseAdminCap) {
@@ -38,10 +38,6 @@ import {
38
38
  releaseKindFieldId,
39
39
  } from "../release-extensions.ts";
40
40
  import { getRecordingMasterReferencesByIds } from "../recording-extensions.ts";
41
- import {
42
- getReleaseMixReferences,
43
- walrusBlobIdFromU256,
44
- } from "../mix.ts";
45
41
  import { getTrackCreditsByRecordingIds } from "../catalog.ts";
46
42
  import { getReleaseById, getReleasesByIds, isNotFound } from "@misofm/protocol";
47
43
  import type { Release } from "@misofm/protocol";
@@ -49,7 +45,6 @@ import type { MisoClient } from "./client.ts";
49
45
  import { getRecordingTitles, parseReleaseObject } from "./works.ts";
50
46
  import { int } from "./internal/scalars.ts";
51
47
  import {
52
- quiltPatchId,
53
48
  u256ToB64Url,
54
49
  walrusBlobReadUrl,
55
50
  } from "./internal/walrus.ts";
@@ -75,17 +70,9 @@ import type {
75
70
 
76
71
  // ── Walrus URLs ──────────────────────────────────────────────────────────────
77
72
 
78
- /** Aggregator URL for a cover image ref, whichever Walrus variant it is. */
73
+ /** Aggregator URL for a cover image ref (a standalone Walrus blob). */
79
74
  function imageUrl(aggregator: string, ref: CoverImageRef): string {
80
- const base = aggregator.replace(/\/$/, "");
81
- if (ref.kind === "blob") return walrusBlobReadUrl(base, ref.blobId);
82
- const patch = quiltPatchId(
83
- ref.quiltId,
84
- ref.version,
85
- ref.startIndex,
86
- ref.endIndex,
87
- );
88
- return `${base}/v1/blobs/by-quilt-patch-id/${patch}`;
75
+ return walrusBlobReadUrl(aggregator.replace(/\/$/, ""), ref.blobId);
89
76
  }
90
77
 
91
78
  function toCover(
@@ -212,11 +199,9 @@ export function toTracks(
212
199
  release: Release,
213
200
  titles: Record<string, string>,
214
201
  masterBlobIds: Partial<Record<string, string>>,
215
- mixBlobIds: Partial<Record<number, string>> = {},
216
202
  ): TrackView[] {
217
203
  return release.tracks.map((track, index) => {
218
204
  const masterBlobId = masterBlobIds[track.recordingId];
219
- const mixBlobId = mixBlobIds[index];
220
205
  return {
221
206
  no: `${index + 1}`,
222
207
  title: titles[track.recordingId] ?? "Untitled",
@@ -225,7 +210,6 @@ export function toTracks(
225
210
  splitBps: int(track.splitBps.value),
226
211
  disc: 1,
227
212
  ...(masterBlobId ? { masterBlobId } : {}),
228
- ...(mixBlobId ? { mixBlobId } : {}),
229
213
  };
230
214
  });
231
215
  }
@@ -373,8 +357,7 @@ export async function getReleaseDetail(
373
357
  );
374
358
 
375
359
  const recordingIds = release.tracks.map((track) => track.recordingId);
376
- const mixPackageId = client.config.protocol.releaseMixReference;
377
- const [titles, masterReferences, mixReferences, trackCredits] = await Promise.all([
360
+ const [titles, masterReferences, trackCredits] = await Promise.all([
378
361
  getRecordingTitles(
379
362
  client.protocol,
380
363
  client.graphql,
@@ -386,9 +369,6 @@ export async function getReleaseDetail(
386
369
  recordingIds,
387
370
  client.config.protocol.recordingMasterReference,
388
371
  ).catch(() => ({})),
389
- mixPackageId
390
- ? getReleaseMixReferences(client.protocol, releaseId, mixPackageId).catch(() => null)
391
- : Promise.resolve(null),
392
372
  options.include?.includes("trackCredits")
393
373
  ? getTrackCreditsForRecordingIds(client, recordingIds)
394
374
  : Promise.resolve(undefined),
@@ -397,11 +377,6 @@ export async function getReleaseDetail(
397
377
  for (const [recordingId, blobId] of Object.entries(masterReferences)) {
398
378
  if (blobId) masterBlobIds[recordingId] = u256ToB64Url(blobId);
399
379
  }
400
- const mixBlobIds: Record<number, string> = {};
401
- for (let index = 0; index < (mixReferences?.length ?? 0); index += 1) {
402
- const reference = mixReferences?.[index];
403
- if (reference) mixBlobIds[index] = walrusBlobIdFromU256(reference.blobId);
404
- }
405
380
 
406
381
  const creditViews = credits ?? [];
407
382
  return {
@@ -416,7 +391,7 @@ export async function getReleaseDetail(
416
391
  credits: creditViews,
417
392
  primaryArtists: primaryArtistNames(creditViews),
418
393
  discCount: release.tracks.length > 0 ? 1 : 0,
419
- tracks: toTracks(release, titles, masterBlobIds, mixBlobIds),
394
+ tracks: toTracks(release, titles, masterBlobIds),
420
395
  ...(trackCredits !== undefined ? { trackCredits } : {}),
421
396
  };
422
397
  }
@@ -33,8 +33,6 @@ export interface ProtocolIds {
33
33
  releaseKind: string;
34
34
  /** `recording_master_reference` — optional Walrus master pointers. */
35
35
  recordingMasterReference: string;
36
- /** `release_mix_reference` — optional per-track delivery descriptor pointers. */
37
- releaseMixReference?: string;
38
36
  /** `composition_credits` / `recording_credits` / `release_credits` extensions. */
39
37
  compositionCredits: string;
40
38
  recordingCredits: string;
@@ -3,7 +3,7 @@
3
3
 
4
4
  import { bcs } from "@mysten/sui/bcs";
5
5
  import { toBase64 } from "@mysten/sui/utils";
6
- import { walrusBlobIdFromU256 } from "../../mix.ts";
6
+ import { walrusBlobIdFromU256 } from "../../walrus-ids.ts";
7
7
 
8
8
  const u256 = bcs.u256();
9
9
 
package/src/read/types.ts CHANGED
@@ -76,8 +76,6 @@ export interface TrackView {
76
76
  disc: number;
77
77
  /** Base64url Walrus blob id for the master stream, when attached on-chain. */
78
78
  masterBlobId?: string;
79
- /** Base64url Walrus blob id for the public mix-delivery descriptor. */
80
- mixBlobId?: string;
81
79
  }
82
80
 
83
81
  /** A release with everything a page renders: metadata, cover, credits, tracklist. */
@@ -6,14 +6,17 @@
6
6
  import { bcs } from "@mysten/sui/bcs";
7
7
  import type { ClientWithCoreApi } from "@mysten/sui/client";
8
8
  import type { Transaction, TransactionArgument, TransactionObjectArgument } from "@mysten/sui/transactions";
9
- import { deriveDynamicFieldID } from "@mysten/sui/utils";
9
+ import { deriveDynamicFieldID, fromHex, toHex } from "@mysten/sui/utils";
10
+ import { isNotFound } from "@misofm/protocol";
10
11
  import type { TxThunk } from "./transactions.ts";
11
12
  import { invokeWithAdminCap, type AdminCapAuthority, type ObjectInput } from "./vault.ts";
12
13
  import * as advisory from "@misofm/protocol/contracts/recording_advisory/recording_advisory";
13
14
  import * as language from "@misofm/protocol/contracts/recording_language/recording_language";
14
- import * as walrusData from "@misofm/protocol/contracts/recording_master_reference/deps/ori/walrus_data";
15
+ import * as walrusData from "@misofm/protocol/contracts/recording_master_reference/deps/ori/data";
15
16
  import * as masterReference from "@misofm/protocol/contracts/recording_master_reference/recording_master_reference";
17
+ import * as engineSession from "@misofm/protocol/contracts/recording_engine_session/recording_engine_session";
16
18
  import * as streamingTranscode from "@misofm/protocol/contracts/recording_streaming_transcode/recording_streaming_transcode";
19
+ import { unencryptedWalrusBlob } from "./internal.ts";
17
20
 
18
21
  export interface RecordingExtensionTarget {
19
22
  readonly recordingId: ObjectInput;
@@ -77,7 +80,7 @@ export function setRecordingInstrumental(p: Omit<SetRecordingLanguagesParams, "l
77
80
  }
78
81
 
79
82
  interface RecordingWalrusReferenceParams extends RecordingExtensionTarget {
80
- /** An ori::WalrusData value assembled in the same PTB (must be a standalone blob on chain). */
83
+ /** An `ori::data::WalrusBlob` value assembled in the same PTB. */
81
84
  readonly reference: TransactionArgument;
82
85
  }
83
86
 
@@ -130,6 +133,100 @@ export type UnsetRecordingStreamingTranscodeParams = Omit<
130
133
  "oriPackageId" | "quiltId"
131
134
  >;
132
135
 
136
+ /** One stem a Session V1 document references, paired with its Walrus blob. */
137
+ export interface RecordingEngineSessionStemInput {
138
+ /** 32-byte SHA-256 of the stem's canonical PCM (engine STEM_IDENTITY_V1): raw bytes or 64 hex chars. */
139
+ readonly digest: Uint8Array | string;
140
+ /** Unencrypted Walrus blob holding the stem's FLAC, as its on-chain `u256`. */
141
+ readonly blobId: bigint | string;
142
+ }
143
+
144
+ export interface SetRecordingEngineSessionParams extends RecordingExtensionTarget {
145
+ readonly recordingEngineSessionPackageId: string;
146
+ /** External `ori` package used to construct the plaintext Walrus blob references. */
147
+ readonly oriPackageId: string;
148
+ /** Unencrypted Walrus blob holding the canonical Session V1 JSON, as its on-chain `u256`. */
149
+ readonly sessionBlobId: bigint | string;
150
+ /** Every stem the document's sources reference. Order does not matter; the builder sorts. */
151
+ readonly stems: readonly RecordingEngineSessionStemInput[];
152
+ }
153
+
154
+ const STEM_DIGEST_BYTES = 32;
155
+
156
+ function stemDigestBytes(digest: Uint8Array | string): Uint8Array {
157
+ const bytes = typeof digest === "string" ? fromHex(digest) : digest;
158
+ if (bytes.length !== STEM_DIGEST_BYTES) {
159
+ throw new Error(`stem digest must be ${STEM_DIGEST_BYTES} bytes`);
160
+ }
161
+ return bytes;
162
+ }
163
+
164
+ function compareBytes(a: Uint8Array, b: Uint8Array): number {
165
+ for (let index = 0; index < a.length; index += 1) {
166
+ if (a[index] !== b[index]) return a[index]! - b[index]!;
167
+ }
168
+ return 0;
169
+ }
170
+
171
+ /**
172
+ * Sets or replaces the Miso Engine session attached to a Recording: the
173
+ * Session V1 blob plus one `Stem` per source. Stems are sorted by digest and
174
+ * checked for duplicates here, mirroring `recording_engine_session::new`, so a
175
+ * bad input fails before signing rather than on chain.
176
+ */
177
+ export function setRecordingEngineSession(p: SetRecordingEngineSessionParams): TxThunk {
178
+ const stems = p.stems
179
+ .map((stem) => ({ digest: stemDigestBytes(stem.digest), blobId: stem.blobId }))
180
+ .sort((a, b) => compareBytes(a.digest, b.digest));
181
+ for (let index = 1; index < stems.length; index += 1) {
182
+ if (compareBytes(stems[index - 1]!.digest, stems[index]!.digest) === 0) {
183
+ throw new Error(`duplicate stem digest ${toHex(stems[index]!.digest)}`);
184
+ }
185
+ }
186
+ return (tx) => {
187
+ const pkg = p.recordingEngineSessionPackageId;
188
+ const sessionBlob = unencryptedWalrusBlob(tx, p.oriPackageId, p.sessionBlobId);
189
+ const stemValues = stems.map((stem) => tx.add(engineSession.newStem({
190
+ package: pkg,
191
+ arguments: [
192
+ tx.pure.vector("u8", Array.from(stem.digest)),
193
+ unencryptedWalrusBlob(tx, p.oriPackageId, stem.blobId),
194
+ ],
195
+ })));
196
+ const stemVector = tx.makeMoveVec({
197
+ type: `${pkg}::recording_engine_session::Stem`,
198
+ elements: stemValues,
199
+ });
200
+ const session = tx.add(engineSession._new({
201
+ package: pkg,
202
+ arguments: [sessionBlob, stemVector],
203
+ }));
204
+ invokeWithAdminCap(tx, p.authority, {
205
+ target: `${pkg}::recording_engine_session::set_engine_session`,
206
+ typeArguments: [p.recordingShareType, p.compositionShareType],
207
+ arguments: [object(tx, p.recordingId), session],
208
+ adminCapIndex: 1,
209
+ });
210
+ };
211
+ }
212
+
213
+ export type UnsetRecordingEngineSessionParams = Omit<
214
+ SetRecordingEngineSessionParams,
215
+ "oriPackageId" | "sessionBlobId" | "stems"
216
+ >;
217
+
218
+ /** Removes the Recording's Miso Engine session reference, if present. */
219
+ export function unsetRecordingEngineSession(p: UnsetRecordingEngineSessionParams): TxThunk {
220
+ return (tx) => {
221
+ invokeWithAdminCap(tx, p.authority, {
222
+ target: `${p.recordingEngineSessionPackageId}::recording_engine_session::unset_engine_session`,
223
+ typeArguments: [p.recordingShareType, p.compositionShareType],
224
+ arguments: [object(tx, p.recordingId)],
225
+ adminCapIndex: 1,
226
+ });
227
+ };
228
+ }
229
+
133
230
  /** Removes the Recording's streaming-transcode reference, if present. */
134
231
  export function unsetRecordingStreamingTranscode(
135
232
  p: UnsetRecordingStreamingTranscodeParams,
@@ -144,32 +241,95 @@ export function unsetRecordingStreamingTranscode(
144
241
  };
145
242
  }
146
243
 
244
+ // ── Engine-session reads ─────────────────────────────────────────────────────
245
+
246
+ /** A Recording's attached engine session, ids as decimal `u256` strings. */
247
+ export interface RecordingEngineSessionView {
248
+ /** Unencrypted Walrus blob holding the canonical Session V1 JSON. */
249
+ readonly sessionBlobId: string;
250
+ /** Sorted by digest, as stored. `digest` is 64 lowercase hex chars. */
251
+ readonly stems: readonly { readonly digest: string; readonly blobId: string }[];
252
+ }
253
+
254
+ const EngineSessionField = bcs.struct("Field", {
255
+ id: bcs.Address,
256
+ name: engineSession.ExtensionKey,
257
+ value: engineSession.EngineSession,
258
+ });
259
+ const ENGINE_SESSION_KEY_BYTES = engineSession.ExtensionKey.serialize([false]).toBytes();
260
+
261
+ /** Parse an engine-session dynamic field's BCS content. */
262
+ export function parseRecordingEngineSessionContent(content: Uint8Array): RecordingEngineSessionView {
263
+ const { value } = EngineSessionField.parse(content);
264
+ if (value.data.confidentiality.$kind !== "Unencrypted") {
265
+ throw new Error("Recording engine session is unexpectedly encrypted");
266
+ }
267
+ return {
268
+ sessionBlobId: String(value.data.blob_id),
269
+ stems: value.stems.map((stem) => {
270
+ if (stem.data.confidentiality.$kind !== "Unencrypted") {
271
+ throw new Error("Recording engine session stem is unexpectedly encrypted");
272
+ }
273
+ return { digest: toHex(Uint8Array.from(stem.digest)), blobId: String(stem.data.blob_id) };
274
+ }),
275
+ };
276
+ }
277
+
278
+ /** Deterministic dynamic-field id for a Recording's engine session. */
279
+ export function recordingEngineSessionFieldId(
280
+ recordingId: string,
281
+ recordingEngineSessionPackageId: string,
282
+ ): string {
283
+ return deriveDynamicFieldID(
284
+ recordingId,
285
+ `${recordingEngineSessionPackageId}::recording_engine_session::ExtensionKey`,
286
+ ENGINE_SESSION_KEY_BYTES,
287
+ );
288
+ }
289
+
290
+ /** Read one Recording's engine session, or null when none is attached. */
291
+ export async function getRecordingEngineSession(
292
+ client: ClientWithCoreApi,
293
+ recordingId: string,
294
+ recordingEngineSessionPackageId: string,
295
+ ): Promise<RecordingEngineSessionView | null> {
296
+ const fieldId = recordingEngineSessionFieldId(recordingId, recordingEngineSessionPackageId);
297
+ const { objects } = await client.core.getObjects({
298
+ objectIds: [fieldId],
299
+ include: { content: true },
300
+ });
301
+ const field = objects[0];
302
+ if (!field) throw new Error("Recording engine-session read returned no result");
303
+ if (field instanceof Error) {
304
+ if (isNotFound(field)) return null;
305
+ throw field;
306
+ }
307
+ if (!field.content) throw new Error("Recording engine-session field is missing BCS content");
308
+ return parseRecordingEngineSessionContent(field.content);
309
+ }
310
+
147
311
  // ── Master-reference reads ───────────────────────────────────────────────────
148
312
 
149
- // recording_master_reference stores the ori::WalrusData value inline in a
313
+ // recording_master_reference stores the ori::data::WalrusBlob value inline in a
150
314
  // dynamic field on the Recording. Its empty ExtensionKey serializes to one false
151
315
  // byte, so the field object id can be derived without listing the Recording's
152
316
  // dynamic fields.
153
317
  const MasterReferenceField = bcs.struct("Field", {
154
318
  id: bcs.Address,
155
319
  name: masterReference.ExtensionKey,
156
- value: walrusData.WalrusData,
320
+ value: walrusData.WalrusBlob,
157
321
  });
158
322
  const MASTER_REFERENCE_KEY_BYTES = masterReference.ExtensionKey.serialize([
159
323
  false,
160
324
  ]).toBytes();
161
325
 
162
- /** Parse an attached master reference's standalone Walrus blob id. */
163
- export function parseRecordingMasterReferenceContent(
164
- content: Uint8Array,
165
- ): string | null {
166
- const reference = MasterReferenceField.parse(content).value as
167
- | { $kind: "Blob"; Blob: [string | number | bigint, unknown] }
168
- | {
169
- $kind: "QuiltPatch";
170
- QuiltPatch: [string | number | bigint, number, number, number];
171
- };
172
- return reference.$kind === "Blob" ? String(reference.Blob[0]) : null;
326
+ /**
327
+ * Parse an attached master reference's standalone Walrus blob id (decimal
328
+ * `u256`). Encrypted masters are returned as-is: the reference is the same
329
+ * either way, and access control belongs to the caller.
330
+ */
331
+ export function parseRecordingMasterReferenceContent(content: Uint8Array): string {
332
+ return String(MasterReferenceField.parse(content).value.blob_id);
173
333
  }
174
334
 
175
335
  /** Deterministic dynamic-field id for a Recording's master reference. */
@@ -231,8 +391,7 @@ export async function getRecordingMasterReferencesByIds(
231
391
  const target = targets[index];
232
392
  if (!target || object instanceof Error || !object.content) return;
233
393
  try {
234
- const blobId = parseRecordingMasterReferenceContent(object.content);
235
- if (blobId) out[target.recordingId] = blobId;
394
+ out[target.recordingId] = parseRecordingMasterReferenceContent(object.content);
236
395
  } catch {
237
396
  // Extension metadata is soft: retain valid tracks when one field is stale.
238
397
  }
@@ -0,0 +1,102 @@
1
+ // Copyright (c) Miso Labs, Inc.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Walrus blob id conversions.
6
+ *
7
+ * On chain a blob id is a `u256` (BCS little-endian). Off chain it is the
8
+ * unpadded base64url encoding of those same 32 little-endian bytes, which is
9
+ * also its key in a `miso-sources` staging bucket and its path on an
10
+ * aggregator. Both directions here are canonical: a value that does not
11
+ * round-trip byte for byte is rejected.
12
+ */
13
+
14
+ const ALPHABET =
15
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
16
+
17
+ /** Parse a `bigint` or decimal string as an exact `u256`. */
18
+ export function walrusU256(label: string, value: bigint | string): bigint {
19
+ let parsed: bigint;
20
+ try {
21
+ parsed = typeof value === "bigint" ? value : BigInt(value);
22
+ } catch {
23
+ throw new Error(`${label} must be an exact u256 integer`);
24
+ }
25
+ if (parsed < 0n || parsed >= 1n << 256n) {
26
+ throw new Error(`${label} is outside u256 range`);
27
+ }
28
+ return parsed;
29
+ }
30
+
31
+ /** The base64url Walrus blob id for an on-chain `u256` blob id. */
32
+ export function walrusBlobIdFromU256(value: bigint | string): string {
33
+ let remaining = walrusU256("Walrus blob id", value);
34
+ const bytes = new Uint8Array(32);
35
+ for (let index = 0; index < 32; index += 1) {
36
+ bytes[index] = Number(remaining & 0xffn);
37
+ remaining >>= 8n;
38
+ }
39
+ return bytesToBase64Url(bytes);
40
+ }
41
+
42
+ /** The on-chain `u256` for a base64url Walrus blob id (43 characters). */
43
+ export function walrusBlobIdToU256(blobId: string): bigint {
44
+ const bytes = base64UrlToBytes(blobId);
45
+ if (bytes.length !== 32) {
46
+ throw new Error("Walrus blob id must decode to exactly 32 bytes");
47
+ }
48
+ let value = 0n;
49
+ for (let index = 31; index >= 0; index -= 1) {
50
+ value = (value << 8n) | BigInt(bytes[index]!);
51
+ }
52
+ return value;
53
+ }
54
+
55
+ /** Unpadded base64url. */
56
+ export function bytesToBase64Url(bytes: Uint8Array): string {
57
+ let output = "";
58
+ for (let index = 0; index < bytes.length; index += 3) {
59
+ const a = bytes[index] ?? 0;
60
+ const b = bytes[index + 1] ?? 0;
61
+ const c = bytes[index + 2] ?? 0;
62
+ output += ALPHABET[a >>> 2];
63
+ output += ALPHABET[((a & 3) << 4) | (b >>> 4)];
64
+ if (index + 1 < bytes.length) {
65
+ output += ALPHABET[((b & 15) << 2) | (c >>> 6)];
66
+ }
67
+ if (index + 2 < bytes.length) output += ALPHABET[c & 63];
68
+ }
69
+ return output;
70
+ }
71
+
72
+ /** Strict inverse of {@link bytesToBase64Url}: no padding, no stray bits. */
73
+ export function base64UrlToBytes(value: string): Uint8Array {
74
+ if (
75
+ typeof value !== "string" ||
76
+ value.length % 4 === 1 ||
77
+ value.includes("=") ||
78
+ !/^[A-Za-z0-9_-]*$/.test(value)
79
+ ) {
80
+ throw new Error("invalid canonical base64url");
81
+ }
82
+ const bytes = new Uint8Array(Math.floor((value.length * 6) / 8));
83
+ let accumulator = 0;
84
+ let bits = 0;
85
+ let outputIndex = 0;
86
+ for (const character of value) {
87
+ const digit = ALPHABET.indexOf(character);
88
+ if (digit < 0) throw new Error("invalid canonical base64url");
89
+ accumulator = (accumulator << 6) | digit;
90
+ bits += 6;
91
+ if (bits >= 8) {
92
+ bits -= 8;
93
+ bytes[outputIndex] = (accumulator >>> bits) & 0xff;
94
+ outputIndex += 1;
95
+ accumulator &= (1 << bits) - 1;
96
+ }
97
+ }
98
+ if (bytesToBase64Url(bytes) !== value) {
99
+ throw new Error("invalid canonical base64url padding bits");
100
+ }
101
+ return bytes;
102
+ }