@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.
- package/README.md +15 -15
- package/dist/client.d.ts +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/contracts.d.ts +1 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +1 -0
- package/dist/contracts.js.map +1 -1
- package/dist/cover.d.ts +5 -8
- package/dist/cover.d.ts.map +1 -1
- package/dist/cover.js +5 -17
- package/dist/cover.js.map +1 -1
- package/dist/deployments.d.ts +7 -3
- package/dist/deployments.d.ts.map +1 -1
- package/dist/deployments.js +3 -1
- package/dist/deployments.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +11 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +18 -1
- package/dist/internal.js.map +1 -1
- package/dist/publication.d.ts +14 -0
- package/dist/publication.d.ts.map +1 -1
- package/dist/publication.js +17 -5
- package/dist/publication.js.map +1 -1
- package/dist/read/catalog.d.ts +1 -1
- package/dist/read/catalog.d.ts.map +1 -1
- package/dist/read/catalog.js +6 -23
- package/dist/read/catalog.js.map +1 -1
- package/dist/read/config.d.ts +0 -2
- package/dist/read/config.d.ts.map +1 -1
- package/dist/read/config.js.map +1 -1
- package/dist/read/internal/walrus.js +1 -1
- package/dist/read/internal/walrus.js.map +1 -1
- package/dist/read/types.d.ts +0 -2
- package/dist/read/types.d.ts.map +1 -1
- package/dist/recording-extensions.d.ts +49 -3
- package/dist/recording-extensions.d.ts.map +1 -1
- package/dist/recording-extensions.js +126 -10
- package/dist/recording-extensions.js.map +1 -1
- package/dist/walrus-ids.d.ts +11 -0
- package/dist/walrus-ids.d.ts.map +1 -0
- package/dist/walrus-ids.js +96 -0
- package/dist/walrus-ids.js.map +1 -0
- package/package.json +2 -8
- package/src/client.ts +1 -1
- package/src/contracts.ts +1 -0
- package/src/cover.ts +21 -41
- package/src/deployments.ts +9 -4
- package/src/index.ts +1 -2
- package/src/internal.ts +25 -0
- package/src/publication.ts +36 -4
- package/src/read/catalog.ts +4 -29
- package/src/read/config.ts +0 -2
- package/src/read/internal/walrus.ts +1 -1
- package/src/read/types.ts +0 -2
- package/src/recording-extensions.ts +177 -18
- package/src/walrus-ids.ts +102 -0
- package/dist/mix.d.ts +0 -144
- package/dist/mix.d.ts.map +0 -1
- package/dist/mix.js +0 -656
- package/dist/mix.js.map +0 -1
- package/src/mix.ts +0 -910
package/src/publication.ts
CHANGED
|
@@ -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.
|
|
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) {
|
package/src/read/catalog.ts
CHANGED
|
@@ -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
|
|
73
|
+
/** Aggregator URL for a cover image ref (a standalone Walrus blob). */
|
|
79
74
|
function imageUrl(aggregator: string, ref: CoverImageRef): string {
|
|
80
|
-
|
|
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
|
|
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
|
|
394
|
+
tracks: toTracks(release, titles, masterBlobIds),
|
|
420
395
|
...(trackCredits !== undefined ? { trackCredits } : {}),
|
|
421
396
|
};
|
|
422
397
|
}
|
package/src/read/config.ts
CHANGED
|
@@ -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;
|
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/
|
|
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::
|
|
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::
|
|
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.
|
|
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
|
-
/**
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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
|
+
}
|