@feralfile/cli 2.6.0 → 2.8.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.
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.unpublishCommand = void 0;
7
+ const commander_1 = require("commander");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const feed_server_1 = require("./helpers/feed-server");
10
+ const prompt_1 = require("./helpers/prompt");
11
+ /**
12
+ * `ff-cli unpublish <id-or-url>` — delete a playlist this key owns from a feed.
13
+ *
14
+ * The delete is irreversible in a way most deletes are not: the feed tombstones the id, so the playlist
15
+ * cannot be restored and the id can never be reused. Hence the confirmation, which shows the title and
16
+ * server first, and defaults to no. `-y` skips it for scripts; without a terminal and without `-y` the
17
+ * command refuses rather than assuming consent.
18
+ */
19
+ exports.unpublishCommand = new commander_1.Command('unpublish')
20
+ .description('Delete a playlist from a feed server (signed delete; owner key required)')
21
+ .argument('<id-or-url>', 'Playlist id, slug, or feed URL')
22
+ .option('-s, --server <index>', 'Feed server index (use this if multiple servers configured)')
23
+ .option('-y, --yes', 'Skip the confirmation prompt')
24
+ .action(async (idOrUrl, options) => {
25
+ try {
26
+ console.log(chalk_1.default.blue('\nUnpublish playlist\n'));
27
+ const { getFeedConfig } = await import('../config.js');
28
+ const { unpublishPlaylist, fetchPlaylistForUnpublish } = await import('../utilities/playlist-unpublisher.js');
29
+ const feedConfig = getFeedConfig();
30
+ const selection = await (0, feed_server_1.selectFeedServer)(feedConfig.baseURLs, {
31
+ serverArg: options.server,
32
+ nonInteractive: !!options.yes,
33
+ });
34
+ if (!selection.ok) {
35
+ console.error(chalk_1.default.red(`\n${selection.error}`));
36
+ if (selection.detail) {
37
+ console.log(chalk_1.default.yellow(selection.detail));
38
+ }
39
+ console.log();
40
+ process.exit(1);
41
+ }
42
+ if (!options.yes) {
43
+ if (!process.stdin.isTTY) {
44
+ console.error(chalk_1.default.red('\nRefusing to delete without confirmation'));
45
+ console.log(chalk_1.default.yellow(' This session has no terminal to confirm on. Pass -y to proceed.\n'));
46
+ process.exit(1);
47
+ }
48
+ // Show what is about to go before asking. A bare id says nothing about which playlist it is, and
49
+ // the answer cannot be taken back. A lookup failure is not fatal here: the delete path repeats
50
+ // the fetch and reports it properly, so the prompt just falls back to the identifier.
51
+ let label = idOrUrl;
52
+ try {
53
+ const stored = await fetchPlaylistForUnpublish(idOrUrl, selection.url);
54
+ const title = typeof stored.title === 'string' ? stored.title : '';
55
+ label = title ? `"${title}" (${stored.id ?? idOrUrl})` : String(stored.id ?? idOrUrl);
56
+ }
57
+ catch {
58
+ label = idOrUrl;
59
+ }
60
+ console.log(chalk_1.default.yellow(` ${label}`));
61
+ console.log(chalk_1.default.dim(` Server: ${selection.url}`));
62
+ console.log(chalk_1.default.dim(' This cannot be undone; the id is tombstoned and cannot be reused.'));
63
+ console.log();
64
+ const prompt = (0, prompt_1.createPrompt)();
65
+ const confirmed = await (0, prompt_1.promptYesNo)(prompt.ask, 'Delete this playlist?', false);
66
+ prompt.close();
67
+ console.log();
68
+ if (!confirmed) {
69
+ console.log(chalk_1.default.dim('Cancelled.\n'));
70
+ return;
71
+ }
72
+ }
73
+ const result = await unpublishPlaylist(idOrUrl, selection.url);
74
+ if (result.success) {
75
+ console.log(chalk_1.default.green('Unpublished'));
76
+ if (result.playlistId) {
77
+ console.log(chalk_1.default.dim(` Playlist ID: ${result.playlistId}`));
78
+ }
79
+ if (result.slug) {
80
+ console.log(chalk_1.default.dim(` Slug: ${result.slug}`));
81
+ }
82
+ console.log(chalk_1.default.dim(` Server: ${result.feedServer}`));
83
+ if (result.message) {
84
+ console.log(chalk_1.default.dim(` Status: ${result.message}`));
85
+ }
86
+ console.log();
87
+ return;
88
+ }
89
+ console.error(chalk_1.default.red('\nUnpublish failed'));
90
+ if (result.error) {
91
+ console.error(chalk_1.default.red(` ${result.error}`));
92
+ }
93
+ if (result.message) {
94
+ console.log(chalk_1.default.yellow(`\n${result.message}`));
95
+ }
96
+ console.log();
97
+ process.exit(1);
98
+ }
99
+ catch (error) {
100
+ console.error(chalk_1.default.red('\nError:'), error.message);
101
+ process.exit(1);
102
+ }
103
+ });
@@ -1,3 +1,4 @@
1
+ "use strict";
1
2
  /**
2
3
  * DP1 Feed Fetcher
3
4
  * Utilities to fetch playlists from DP1 Feed API
@@ -0,0 +1,371 @@
1
+ "use strict";
2
+ /**
3
+ * Shared machinery for the feed's two owner-bound mutations: `PUT` (replace) and `DELETE`.
4
+ *
5
+ * The feed has no API key. Every mutating request is authorized by signatures the body carries, and for
6
+ * replace and delete those signatures must come from a key the *stored* resource already names as an
7
+ * owner (dp1-feed-v2 `docs/api_design.md`, "Authentication and authorization"). A create signs the
8
+ * document itself; a replace and a delete additionally sign a short-lived **intent** envelope, because an
9
+ * owner's document signatures are public via `GET` and could otherwise be replayed — the per-signature
10
+ * `ts` is not covered by the signature, so only a `created` inside a signed payload can bound replay.
11
+ *
12
+ * Both intents therefore share the same shape and the same failure modes, which is why they live here
13
+ * rather than in the publisher and unpublisher separately: the envelope, the freshness rule, the
14
+ * ownership rule and the error vocabulary are one contract with two verbs.
15
+ */
16
+ var __importDefault = (this && this.__importDefault) || function (mod) {
17
+ return (mod && mod.__esModule) ? mod : { "default": mod };
18
+ };
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.OWNER_ROLE = void 0;
21
+ exports.intentTimestamp = intentTimestamp;
22
+ exports.buildDeleteIntent = buildDeleteIntent;
23
+ exports.buildReplaceIntent = buildReplaceIntent;
24
+ exports.documentPayloadHash = documentPayloadHash;
25
+ exports.signIntent = signIntent;
26
+ exports.resolvePlaylistIdentifier = resolvePlaylistIdentifier;
27
+ exports.fetchStoredPlaylist = fetchStoredPlaylist;
28
+ exports.storedOwnerKeys = storedOwnerKeys;
29
+ exports.storedOwnership = storedOwnership;
30
+ exports.ownershipPreflight = ownershipPreflight;
31
+ exports.describeFeedMutationError = describeFeedMutationError;
32
+ exports.asPlaylist = asPlaylist;
33
+ const axios_1 = __importDefault(require("axios"));
34
+ // playlist-signer is still CommonJS; require keeps the interop simple.
35
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
36
+ const { normalizeSigningKeyToBase64Pkcs8 } = require('./playlist-signer');
37
+ /**
38
+ * DP-1 signature role that carries ownership of a playlist.
39
+ *
40
+ * Mirrors the feed's owner role for playlists (channels use `publisher`, which ff-cli does not publish).
41
+ * Intents are always signed in this role: only an owner signature authorizes a replace or a delete, so
42
+ * signing an intent under any other role produces a body the feed answers with `403`.
43
+ */
44
+ exports.OWNER_ROLE = 'curator';
45
+ /**
46
+ * Whole-second RFC 3339 timestamp for an intent's `created`.
47
+ *
48
+ * The feed compares this against its own clock and rejects anything outside `auth.intent_max_clock_skew`
49
+ * (default 5 minutes) with `400 invalid_timestamp`. Milliseconds are stripped to match the form the
50
+ * playlist signer already emits, so both timestamps in a request read alike.
51
+ */
52
+ function intentTimestamp() {
53
+ return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
54
+ }
55
+ /**
56
+ * Build the DELETE body's unsigned half.
57
+ *
58
+ * `id` and `slug` must match the stored row exactly — the feed compares both and answers `400
59
+ * bad_request` on any disagreement — which is why callers read them from a `GET` rather than from
60
+ * whatever the user typed.
61
+ */
62
+ function buildDeleteIntent(id, slug, created) {
63
+ return { action: 'delete', target: { type: 'playlist', id, slug }, created };
64
+ }
65
+ /**
66
+ * Build the `authorization` half of a PUT body.
67
+ *
68
+ * `payloadHash` binds the intent to the exact document being installed, so a captured intent cannot be
69
+ * replayed to install different bytes. It is the DP-1 signing digest of the accompanying document.
70
+ */
71
+ function buildReplaceIntent(id, slug, payloadHash, created) {
72
+ return { action: 'replace', target: { type: 'playlist', id, slug }, payloadHash, created };
73
+ }
74
+ /**
75
+ * DP-1 signing digest of a document, in the `sha256:<hex>` form the feed's `payloadHash` expects.
76
+ *
77
+ * dp1-js strips `signature`/`signatures` and JCS-canonicalizes internally, so the full signed document
78
+ * is passed through unchanged; computing it any other way risks diverging from the digest the document's
79
+ * own signatures were taken over.
80
+ */
81
+ async function documentPayloadHash(document) {
82
+ const dp1 = await import('dp1-js');
83
+ return dp1.PayloadHashString(Buffer.from(JSON.stringify(document)));
84
+ }
85
+ /**
86
+ * Sign an intent with the playlist signing key, in the owner role.
87
+ *
88
+ * The signature covers the intent bytes with `signatures` stripped and JCS-canonicalized — the same
89
+ * payload rule DP-1 uses for documents — so the unsigned intent object is passed here as-is and the
90
+ * caller attaches the result afterwards. `ts` is set to the intent's own `created` so the two timestamps
91
+ * a reviewer sees in a request body agree.
92
+ */
93
+ async function signIntent(intent, privateKeyMaterial) {
94
+ const dp1 = await import('dp1-js');
95
+ const normalizedKey = normalizeSigningKeyToBase64Pkcs8(privateKeyMaterial);
96
+ return (await dp1.SignMultiEd25519(Buffer.from(JSON.stringify(intent)), normalizedKey, exports.OWNER_ROLE, intent.created));
97
+ }
98
+ /**
99
+ * Reduce user input to the path segment the feed resolves.
100
+ *
101
+ * Operators copy playlist URLs out of a browser far more often than they copy bare UUIDs, and the feed's
102
+ * `{id}` parameter accepts either an id or a slug. Anything that parses as an HTTP(S) URL contributes
103
+ * its last non-empty path segment; everything else is passed through trimmed.
104
+ */
105
+ function resolvePlaylistIdentifier(input) {
106
+ const trimmed = input.trim();
107
+ if (!/^https?:\/\//i.test(trimmed)) {
108
+ return trimmed;
109
+ }
110
+ let parsed;
111
+ try {
112
+ parsed = new URL(trimmed);
113
+ }
114
+ catch {
115
+ return trimmed;
116
+ }
117
+ const segments = parsed.pathname.split('/').filter((segment) => segment.length > 0);
118
+ return segments.length > 0 ? decodeURIComponent(segments[segments.length - 1]) : trimmed;
119
+ }
120
+ /**
121
+ * Fetch the stored playlist a mutation will target.
122
+ *
123
+ * Both mutations need the stored copy before they can be built at all: the delete intent must name the
124
+ * stored `slug`, and a replace must prove its identity fields still equal the stored ones. Reading the
125
+ * owners from here rather than from the local file is the point — a local document can claim any
126
+ * `curators[]`, but only the stored set decides who may write.
127
+ */
128
+ async function fetchStoredPlaylist(feedServerUrl, identifier) {
129
+ const response = await axios_1.default.get(`${feedServerUrl}/playlists/${encodeURIComponent(identifier)}`, {
130
+ headers: { Accept: 'application/json' },
131
+ timeout: 30000,
132
+ });
133
+ const stored = response.data;
134
+ if (!stored || typeof stored !== 'object') {
135
+ throw new Error('Feed returned no playlist document');
136
+ }
137
+ return stored;
138
+ }
139
+ /** Owner `did:key`s the stored playlist declares, in declaration order. */
140
+ function storedOwnerKeys(stored) {
141
+ const curators = Array.isArray(stored.curators) ? stored.curators : [];
142
+ return curators
143
+ .map((curator) => (typeof curator?.key === 'string' ? curator.key.trim() : ''))
144
+ .filter((key) => key.length > 0);
145
+ }
146
+ /**
147
+ * Resolve who can actually authorize a mutation on a stored playlist.
148
+ *
149
+ * Each candidate signature is verified individually rather than through a whole-envelope check: a stored
150
+ * document legitimately carries entries this CLI does not depend on — the feed's own `feed` signature,
151
+ * and any co-curator's — and one unverifiable stranger among them must not invalidate a proof that is
152
+ * itself sound. Verification is over the stored bytes with `signatures` stripped and JCS-canonicalized,
153
+ * so re-serializing the parsed document here is safe: canonicalization removes key-order and whitespace
154
+ * differences before the digest is taken.
155
+ *
156
+ * @param stored - The playlist as the feed serves it
157
+ * @returns Declared and proven owner keys
158
+ */
159
+ async function storedOwnership(stored) {
160
+ const declared = storedOwnerKeys(stored);
161
+ const signatures = Array.isArray(stored.signatures)
162
+ ? (stored.signatures ?? []).filter(Boolean)
163
+ : [];
164
+ if (declared.length === 0 || signatures.length === 0) {
165
+ return { declared, proven: [] };
166
+ }
167
+ const dp1 = await import('dp1-js');
168
+ const raw = Buffer.from(JSON.stringify(stored));
169
+ const proven = [];
170
+ for (const signature of signatures) {
171
+ const kid = typeof signature?.kid === 'string' ? signature.kid.trim() : '';
172
+ if (signature?.role !== exports.OWNER_ROLE || !declared.includes(kid) || proven.includes(kid)) {
173
+ continue;
174
+ }
175
+ try {
176
+ dp1.VerifyMultiSignature(raw, signature);
177
+ proven.push(kid);
178
+ }
179
+ catch {
180
+ // An entry that does not verify is not proof. It is also not this command's business to report:
181
+ // a stale or tampered signature on someone else's key changes nothing about whether the
182
+ // configured key can act, and the answer below is derived from the set that survived.
183
+ }
184
+ }
185
+ return { declared, proven };
186
+ }
187
+ /**
188
+ * Refuse locally when the configured key cannot authorize a mutation on the stored playlist.
189
+ *
190
+ * The feed's own answer to every case here is a bare `403 forbidden`, which says nothing about which
191
+ * identity was offered, which ones would have worked, or whether the problem is the declaration or the
192
+ * proof. Separating them costs no request beyond the `GET` the mutation already had to make, and the
193
+ * three answers point in genuinely different directions: switch keys, nothing can ever work, or the
194
+ * declaration is right and the signature that would back it was never made.
195
+ *
196
+ * @param stored - The playlist as the feed serves it
197
+ * @param signerDidKey - `did:key` the configured signing key will assert
198
+ * @param action - Which mutation is being authorized, for the wording
199
+ * @returns A failure to report, or `null` when the configured key is a proven owner.
200
+ */
201
+ async function ownershipPreflight(stored, signerDidKey, action) {
202
+ const { declared, proven } = await storedOwnership(stored);
203
+ if (proven.includes(signerDidKey)) {
204
+ return null;
205
+ }
206
+ const verb = action === 'delete' ? 'delete' : 'replace';
207
+ // A playlist with no declared curators is not "owned by someone else" — it is owned by nobody, and
208
+ // nothing can ever authorize a write to it. Telling that operator to switch keys sends them looking
209
+ // for one that does not exist; this is the state a playlist published without any curator declaration
210
+ // is frozen in, and it is exactly what the publish-time curator check now prevents.
211
+ if (declared.length === 0) {
212
+ return {
213
+ error: `This playlist declares no owners, so no key can ${verb} it.`,
214
+ message: `The feed derives ownership from the stored document's curators[], and this one is empty — the\n` +
215
+ ` shape a playlist published without an owner-role signature is frozen in. No signature can\n` +
216
+ ` authorize a ${verb}, including yours (${signerDidKey}).\n` +
217
+ ` Nothing can repair it: the owner set is immutable, and only an owner could change it. Publish\n` +
218
+ ` a corrected playlist under a new id instead.`,
219
+ };
220
+ }
221
+ // Declared, but nobody proved it. Same terminal outcome as the case above, different cause and
222
+ // therefore a different message: curators[] is correct and the owner-role signature that would back
223
+ // it was never made. This is the state of everything published from 2.5.0 with the default `agent`
224
+ // role, so it is the case an operator is most likely to meet on an older playlist.
225
+ if (proven.length === 0) {
226
+ return {
227
+ error: `No key has proved ownership of this playlist, so none can ${verb} it.`,
228
+ message: `A key counts as an owner only when it is named in curators[] AND signed the document as\n` +
229
+ ` "${exports.OWNER_ROLE}". This playlist names ${declared.length === 1 ? 'a curator' : `${declared.length} curators`} and carries no valid ${exports.OWNER_ROLE} signature\n` +
230
+ ` from ${declared.length === 1 ? 'that key' : 'any of them'} — the shape of a playlist published while the default signing role was\n` +
231
+ ` "agent".\n` +
232
+ ` Declared:\n` +
233
+ `${declared.map((key) => ` ${key}`).join('\n')}\n` +
234
+ ` Nothing can repair it, including holding one of those keys: adding the missing signature\n` +
235
+ ` would be a replace, and a replace needs the very proof that is missing. Publish a corrected\n` +
236
+ ` playlist under a new id instead.`,
237
+ };
238
+ }
239
+ // Declared, and someone proved it — but not this key. Distinct from the case above: an owner exists,
240
+ // it is simply not the one configured here.
241
+ if (declared.includes(signerDidKey)) {
242
+ return {
243
+ error: `Your key is declared on this playlist but never signed it as ${exports.OWNER_ROLE}, so it cannot ${verb} it.`,
244
+ message: `The feed treats a declared key as an owner only once it has also signed the stored document\n` +
245
+ ` as "${exports.OWNER_ROLE}". Yours is named in curators[] but carries no such signature, so an intent\n` +
246
+ ` signed with it would be refused.\n` +
247
+ ` Your configured identity:\n` +
248
+ ` ${signerDidKey}\n` +
249
+ ` Keys that have proved ownership:\n` +
250
+ `${proven.map((key) => ` ${key}`).join('\n')}\n` +
251
+ ` Point playlist.privateKey at one of those (confirm any key's identity with\n` +
252
+ ` "ff-cli status --key <private key>"). Your own declaration cannot be upgraded after the fact:\n` +
253
+ ` the proof would have to be a signature over the document as published.`,
254
+ };
255
+ }
256
+ return {
257
+ error: `The configured signing key is not an owner of this playlist, so it cannot ${verb} it.`,
258
+ message: `Only a key the stored playlist names in curators[] AND that signed it as "${exports.OWNER_ROLE}" can\n` +
259
+ ` authorize a ${verb}; the feed derives ownership from the stored document, not from a local copy.\n` +
260
+ ` Your configured identity:\n` +
261
+ ` ${signerDidKey}\n` +
262
+ ` Keys that have proved ownership:\n` +
263
+ `${proven.map((key) => ` ${key}`).join('\n')}\n` +
264
+ ` Point playlist.privateKey at a key listed above (confirm any key's identity with\n` +
265
+ ` "ff-cli status --key <private key>"). Ownership cannot be granted after the fact: the owner set\n` +
266
+ ` is immutable, so a playlist signed by the wrong key stays that way.`,
267
+ };
268
+ }
269
+ /** Feed error code carried in an error response body, when there is one. */
270
+ function feedErrorCode(error) {
271
+ const data = error.response?.data;
272
+ const code = data?.error ?? data?.code;
273
+ return typeof code === 'string' ? code : '';
274
+ }
275
+ /** Human-readable detail the feed supplied, if any. */
276
+ function feedErrorDetail(error) {
277
+ const data = error.response?.data;
278
+ if (typeof data?.message === 'string' && data.message.trim().length > 0) {
279
+ return data.message.trim();
280
+ }
281
+ return '';
282
+ }
283
+ /**
284
+ * Translate a feed mutation failure into the CLI's diagnosis-plus-remedy shape.
285
+ *
286
+ * The feed's status codes each mean something specific for an owner-bound write, and the raw JSON body
287
+ * does not say which of them the operator hit — `400` alone covers both a stale intent and a target
288
+ * mismatch. Mapping them here keeps `unpublish` and `publish --replace` answering in the same
289
+ * vocabulary, and keeps that vocabulary next to the contract it mirrors.
290
+ *
291
+ * The codes handled are exactly those dp1-feed-v2 documents for these routes; anything else falls
292
+ * through with the server's own words rather than being guessed at.
293
+ */
294
+ function describeFeedMutationError(error, action) {
295
+ const axiosError = error;
296
+ const status = axiosError.response?.status;
297
+ const code = feedErrorCode(axiosError);
298
+ const detail = feedErrorDetail(axiosError);
299
+ const verb = action === 'delete' ? 'Delete' : 'Replace';
300
+ const suffix = detail ? `\n Feed said: ${detail}` : '';
301
+ if (status === undefined) {
302
+ return { error: `${verb} failed: ${axiosError.message}` };
303
+ }
304
+ if (status === 401) {
305
+ return {
306
+ error: `${verb} refused: the feed saw no signatures on the request.`,
307
+ message: `Every mutating request is authorized by the signatures in its body — there is no API key.\n` +
308
+ ` This usually means no signing key is configured: run "ff-cli status" to check, and\n` +
309
+ ` "ff-cli setup" to generate one.${suffix}`,
310
+ };
311
+ }
312
+ // A 403 that reaches here has already passed the local ownership proof: the configured key was found
313
+ // in the stored curators[] with a valid curator-role signature over the stored bytes. Repeating "your
314
+ // key is not declared" would therefore be a lie, and it is the wrong place to send someone — the
315
+ // remaining causes are the feed disagreeing about the stored document, or a replace touching the
316
+ // owner set. Say that the feed refused, and that the local check disagreed.
317
+ if (status === 403) {
318
+ return {
319
+ error: `${verb} refused by the feed: it did not accept the signing key as an owner.`,
320
+ message: `The local check disagreed — the configured key is named in the stored playlist's curators[]\n` +
321
+ ` and carries a valid "${exports.OWNER_ROLE}" signature over the stored document — so this is the feed's\n` +
322
+ ` own judgement, not a missing declaration.\n` +
323
+ (action === 'replace'
324
+ ? ` For a replace the usual cause is the submitted document changing the owner set: curators[]\n` +
325
+ ` is immutable, and altering it is refused as a forbidden write rather than as a bad field.\n`
326
+ : '') +
327
+ ` Otherwise the stored document changed since it was read; run the command again, and report it\n` +
328
+ ` if it persists.${suffix}`,
329
+ };
330
+ }
331
+ if (status === 400 && code === 'invalid_timestamp') {
332
+ return {
333
+ error: `${verb} refused: the signed intent was outside the feed's freshness window.`,
334
+ message: `An intent is valid for about five minutes, and the window is wall-clock, so a machine whose\n` +
335
+ ` clock has drifted fails every attempt. Check the system clock, then run the command again — a\n` +
336
+ ` new intent is signed each time.${suffix}`,
337
+ };
338
+ }
339
+ if (status === 400) {
340
+ return {
341
+ error: `${verb} refused: the request did not match the stored playlist.`,
342
+ message: action === 'delete'
343
+ ? ` The delete intent must name the stored id and slug exactly. They are read from the feed\n` +
344
+ ` immediately before the request, so this means the playlist changed in between: run the\n` +
345
+ ` command again.${suffix}`
346
+ : ` A replace may not change identity: id, slug, and created must equal the stored document's,\n` +
347
+ ` and every signature must verify. Re-read the stored copy and re-sign from it.${suffix}`,
348
+ };
349
+ }
350
+ if (status === 404) {
351
+ return {
352
+ error: `${verb} refused: the feed has no such playlist.`,
353
+ message: `Either the id or slug is wrong for this feed, or the playlist was already deleted. Deleted ids\n` +
354
+ ` are tombstoned and never reused, so a deleted playlist cannot be restored under the same id.${suffix}`,
355
+ };
356
+ }
357
+ if (status === 409) {
358
+ return {
359
+ error: `${verb} refused: the playlist changed between authorization and the write.`,
360
+ message: `Another write landed first. Re-read the playlist and run the command again.${suffix}`,
361
+ };
362
+ }
363
+ const body = axiosError.response?.data
364
+ ? JSON.stringify(axiosError.response.data)
365
+ : axiosError.message;
366
+ return { error: `${verb} failed with status ${status}: ${body}` };
367
+ }
368
+ /** Narrow a stored playlist to a `Playlist` for the shared verification paths. */
369
+ function asPlaylist(stored) {
370
+ return stored;
371
+ }
@@ -1,40 +1,8 @@
1
+ "use strict";
1
2
  /**
2
3
  * Function Calling Layer
3
4
  * These are the actual implementations called by AI orchestrator via function calling
4
5
  */
5
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
- if (k2 === undefined) k2 = k;
7
- var desc = Object.getOwnPropertyDescriptor(m, k);
8
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
- desc = { enumerable: true, get: function() { return m[k]; } };
10
- }
11
- Object.defineProperty(o, k2, desc);
12
- }) : (function(o, m, k, k2) {
13
- if (k2 === undefined) k2 = k;
14
- o[k2] = m[k];
15
- }));
16
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
- Object.defineProperty(o, "default", { enumerable: true, value: v });
18
- }) : function(o, v) {
19
- o["default"] = v;
20
- });
21
- var __importStar = (this && this.__importStar) || (function () {
22
- var ownKeys = function(o) {
23
- ownKeys = Object.getOwnPropertyNames || function (o) {
24
- var ar = [];
25
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
- return ar;
27
- };
28
- return ownKeys(o);
29
- };
30
- return function (mod) {
31
- if (mod && mod.__esModule) return mod;
32
- var result = {};
33
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
- __setModuleDefault(result, mod);
35
- return result;
36
- };
37
- })();
38
6
  const chalk = require('chalk');
39
7
  const playlistBuilder = require('./playlist-builder');
40
8
  const ff1Device = require('./ff1-device');
@@ -162,7 +130,7 @@ async function verifyPlaylist(params) {
162
130
  }
163
131
  logger.verbose(chalk.cyan('\nValidate playlist'));
164
132
  // Dynamic import to avoid circular dependency
165
- const playlistVerifier = await Promise.resolve().then(() => __importStar(require('./playlist-verifier')));
133
+ const playlistVerifier = await import('./playlist-verifier.js');
166
134
  const validate = playlistVerifier.validatePlaylist ||
167
135
  (playlistVerifier.default && playlistVerifier.default.validatePlaylist) ||
168
136
  playlistVerifier.default;
@@ -226,7 +194,7 @@ async function verifyAddresses(params) {
226
194
  };
227
195
  }
228
196
  // Dynamic import to avoid circular dependency
229
- const addressValidator = await Promise.resolve().then(() => __importStar(require('./address-validator')));
197
+ const addressValidator = await import('./address-validator.js');
230
198
  const validateAddresses = addressValidator.validateAddresses ||
231
199
  (addressValidator.default && addressValidator.default.validateAddresses) ||
232
200
  addressValidator.default;
@@ -268,7 +236,7 @@ async function verifyAddresses(params) {
268
236
  * }
269
237
  */
270
238
  async function getConfiguredDevices() {
271
- const configModule = await Promise.resolve().then(() => __importStar(require('../config')));
239
+ const configModule = await import('../config.js');
272
240
  const getFF1DeviceConfig = configModule.getFF1DeviceConfig ||
273
241
  (configModule.default && configModule.default.getFF1DeviceConfig) ||
274
242
  configModule.default;
@@ -1,40 +1,8 @@
1
+ "use strict";
1
2
  /**
2
3
  * Utilities - Actual function implementations
3
4
  * Contains the real logic for querying NFTs and building playlists
4
5
  */
5
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
- if (k2 === undefined) k2 = k;
7
- var desc = Object.getOwnPropertyDescriptor(m, k);
8
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
- desc = { enumerable: true, get: function() { return m[k]; } };
10
- }
11
- Object.defineProperty(o, k2, desc);
12
- }) : (function(o, m, k, k2) {
13
- if (k2 === undefined) k2 = k;
14
- o[k2] = m[k];
15
- }));
16
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
- Object.defineProperty(o, "default", { enumerable: true, value: v });
18
- }) : function(o, v) {
19
- o["default"] = v;
20
- });
21
- var __importStar = (this && this.__importStar) || (function () {
22
- var ownKeys = function(o) {
23
- ownKeys = Object.getOwnPropertyNames || function (o) {
24
- var ar = [];
25
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
- return ar;
27
- };
28
- return ownKeys(o);
29
- };
30
- return function (mod) {
31
- if (mod && mod.__esModule) return mod;
32
- var result = {};
33
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
- __setModuleDefault(result, mod);
35
- return result;
36
- };
37
- })();
38
6
  const chalk = require('chalk');
39
7
  const nftIndexer = require('./nft-indexer');
40
8
  const feedFetcher = require('./feed-fetcher');
@@ -400,7 +368,7 @@ async function buildDP1Playlist(items, title, slug) {
400
368
  * @returns {Promise<Object>} Result
401
369
  */
402
370
  async function sendToDevice(playlist, deviceName) {
403
- const playlistVerifier = await Promise.resolve().then(() => __importStar(require('./playlist-verifier')));
371
+ const playlistVerifier = await import('./playlist-verifier.js');
404
372
  const verifyPlaylist = playlistVerifier.verifyPlaylist ||
405
373
  (playlistVerifier.default && playlistVerifier.default.verifyPlaylist) ||
406
374
  playlistVerifier.default;