@misofm/platform 0.0.0 → 0.18.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 (168) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +686 -3
  3. package/dist/auth.d.ts +73 -0
  4. package/dist/auth.d.ts.map +1 -0
  5. package/dist/auth.js +193 -0
  6. package/dist/auth.js.map +1 -0
  7. package/dist/catalog.d.ts +48 -0
  8. package/dist/catalog.d.ts.map +1 -0
  9. package/dist/catalog.js +92 -0
  10. package/dist/catalog.js.map +1 -0
  11. package/dist/client.d.ts +462 -0
  12. package/dist/client.d.ts.map +1 -0
  13. package/dist/client.js +788 -0
  14. package/dist/client.js.map +1 -0
  15. package/dist/contracts.d.ts +34 -0
  16. package/dist/contracts.d.ts.map +1 -0
  17. package/dist/contracts.js +49 -0
  18. package/dist/contracts.js.map +1 -0
  19. package/dist/cover.d.ts +69 -0
  20. package/dist/cover.d.ts.map +1 -0
  21. package/dist/cover.js +132 -0
  22. package/dist/cover.js.map +1 -0
  23. package/dist/credits.d.ts +200 -0
  24. package/dist/credits.d.ts.map +1 -0
  25. package/dist/credits.js +465 -0
  26. package/dist/credits.js.map +1 -0
  27. package/dist/deployments.d.ts +224 -0
  28. package/dist/deployments.d.ts.map +1 -0
  29. package/dist/deployments.js +177 -0
  30. package/dist/deployments.js.map +1 -0
  31. package/dist/execute.d.ts +30 -0
  32. package/dist/execute.d.ts.map +1 -0
  33. package/dist/execute.js +31 -0
  34. package/dist/execute.js.map +1 -0
  35. package/dist/index.d.ts +21 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +52 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/internal.d.ts +13 -0
  40. package/dist/internal.d.ts.map +1 -0
  41. package/dist/internal.js +41 -0
  42. package/dist/internal.js.map +1 -0
  43. package/dist/mix.d.ts +144 -0
  44. package/dist/mix.d.ts.map +1 -0
  45. package/dist/mix.js +656 -0
  46. package/dist/mix.js.map +1 -0
  47. package/dist/pressing.d.ts +137 -0
  48. package/dist/pressing.d.ts.map +1 -0
  49. package/dist/pressing.js +390 -0
  50. package/dist/pressing.js.map +1 -0
  51. package/dist/publication.d.ts +216 -0
  52. package/dist/publication.d.ts.map +1 -0
  53. package/dist/publication.js +872 -0
  54. package/dist/publication.js.map +1 -0
  55. package/dist/queries.d.ts +2 -0
  56. package/dist/queries.d.ts.map +1 -0
  57. package/dist/queries.js +8 -0
  58. package/dist/queries.js.map +1 -0
  59. package/dist/read/artist.d.ts +18 -0
  60. package/dist/read/artist.d.ts.map +1 -0
  61. package/dist/read/artist.js +86 -0
  62. package/dist/read/artist.js.map +1 -0
  63. package/dist/read/catalog.d.ts +93 -0
  64. package/dist/read/catalog.d.ts.map +1 -0
  65. package/dist/read/catalog.js +441 -0
  66. package/dist/read/catalog.js.map +1 -0
  67. package/dist/read/client.d.ts +37 -0
  68. package/dist/read/client.d.ts.map +1 -0
  69. package/dist/read/client.js +50 -0
  70. package/dist/read/client.js.map +1 -0
  71. package/dist/read/config.d.ts +68 -0
  72. package/dist/read/config.d.ts.map +1 -0
  73. package/dist/read/config.js +63 -0
  74. package/dist/read/config.js.map +1 -0
  75. package/dist/read/genres.d.ts +7 -0
  76. package/dist/read/genres.d.ts.map +1 -0
  77. package/dist/read/genres.js +38 -0
  78. package/dist/read/genres.js.map +1 -0
  79. package/dist/read/index.d.ts +14 -0
  80. package/dist/read/index.d.ts.map +1 -0
  81. package/dist/read/index.js +23 -0
  82. package/dist/read/index.js.map +1 -0
  83. package/dist/read/internal/scalars.d.ts +23 -0
  84. package/dist/read/internal/scalars.d.ts.map +1 -0
  85. package/dist/read/internal/scalars.js +57 -0
  86. package/dist/read/internal/scalars.js.map +1 -0
  87. package/dist/read/internal/walrus.d.ts +6 -0
  88. package/dist/read/internal/walrus.d.ts.map +1 -0
  89. package/dist/read/internal/walrus.js +29 -0
  90. package/dist/read/internal/walrus.js.map +1 -0
  91. package/dist/read/receipts.d.ts +41 -0
  92. package/dist/read/receipts.d.ts.map +1 -0
  93. package/dist/read/receipts.js +398 -0
  94. package/dist/read/receipts.js.map +1 -0
  95. package/dist/read/types.d.ts +328 -0
  96. package/dist/read/types.d.ts.map +1 -0
  97. package/dist/read/types.js +21 -0
  98. package/dist/read/types.js.map +1 -0
  99. package/dist/read/wallet.d.ts +74 -0
  100. package/dist/read/wallet.d.ts.map +1 -0
  101. package/dist/read/wallet.js +578 -0
  102. package/dist/read/wallet.js.map +1 -0
  103. package/dist/read/works.d.ts +29 -0
  104. package/dist/read/works.d.ts.map +1 -0
  105. package/dist/read/works.js +191 -0
  106. package/dist/read/works.js.map +1 -0
  107. package/dist/recording-extensions.d.ts +62 -0
  108. package/dist/recording-extensions.d.ts.map +1 -0
  109. package/dist/recording-extensions.js +159 -0
  110. package/dist/recording-extensions.js.map +1 -0
  111. package/dist/release-extensions.d.ts +93 -0
  112. package/dist/release-extensions.d.ts.map +1 -0
  113. package/dist/release-extensions.js +190 -0
  114. package/dist/release-extensions.js.map +1 -0
  115. package/dist/release-graph.d.ts +65 -0
  116. package/dist/release-graph.d.ts.map +1 -0
  117. package/dist/release-graph.js +136 -0
  118. package/dist/release-graph.js.map +1 -0
  119. package/dist/share-template.d.ts +3 -0
  120. package/dist/share-template.d.ts.map +1 -0
  121. package/dist/share-template.js +46 -0
  122. package/dist/share-template.js.map +1 -0
  123. package/dist/share.d.ts +67 -0
  124. package/dist/share.d.ts.map +1 -0
  125. package/dist/share.js +157 -0
  126. package/dist/share.js.map +1 -0
  127. package/dist/transactions.d.ts +216 -0
  128. package/dist/transactions.d.ts.map +1 -0
  129. package/dist/transactions.js +314 -0
  130. package/dist/transactions.js.map +1 -0
  131. package/dist/vault.d.ts +364 -0
  132. package/dist/vault.d.ts.map +1 -0
  133. package/dist/vault.js +507 -0
  134. package/dist/vault.js.map +1 -0
  135. package/package.json +145 -5
  136. package/src/auth.ts +275 -0
  137. package/src/catalog.ts +189 -0
  138. package/src/client.ts +1320 -0
  139. package/src/contracts.ts +54 -0
  140. package/src/cover.ts +240 -0
  141. package/src/credits.ts +906 -0
  142. package/src/deployments.ts +357 -0
  143. package/src/execute.ts +52 -0
  144. package/src/index.ts +69 -0
  145. package/src/internal.ts +46 -0
  146. package/src/mix.ts +910 -0
  147. package/src/pressing.ts +705 -0
  148. package/src/publication.ts +1214 -0
  149. package/src/queries.ts +9 -0
  150. package/src/read/artist.ts +118 -0
  151. package/src/read/catalog.ts +668 -0
  152. package/src/read/client.ts +87 -0
  153. package/src/read/config.ts +138 -0
  154. package/src/read/genres.ts +37 -0
  155. package/src/read/index.ts +85 -0
  156. package/src/read/internal/scalars.ts +59 -0
  157. package/src/read/internal/walrus.ts +42 -0
  158. package/src/read/receipts.ts +476 -0
  159. package/src/read/types.ts +415 -0
  160. package/src/read/wallet.ts +687 -0
  161. package/src/read/works.ts +297 -0
  162. package/src/recording-extensions.ts +256 -0
  163. package/src/release-extensions.ts +311 -0
  164. package/src/release-graph.ts +219 -0
  165. package/src/share-template.ts +55 -0
  166. package/src/share.ts +261 -0
  167. package/src/transactions.ts +542 -0
  168. package/src/vault.ts +1009 -0
package/package.json CHANGED
@@ -1,10 +1,150 @@
1
1
  {
2
2
  "name": "@misofm/platform",
3
- "version": "0.0.0",
4
- "description": "Placeholder reserving @misofm/platform. Not usable — see the real release.",
3
+ "version": "0.18.0",
4
+ "packageManager": "bun@1.4.0",
5
+ "description": "Complete client SDK for the Miso platform on Sui — high-level reads, pressings, listings, records, and work publishing.",
5
6
  "license": "Apache-2.0",
6
- "repository": { "type": "git", "url": "git+https://github.com/misofm/sdk.git" },
7
- "publishConfig": { "access": "public" },
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/misofm/sdks.git"
10
+ },
11
+ "homepage": "https://github.com/misofm/sdks#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/misofm/sdks/issues"
14
+ },
15
+ "keywords": [
16
+ "sui",
17
+ "move",
18
+ "miso",
19
+ "music",
20
+ "blockchain",
21
+ "sdk",
22
+ "pressing"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "module": "./dist/index.js",
28
+ "types": "./dist/index.d.ts",
8
29
  "type": "module",
9
- "files": ["README.md"]
30
+ "sideEffects": false,
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.js"
35
+ },
36
+ "./client": {
37
+ "types": "./dist/client.d.ts",
38
+ "default": "./dist/client.js"
39
+ },
40
+ "./deployments": {
41
+ "types": "./dist/deployments.d.ts",
42
+ "default": "./dist/deployments.js"
43
+ },
44
+ "./pressing": {
45
+ "types": "./dist/pressing.d.ts",
46
+ "default": "./dist/pressing.js"
47
+ },
48
+ "./transactions": {
49
+ "types": "./dist/transactions.d.ts",
50
+ "default": "./dist/transactions.js"
51
+ },
52
+ "./execute": {
53
+ "types": "./dist/execute.d.ts",
54
+ "default": "./dist/execute.js"
55
+ },
56
+ "./share": {
57
+ "types": "./dist/share.d.ts",
58
+ "default": "./dist/share.js"
59
+ },
60
+ "./share-template": {
61
+ "types": "./dist/share-template.d.ts",
62
+ "default": "./dist/share-template.js"
63
+ },
64
+ "./release-graph": {
65
+ "types": "./dist/release-graph.d.ts",
66
+ "default": "./dist/release-graph.js"
67
+ },
68
+ "./publication": {
69
+ "types": "./dist/publication.d.ts",
70
+ "default": "./dist/publication.js"
71
+ },
72
+ "./catalog": {
73
+ "types": "./dist/catalog.d.ts",
74
+ "default": "./dist/catalog.js"
75
+ },
76
+ "./credits": {
77
+ "types": "./dist/credits.d.ts",
78
+ "default": "./dist/credits.js"
79
+ },
80
+ "./cover": {
81
+ "types": "./dist/cover.d.ts",
82
+ "default": "./dist/cover.js"
83
+ },
84
+ "./release-extensions": {
85
+ "types": "./dist/release-extensions.d.ts",
86
+ "default": "./dist/release-extensions.js"
87
+ },
88
+ "./recording-extensions": {
89
+ "types": "./dist/recording-extensions.d.ts",
90
+ "default": "./dist/recording-extensions.js"
91
+ },
92
+ "./mix": {
93
+ "types": "./dist/mix.d.ts",
94
+ "default": "./dist/mix.js"
95
+ },
96
+ "./read": {
97
+ "types": "./dist/read/index.d.ts",
98
+ "default": "./dist/read/index.js"
99
+ },
100
+ "./read/types": {
101
+ "types": "./dist/read/types.d.ts",
102
+ "default": "./dist/read/types.js"
103
+ },
104
+ "./auth": {
105
+ "types": "./dist/auth.d.ts",
106
+ "default": "./dist/auth.js"
107
+ },
108
+ "./vault": {
109
+ "types": "./dist/vault.d.ts",
110
+ "default": "./dist/vault.js"
111
+ },
112
+ "./package.json": "./package.json"
113
+ },
114
+ "files": [
115
+ "dist",
116
+ "src",
117
+ "README.md",
118
+ "LICENSE",
119
+ "package.json"
120
+ ],
121
+ "scripts": {
122
+ "codegen": "sui-ts-codegen generate && bun run codegen:format",
123
+ "codegen:format": "bun scripts/format-codegen.ts",
124
+ "codegen:check": "bun run codegen && git diff --exit-code -- src/contracts",
125
+ "typecheck": "tsc --noEmit",
126
+ "test": "bun test",
127
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
128
+ "prepack": "rm -rf dist && tsc -p tsconfig.build.json",
129
+ "prepare": "rm -rf dist && tsc -p tsconfig.build.json --noCheck"
130
+ },
131
+ "dependencies": {
132
+ "@misofm/protocol": "0.12.0",
133
+ "@mysten/bcs": "^2.1.1"
134
+ },
135
+ "devDependencies": {
136
+ "@mysten/codegen": "^0.11.14",
137
+ "@mysten/move-bytecode-template": "^0.4.0",
138
+ "@mysten/seal": "1.4.6",
139
+ "@mysten/sui": "2.29.0",
140
+ "@types/bun": "^1.3.14",
141
+ "typescript": "^7.0.2"
142
+ },
143
+ "peerDependencies": {
144
+ "@mysten/move-bytecode-template": "^0.4.0",
145
+ "@mysten/seal": "1.4.6",
146
+ "@mysten/sui": "2.29.0",
147
+ "typescript": "^5.7 || ^6 || ^7"
148
+ },
149
+ "main": "./dist/index.js"
10
150
  }
package/src/auth.ts ADDED
@@ -0,0 +1,275 @@
1
+ // Copyright (c) Miso Labs, Inc.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { isValidSuiAddress, normalizeSuiAddress } from "@mysten/sui/utils";
5
+
6
+ export type AuthNetwork = "testnet" | "mainnet";
7
+
8
+ export interface ApiAuthorizationFields {
9
+ method: string;
10
+ path: string;
11
+ address: string;
12
+ network: AuthNetwork;
13
+ issuedAt: string;
14
+ }
15
+
16
+ export interface AuthorizationChallenge extends ApiAuthorizationFields {
17
+ payload: string;
18
+ expiresAt: string;
19
+ }
20
+
21
+ export interface PersonalMessageSigner {
22
+ signPersonalMessage(message: Uint8Array): Promise<{ signature: string; bytes?: string }>;
23
+ }
24
+
25
+ export const AUTHORIZATION_MAX_AGE_MS = 5 * 60 * 1_000;
26
+ export const AUTHORIZATION_FUTURE_SKEW_MS = 60 * 1_000;
27
+
28
+ export const MISO_AUTH_HEADERS = {
29
+ address: "X-Sui-Address",
30
+ signature: "X-Sui-Signature",
31
+ issuedAt: "X-Sui-Issued-At",
32
+ } as const;
33
+
34
+ export type MisoAuthErrorCode =
35
+ | "invalid_target"
36
+ | "challenge_rejected"
37
+ | "invalid_challenge"
38
+ | "signing_failed";
39
+
40
+ export class MisoAuthError extends Error {
41
+ readonly code: MisoAuthErrorCode;
42
+ readonly status?: number;
43
+
44
+ constructor(code: MisoAuthErrorCode, message: string, options: { status?: number; cause?: unknown } = {}) {
45
+ super(message, options.cause === undefined ? undefined : { cause: options.cause });
46
+ this.name = "MisoAuthError";
47
+ this.code = code;
48
+ this.status = options.status;
49
+ }
50
+ }
51
+
52
+ /** The byte-exact Sui personal message required for an authenticated API mutation. */
53
+ export function buildApiAuthorizationPayload(fields: ApiAuthorizationFields): string {
54
+ return [
55
+ "miso.fm API authorization v1",
56
+ "",
57
+ `Method: ${fields.method.toUpperCase()}`,
58
+ `Path: ${fields.path}`,
59
+ `Address: ${fields.address}`,
60
+ `Network: ${fields.network}`,
61
+ `Issued At: ${fields.issuedAt}`,
62
+ ].join("\n");
63
+ }
64
+
65
+ export function isValidAuthorizationTarget(method: string, path: string): boolean {
66
+ return (
67
+ ["POST", "PUT", "PATCH", "DELETE"].includes(method.toUpperCase()) &&
68
+ path.startsWith("/platform/") &&
69
+ path.length <= 2_048 &&
70
+ !/[\r\n?#]/.test(path)
71
+ );
72
+ }
73
+
74
+ /** Accept only fresh timestamps in one canonical spelling. */
75
+ export function parseFreshAuthorizationIssuedAt(input: unknown, nowMs: number = Date.now()): string | null {
76
+ if (typeof input !== "string") return null;
77
+ const issuedAtMs = Date.parse(input);
78
+ if (!Number.isFinite(issuedAtMs)) return null;
79
+ if (
80
+ issuedAtMs > nowMs + AUTHORIZATION_FUTURE_SKEW_MS ||
81
+ nowMs - issuedAtMs > AUTHORIZATION_MAX_AGE_MS
82
+ ) {
83
+ return null;
84
+ }
85
+ const canonical = new Date(issuedAtMs).toISOString();
86
+ return input === canonical ? canonical : null;
87
+ }
88
+
89
+ function target(method: string, path: string): { method: string; path: string } {
90
+ const normalized = { method: method.toUpperCase(), path };
91
+ if (!isValidAuthorizationTarget(normalized.method, normalized.path)) {
92
+ throw new MisoAuthError("invalid_target", "Authenticated requests require a mutation under /platform/.");
93
+ }
94
+ return normalized;
95
+ }
96
+
97
+ function parseChallenge(
98
+ input: unknown,
99
+ expected: { method: string; path: string; address: string; network?: AuthNetwork; nowMs?: number },
100
+ ): AuthorizationChallenge {
101
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
102
+ throw new MisoAuthError("invalid_challenge", "The API returned an invalid authorization challenge.");
103
+ }
104
+ const body = input as Partial<Record<keyof AuthorizationChallenge, unknown>>;
105
+ if (
106
+ typeof body.payload !== "string" ||
107
+ typeof body.method !== "string" ||
108
+ typeof body.path !== "string" ||
109
+ typeof body.address !== "string" ||
110
+ typeof body.network !== "string" ||
111
+ typeof body.issuedAt !== "string" ||
112
+ typeof body.expiresAt !== "string" ||
113
+ !isValidSuiAddress(body.address) ||
114
+ (body.network !== "testnet" && body.network !== "mainnet")
115
+ ) {
116
+ throw new MisoAuthError("invalid_challenge", "The API returned an invalid authorization challenge.");
117
+ }
118
+
119
+ const nowMs = expected.nowMs ?? Date.now();
120
+ const issuedAt = parseFreshAuthorizationIssuedAt(body.issuedAt, nowMs);
121
+ const expiresAtMs = Date.parse(body.expiresAt);
122
+ const canonicalAddress = normalizeSuiAddress(body.address);
123
+ const expectedAddress = normalizeSuiAddress(expected.address);
124
+ const challenge: AuthorizationChallenge = {
125
+ payload: body.payload,
126
+ method: body.method,
127
+ path: body.path,
128
+ address: canonicalAddress,
129
+ network: body.network,
130
+ issuedAt: body.issuedAt,
131
+ expiresAt: body.expiresAt,
132
+ };
133
+ const canonicalExpiresAt = Number.isFinite(expiresAtMs) ? new Date(expiresAtMs).toISOString() : "";
134
+
135
+ if (
136
+ !issuedAt ||
137
+ body.method !== expected.method ||
138
+ body.path !== expected.path ||
139
+ canonicalAddress !== expectedAddress ||
140
+ (expected.network !== undefined && body.network !== expected.network) ||
141
+ body.expiresAt !== canonicalExpiresAt ||
142
+ expiresAtMs <= nowMs ||
143
+ expiresAtMs > Date.parse(issuedAt) + AUTHORIZATION_MAX_AGE_MS ||
144
+ body.payload !== buildApiAuthorizationPayload(challenge)
145
+ ) {
146
+ throw new MisoAuthError("invalid_challenge", "The authorization challenge did not match this request.");
147
+ }
148
+ return challenge;
149
+ }
150
+
151
+ async function responseMessage(response: Response): Promise<string> {
152
+ const body = await response.clone().json().catch(() => null) as {
153
+ error?: { message?: unknown } | string;
154
+ message?: unknown;
155
+ } | null;
156
+ if (typeof body?.error === "object" && typeof body.error.message === "string") return body.error.message;
157
+ if (typeof body?.error === "string") return body.error;
158
+ if (typeof body?.message === "string") return body.message;
159
+ return `Authorization challenge failed (${response.status}).`;
160
+ }
161
+
162
+ export interface RequestAuthorizationChallengeOptions {
163
+ apiUrl: string | URL;
164
+ token: string;
165
+ address: string;
166
+ method: string;
167
+ path: string;
168
+ network?: AuthNetwork;
169
+ challengeUrl?: string | URL;
170
+ fetch?: typeof globalThis.fetch;
171
+ nowMs?: number;
172
+ }
173
+
174
+ /** Ask Miso to verify the Enoki account and issue the exact message to sign. */
175
+ export async function requestAuthorizationChallenge(
176
+ options: RequestAuthorizationChallengeOptions,
177
+ ): Promise<AuthorizationChallenge> {
178
+ const expected = target(options.method, options.path);
179
+ if (!options.token || !isValidSuiAddress(options.address)) {
180
+ throw new MisoAuthError("challenge_rejected", "A valid Enoki token and Sui address are required.");
181
+ }
182
+ const apiUrl = new URL(options.apiUrl);
183
+ const challengeUrl = options.challengeUrl === undefined
184
+ ? new URL("/platform/auth/challenge", apiUrl.origin)
185
+ : new URL(options.challengeUrl, apiUrl);
186
+ const fetcher = options.fetch ?? globalThis.fetch;
187
+ const response = await fetcher(challengeUrl, {
188
+ method: "POST",
189
+ headers: {
190
+ Authorization: `Bearer ${options.token}`,
191
+ [MISO_AUTH_HEADERS.address]: normalizeSuiAddress(options.address),
192
+ "Content-Type": "application/json",
193
+ },
194
+ body: JSON.stringify(expected),
195
+ });
196
+ if (!response.ok) {
197
+ throw new MisoAuthError("challenge_rejected", await responseMessage(response), { status: response.status });
198
+ }
199
+ const body = await response.json().catch(() => null);
200
+ return parseChallenge(body, {
201
+ ...expected,
202
+ address: options.address,
203
+ network: options.network,
204
+ nowMs: options.nowMs,
205
+ });
206
+ }
207
+
208
+ export interface CreateAuthorizationHeadersOptions extends RequestAuthorizationChallengeOptions {
209
+ signer: PersonalMessageSigner;
210
+ }
211
+
212
+ /** Verify the challenge locally, sign it as a Sui personal message, and produce API headers. */
213
+ export async function createAuthorizationHeaders(
214
+ options: CreateAuthorizationHeadersOptions,
215
+ ): Promise<{ challenge: AuthorizationChallenge; headers: Headers }> {
216
+ const challenge = await requestAuthorizationChallenge(options);
217
+ let signature: string;
218
+ try {
219
+ const signed = await options.signer.signPersonalMessage(new TextEncoder().encode(challenge.payload));
220
+ signature = signed.signature;
221
+ } catch (cause) {
222
+ throw new MisoAuthError("signing_failed", "The authorization request was not signed.", { cause });
223
+ }
224
+ if (!signature) {
225
+ throw new MisoAuthError("signing_failed", "The signer returned an empty authorization signature.");
226
+ }
227
+ return {
228
+ challenge,
229
+ headers: new Headers({
230
+ Authorization: `Bearer ${options.token}`,
231
+ [MISO_AUTH_HEADERS.address]: challenge.address,
232
+ [MISO_AUTH_HEADERS.signature]: signature,
233
+ [MISO_AUTH_HEADERS.issuedAt]: challenge.issuedAt,
234
+ }),
235
+ };
236
+ }
237
+
238
+ export interface AuthenticatedFetchOptions extends RequestInit {
239
+ auth: {
240
+ token: string;
241
+ address: string;
242
+ signer: PersonalMessageSigner;
243
+ network?: AuthNetwork;
244
+ };
245
+ challengeUrl?: string | URL;
246
+ fetch?: typeof globalThis.fetch;
247
+ }
248
+
249
+ /** Perform a protected Miso API mutation with a fresh Enoki-verified Sui signature. */
250
+ export async function authenticatedFetch(
251
+ input: string | URL,
252
+ options: AuthenticatedFetchOptions,
253
+ ): Promise<Response> {
254
+ const { auth, challengeUrl, fetch: fetchOption, ...init } = options;
255
+ const url = new URL(input);
256
+ const method = (init.method ?? "").toUpperCase();
257
+ if (url.search || url.hash) {
258
+ throw new MisoAuthError("invalid_target", "Authenticated request URLs cannot include a query or fragment.");
259
+ }
260
+ const fetcher = fetchOption ?? globalThis.fetch;
261
+ const authorization = await createAuthorizationHeaders({
262
+ apiUrl: url,
263
+ challengeUrl,
264
+ fetch: fetcher,
265
+ token: auth.token,
266
+ address: auth.address,
267
+ signer: auth.signer,
268
+ network: auth.network,
269
+ method,
270
+ path: url.pathname,
271
+ });
272
+ const headers = new Headers(init.headers);
273
+ authorization.headers.forEach((value, name) => headers.set(name, value));
274
+ return fetcher(url, { ...init, method, headers });
275
+ }
package/src/catalog.ts ADDED
@@ -0,0 +1,189 @@
1
+ // Copyright (c) Miso Labs, Inc.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // Composed catalog reads.
5
+ //
6
+ // `@misofm/protocol` exposes one-question reads: fetch an object by id, list the
7
+ // caps an address owns, resolve a share type to its work. Stitching several of
8
+ // those together to answer a PRODUCT question — "show me everything this artist
9
+ // administers" — is orchestration, and it lives here for the same reason the
10
+ // opinionated publish flows do: it picks a traversal strategy, decides how much
11
+ // to fetch, and chooses a concurrency/rate-limit tradeoff. Those are platform
12
+ // calls, not protocol facts.
13
+
14
+ import type { ClientWithCoreApi } from "@mysten/sui/client";
15
+ import type { SuiGraphQLClient } from "@mysten/sui/graphql";
16
+ import {
17
+ extractTypeParams2,
18
+ getOwnedRecordingAdminCaps,
19
+ getRecordingsByIds,
20
+ getReleaseById,
21
+ type Recording,
22
+ } from "@misofm/protocol";
23
+ import { getWorkAddressesByShareTypes } from "./read/works.ts";
24
+ import {
25
+ getCompositionCreditsByIds,
26
+ getRecordingCreditsByIds,
27
+ type CreditView,
28
+ type RecordingCreditsView,
29
+ } from "./credits.ts";
30
+
31
+ export interface GetReleaseTrackCreditsOptions {
32
+ /** Core Miso package that defines Composition and Recording. */
33
+ misoPackageId: string;
34
+ /** Package that defines the composition_credits extension key. */
35
+ compositionCreditsPackageId: string;
36
+ /** Package that defines the recording_credits extension key. */
37
+ recordingCreditsPackageId: string;
38
+ }
39
+
40
+ export interface ReleaseTrackCredits {
41
+ compositionCredits: CreditView[];
42
+ recordingCredits: RecordingCreditsView;
43
+ }
44
+
45
+ const EMPTY_RECORDING_CREDITS: RecordingCreditsView = {
46
+ credits: [],
47
+ primaryArtistIds: [],
48
+ featuredArtistIds: [],
49
+ };
50
+
51
+ /**
52
+ * Composition and recording credits for every track on a release, keyed by
53
+ * recording id.
54
+ *
55
+ * The read follows the chain's actual object graph instead of release credits:
56
+ * Release -> Recording -> Composition. Recording credit fields and recording
57
+ * type parameters are fetched together for every track. Composition addresses
58
+ * are then resolved concurrently, deduplicated, and all composition credit
59
+ * fields are fetched concurrently as well.
60
+ */
61
+ export async function getReleaseTrackCredits(
62
+ client: ClientWithCoreApi,
63
+ graphqlClient: SuiGraphQLClient,
64
+ releaseId: string,
65
+ options: GetReleaseTrackCreditsOptions,
66
+ ): Promise<Record<string, ReleaseTrackCredits>> {
67
+ const release = await getReleaseById(client, releaseId);
68
+ return getTrackCreditsByRecordingIds(
69
+ client,
70
+ graphqlClient,
71
+ release.tracks.map((track) => track.recordingId),
72
+ options,
73
+ );
74
+ }
75
+
76
+ /**
77
+ * Composition and recording credits for the supplied recordings, keyed by
78
+ * recording id. This form is intended for consumers that already loaded a
79
+ * release and do not need to fetch it again.
80
+ */
81
+ export async function getTrackCreditsByRecordingIds(
82
+ client: ClientWithCoreApi,
83
+ graphqlClient: SuiGraphQLClient,
84
+ recordingIdsInput: readonly string[],
85
+ options: GetReleaseTrackCreditsOptions,
86
+ ): Promise<Record<string, ReleaseTrackCredits>> {
87
+ const recordingIds = [...new Set(recordingIdsInput)];
88
+ if (recordingIds.length === 0) return {};
89
+
90
+ const [recordingCreditsById, recordingObjects] = await Promise.all([
91
+ getRecordingCreditsByIds(
92
+ client,
93
+ recordingIds,
94
+ options.recordingCreditsPackageId,
95
+ ),
96
+ client.core.getObjects({ objectIds: recordingIds }),
97
+ ]);
98
+ const recordingReads = recordingObjects.objects.map((object, index) => {
99
+ const recordingId = recordingIds[index]!;
100
+ if (object instanceof Error) throw object;
101
+ const [, compositionShareType] = extractTypeParams2(object.type);
102
+ return {
103
+ recordingId,
104
+ recordingCredits: recordingCreditsById[recordingId] ?? null,
105
+ compositionShareType,
106
+ };
107
+ });
108
+
109
+ const compositionShareTypes = [
110
+ ...new Set(recordingReads.map((read) => read.compositionShareType)),
111
+ ];
112
+ const addresses = await getWorkAddressesByShareTypes(
113
+ graphqlClient,
114
+ { compositions: compositionShareTypes, recordings: [] },
115
+ options.misoPackageId,
116
+ );
117
+ for (const shareType of compositionShareTypes) {
118
+ if (!addresses.compositions[shareType]) {
119
+ throw new Error(`Composition not found for share type: ${shareType}`);
120
+ }
121
+ }
122
+ const compositionIds = [
123
+ ...new Set(
124
+ Object.values(addresses.compositions).filter(
125
+ (id): id is string => id !== undefined,
126
+ ),
127
+ ),
128
+ ];
129
+ const compositionCreditsById = await getCompositionCreditsByIds(
130
+ client,
131
+ compositionIds,
132
+ options.compositionCreditsPackageId,
133
+ );
134
+
135
+ return Object.fromEntries(
136
+ recordingReads.map((read) => {
137
+ const compositionId = addresses.compositions[read.compositionShareType]!;
138
+ return [
139
+ read.recordingId,
140
+ {
141
+ compositionCredits: compositionCreditsById[compositionId] ?? [],
142
+ recordingCredits: read.recordingCredits ?? EMPTY_RECORDING_CREDITS,
143
+ },
144
+ ];
145
+ }),
146
+ );
147
+ }
148
+
149
+ export interface GetAdministeredRecordingsOptions {
150
+ /**
151
+ * @deprecated Resolution is batched; this option is retained for source
152
+ * compatibility and no longer affects request concurrency.
153
+ */
154
+ concurrency?: number;
155
+ }
156
+
157
+ /**
158
+ * Every `Recording` administered by `owner`.
159
+ *
160
+ * Three bounded stages: list the owner's `RecordingAdminCap`s, resolve every
161
+ * share type in one aliased GraphQL query, then batch-fetch the recordings.
162
+ */
163
+ export async function getAdministeredRecordings(
164
+ client: ClientWithCoreApi,
165
+ graphqlClient: SuiGraphQLClient,
166
+ owner: string,
167
+ misoPackageId: string,
168
+ options: GetAdministeredRecordingsOptions = {},
169
+ ): Promise<Recording[]> {
170
+ void options;
171
+ const caps = await getOwnedRecordingAdminCaps(client, owner, misoPackageId);
172
+ if (caps.length === 0) return [];
173
+ const addresses = await getWorkAddressesByShareTypes(
174
+ graphqlClient,
175
+ { compositions: [], recordings: caps.map((cap) => cap.shareType) },
176
+ misoPackageId,
177
+ );
178
+ const byId = await getRecordingsByIds(
179
+ client,
180
+ Object.values(addresses.recordings).filter(
181
+ (id): id is string => id !== undefined,
182
+ ),
183
+ );
184
+ return caps.flatMap((cap) => {
185
+ const id = addresses.recordings[cap.shareType];
186
+ const recording = id ? byId[id] : undefined;
187
+ return recording ? [recording] : [];
188
+ });
189
+ }