@antiphony/shared 0.3.0 → 0.5.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 (45) hide show
  1. package/README.md +4 -4
  2. package/dist/cjs/api-codecs.cjs +149 -142
  3. package/dist/cjs/api-codecs.d.cts +157 -68
  4. package/dist/cjs/index.cjs +187 -192
  5. package/dist/cjs/index.d.cts +310 -7
  6. package/dist/cjs/nsid.cjs +6 -9
  7. package/dist/cjs/nsid.d.cts +7 -8
  8. package/dist/cjs/types/audio.cjs +133 -119
  9. package/dist/cjs/types/audio.d.cts +343 -199
  10. package/dist/cjs/types/blob.cjs +5 -9
  11. package/dist/cjs/types/blob.d.cts +21 -21
  12. package/dist/cjs/types/records.cjs +0 -45
  13. package/dist/cjs/types/records.d.cts +1 -103
  14. package/dist/esm/api-codecs.d.ts +187 -98
  15. package/dist/esm/api-codecs.js +4 -5
  16. package/dist/esm/chunk-D655OH2I.js +35 -0
  17. package/dist/esm/chunk-F5ZKFJL2.js +39 -0
  18. package/dist/esm/{chunk-6LROFBKK.js → chunk-SBYNIQRZ.js} +7 -9
  19. package/dist/esm/chunk-SMK4OZNU.js +17 -0
  20. package/dist/esm/chunk-TVOXIQKF.js +294 -0
  21. package/dist/esm/index.d.ts +310 -7
  22. package/dist/esm/index.js +5 -7
  23. package/dist/esm/nsid.d.ts +7 -8
  24. package/dist/esm/nsid.js +1 -1
  25. package/dist/esm/types/audio.d.ts +420 -276
  26. package/dist/esm/types/audio.js +3 -4
  27. package/dist/esm/types/blob.d.ts +21 -21
  28. package/dist/esm/types/blob.js +1 -1
  29. package/dist/esm/types/records.d.ts +1 -103
  30. package/dist/esm/types/records.js +1 -1
  31. package/package.json +94 -26
  32. package/dist/cjs/types/api.cjs +0 -166
  33. package/dist/cjs/types/api.d.cts +0 -242
  34. package/dist/cjs/types/views.cjs +0 -179
  35. package/dist/cjs/types/views.d.cts +0 -726
  36. package/dist/esm/chunk-5EHV73BA.js +0 -20
  37. package/dist/esm/chunk-F7IHVM34.js +0 -144
  38. package/dist/esm/chunk-FDM5FDN4.js +0 -46
  39. package/dist/esm/chunk-IHIBHQBJ.js +0 -78
  40. package/dist/esm/chunk-R6SBR3IG.js +0 -97
  41. package/dist/esm/chunk-Y6HNNRVD.js +0 -12
  42. package/dist/esm/types/api.d.ts +0 -242
  43. package/dist/esm/types/api.js +0 -4
  44. package/dist/esm/types/views.d.ts +0 -726
  45. package/dist/esm/types/views.js +0 -3
@@ -6,18 +6,15 @@ var zod = require('zod');
6
6
  var BlobRefSchema = zod.z.object({
7
7
  /** Discriminator for AT Protocol type system */
8
8
  $type: zod.z.literal("blob"),
9
- /** Content Identifier (CID) or URL pointing to the blob */
10
- ref: zod.z.string(),
9
+ /** IPLD link to the blob bytes: the content CID. */
10
+ ref: zod.z.object({
11
+ $link: zod.z.string().min(1)
12
+ }),
11
13
  /** MIME type of the blob (e.g., 'audio/webm') */
12
14
  mimeType: zod.z.string(),
13
15
  /** Size of the blob in bytes */
14
- size: zod.z.number()
16
+ size: zod.z.number().int().min(0)
15
17
  });
16
- function httpsUrl() {
17
- return zod.z.string().trim().url().refine((u) => /^https?:\/\//i.test(u), {
18
- message: "URL must use the http or https scheme"
19
- });
20
- }
21
18
  var FirestoreTimestampSchema = zod.z.union([
22
19
  zod.z.custom((data) => {
23
20
  return data && typeof data === "object" && (typeof data.toDate === "function" || "seconds" in data && "nanoseconds" in data);
@@ -48,118 +45,86 @@ var FirestoreTimestampSchema = zod.z.union([
48
45
  }
49
46
  return date;
50
47
  });
48
+ var ProcessingStageStatusSchema = zod.z.enum(["pending", "ready", "failed", "skipped"]);
49
+ var PROCESSING_STAGES = ["denoise", "trim", "transcribe", "waveform"];
50
+ zod.z.enum(PROCESSING_STAGES);
51
51
  zod.z.object({
52
- /** Unique Firebase UID */
53
- id: zod.z.string(),
54
- /** Public handle (e.g. @brad). Optional for Lite Users. */
55
- handle: zod.z.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/).nullable().optional(),
56
- /** User stated intent (e.g. "Podcaster", "Listener") */
57
- usageIntent: zod.z.string().nullable().optional(),
58
- /** Domain for federated handle support. */
59
- domain: zod.z.string().default("antiphony.dev"),
52
+ transcribe: zod.z.boolean().optional(),
53
+ denoise: zod.z.boolean().optional(),
54
+ trim: zod.z.boolean().optional(),
55
+ waveform: zod.z.boolean().optional(),
60
56
  /**
61
- * Display Name (e.g. "Brad Thorson"). Nullable: Firestore stores `null`
62
- * when the user clears this field via the settings form, and the schema
63
- * must match storage reality or `UserRecordSchema.parse` (in
64
- * `getUserRecordByUid`) will throw.
57
+ * Whether a completed byte-mutating stage should invalidate and recompute
58
+ * the derived artifacts that describe the old audio. Defaults to **true**
59
+ * a transcript of superseded audio is wrong, not merely stale.
60
+ *
61
+ * `false` opts out, for an app that would rather keep the existing
62
+ * transcript than pay to regenerate it. It does NOT name a stage, so a
63
+ * request carrying only `reprocess` requests no work.
65
64
  */
66
- displayName: zod.z.string().max(50).nullable().optional(),
67
- /** Short bio/description — nullable for the same reason as displayName. */
68
- bio: zod.z.string().max(160).nullable().optional(),
69
- /** URL to avatar image — nullable for the same reason as displayName. */
70
- avatarUrl: zod.z.string().url().nullable().optional(),
71
- /** Optional personal website surfaced on the public profile. */
72
- website: httpsUrl().nullable().optional(),
73
- /** Up to 5 additional public links (label + URL) shown under the bio. */
74
- links: zod.z.array(zod.z.object({
75
- label: zod.z.string().min(1).max(40),
76
- url: httpsUrl()
77
- })).max(5).optional(),
78
- /** When true and a Bluesky identity is linked, surfaces it on the public profile. */
79
- showBlueskyPublicly: zod.z.boolean().optional(),
80
- /** Server timestamp of creation */
81
- createdAt: FirestoreTimestampSchema,
82
- /** Individual account tier — free or creator_pro */
83
- tier: zod.z.enum(["free", "creator_pro"]).default("free"),
84
- /** Account status. Deactivated accounts retain data but are excluded from lookups. */
85
- status: zod.z.enum(["active", "deactivated"]).default("active"),
86
- /** Timestamp when the account was deactivated (soft deleted) */
87
- deactivatedAt: FirestoreTimestampSchema.optional()
65
+ reprocess: zod.z.boolean().optional()
88
66
  });
89
- var ProfileViewBasicSchema = zod.z.object({
90
- id: zod.z.string(),
91
- handle: zod.z.string().nullable().optional(),
92
- // `displayName` and `bio` are `.nullable()` — Firestore stores `null` for
93
- // empty values on these fields (see users-dependencies.ts), and Zod's
94
- // `.optional()` alone rejects `null`. Consumers already use truthy
95
- // checks / `??` / `||`, so widening the type to include `null` is safe.
96
- displayName: zod.z.string().nullable().optional(),
97
- avatarUrl: zod.z.string().nullable().optional(),
98
- bio: zod.z.string().nullable().optional(),
99
- /** Personal website link surfaced on the public profile. */
100
- website: zod.z.string().nullable().optional(),
101
- /** Public links (label + URL) shown under the bio. */
102
- links: zod.z.array(zod.z.object({
103
- label: zod.z.string(),
104
- url: zod.z.string()
105
- })).optional(),
106
- /**
107
- * AT Protocol identity, surfaced on the public profile only when the user
108
- * opts in (`UserRecord.showBlueskyPublicly === true`). Projection happens
109
- * in the user dependency layer; this schema simply allows the field.
110
- */
111
- bluesky: zod.z.object({
112
- handle: zod.z.string(),
113
- did: zod.z.string()
114
- }).optional(),
115
- stats: zod.z.object({
116
- followers: zod.z.number().default(0),
117
- following: zod.z.number().default(0),
118
- prompts: zod.z.number().default(0)
119
- }).optional(),
120
- badges: zod.z.array(zod.z.string()).optional(),
121
- isVerified: zod.z.boolean().optional(),
122
- createdAt: FirestoreTimestampSchema.optional()
67
+ var ProcessingStageMapSchema = zod.z.object({
68
+ transcribe: ProcessingStageStatusSchema.optional(),
69
+ denoise: ProcessingStageStatusSchema.optional(),
70
+ trim: ProcessingStageStatusSchema.optional(),
71
+ waveform: ProcessingStageStatusSchema.optional()
123
72
  });
124
- var ProfileViewDetailedSchema = ProfileViewBasicSchema.extend({
125
- /** AT Protocol Identity link */
126
- bluesky: zod.z.object({
127
- handle: zod.z.string(),
128
- did: zod.z.string()
129
- }).optional(),
130
- usageIntent: zod.z.string().nullable().optional()
73
+ var ResolvedProcessingSchema = ProcessingStageMapSchema.extend({
74
+ reprocess: zod.z.boolean().optional()
131
75
  });
132
- var ProfileViewSelfSchema = ProfileViewDetailedSchema.extend({
133
- phoneNumber: zod.z.string().nullable().optional(),
134
- email: zod.z.string().optional(),
135
- lastSeenAt: FirestoreTimestampSchema.optional(),
136
- lastActiveAt: FirestoreTimestampSchema.optional(),
137
- unreadReplyCount: zod.z.number().default(0),
138
- newReplierCount: zod.z.number().default(0),
76
+ var ProcessingStateSchema = ResolvedProcessingSchema.extend({
139
77
  /**
140
- * Account tier from UserRecord surfaced on the self profile so the
141
- * client can gate paid features. Optional for legacy docs without the
142
- * field; consumers should treat missing as `'free'`.
78
+ * Content CID of the processed audio variant the composed output of every
79
+ * byte-mutating stage that has completed. The record's own
80
+ * `embed.audio.ref.$link` stays the ORIGINAL CID (immutable content
81
+ * address); only the read-time view swaps playback to this variant.
143
82
  */
144
- tier: zod.z.enum(["free", "creator_pro"]).optional(),
83
+ processedBlobCid: zod.z.string().optional(),
145
84
  /**
146
- * Surfaces the linked Bluesky identity (handle + DID) on the public profile
147
- * when true. Persisted on UserRecord; exposed in self/detailed views so the
148
- * settings form can render the toggle's current state.
85
+ * MIME type of the processed variant. Present because providers may
86
+ * TRANSCODE the ElevenLabs Voice Isolator returns MP3 regardless of what
87
+ * it is given so the variant's type cannot be assumed to match
88
+ * `embed.audio.mimeType`. Anything reading the variant's bytes must use
89
+ * this, not the embed's.
149
90
  */
150
- showBlueskyPublicly: zod.z.boolean().optional(),
151
- settings: zod.z.object({
152
- notifications: zod.z.boolean().optional(),
153
- theme: zod.z.string().optional()
154
- }).optional()
155
- });
156
- ProfileViewSelfSchema.extend({
157
- blockedUsers: zod.z.array(zod.z.string()).optional(),
158
- followers: zod.z.array(zod.z.string()).optional(),
159
- following: zod.z.array(zod.z.string()).optional(),
160
- reportCount: zod.z.number().optional(),
161
- isBanned: zod.z.boolean().optional()
91
+ processedMimeType: zod.z.string().optional(),
92
+ /**
93
+ * Duration of the processed variant, when a byte-mutating stage changed it
94
+ * (i.e. trim). Absent when the variant's duration matches the original.
95
+ */
96
+ processedDurationMs: zod.z.number().int().min(0).optional(),
97
+ /**
98
+ * Peaks for the processed variant, once the `waveform` stage completes.
99
+ * Same normalization and bounds as `embed.waveform` (0–100, max 1000), so
100
+ * a view can never carry a larger payload than the record allows.
101
+ */
102
+ waveformPeaks: zod.z.array(zod.z.number().int().min(0).max(100)).max(1e3).optional(),
103
+ /**
104
+ * When the current runner's exclusive claim on this post expires.
105
+ *
106
+ * Queue delivery is at-least-once, so the same job can arrive twice and
107
+ * run CONCURRENTLY. `process()` is idempotent under sequential retry — it
108
+ * acts on `pending` and re-does nothing already settled — but two passes
109
+ * interleaved is a different failure: both read the same `pending` state,
110
+ * both bill the provider for the same stage, and both write
111
+ * `processedBlobCid`, so the surviving variant is whichever finished last
112
+ * and the other's blob is orphaned.
113
+ *
114
+ * A runner claims this field transactionally before doing any work and
115
+ * clears it when finished; a second runner finding it unexpired declines
116
+ * and returns. It is an EXPIRY, not a boolean lock, because the holder can
117
+ * die mid-run (instance recycled, process killed) with no chance to
118
+ * release — a plain flag would strand the post permanently, where a lapsed
119
+ * lease lets the next delivery pick it up.
120
+ *
121
+ * Internal, like the variant fields above: `toProcessingView` projects
122
+ * stages only, so this never reaches a client.
123
+ */
124
+ leaseUntil: FirestoreTimestampSchema.optional(),
125
+ updatedAt: FirestoreTimestampSchema
162
126
  });
127
+ var ProcessingViewSchema = ProcessingStageMapSchema;
163
128
 
164
129
  // types/audio.ts
165
130
  var StrongRefSchema = zod.z.object({
@@ -172,7 +137,7 @@ var ReplyRefSchema = zod.z.object({
172
137
  });
173
138
  var AudioEmbedSchema = zod.z.object({
174
139
  $type: zod.z.literal("dev.antiphony.embed.audio"),
175
- /** The audio bytes as a content-addressed storage ref (CID/path). */
140
+ /** The audio bytes as a content-addressed blob ref (`ref.$link` = CID). */
176
141
  audio: BlobRefSchema,
177
142
  /** Duration in MILLISECONDS (platform-wide unit; not seconds). */
178
143
  durationMs: zod.z.number().int().min(0).optional(),
@@ -195,18 +160,47 @@ var TimedTranscriptSchema = zod.z.object({
195
160
  });
196
161
  var AudioEmbedViewSchema = zod.z.object({
197
162
  $type: zod.z.literal("dev.antiphony.embed.audio#view"),
163
+ /**
164
+ * `url`, `durationMs` and `waveform` are RESOLVED, not copied: once
165
+ * processing has produced an audio variant they describe that variant
166
+ * rather than the bytes the client uploaded. They always agree with one
167
+ * another — a duration and a set of peaks are only meaningful against the
168
+ * audio `url` actually points at.
169
+ *
170
+ * A client that stored `durationMs` at upload time should therefore expect
171
+ * it to change (trim removes leading/trailing silence), and should render
172
+ * these three as a set rather than caching them independently. The record's
173
+ * originals are immutable and unaffected; this is a read-time resolution.
174
+ */
198
175
  url: zod.z.string().url(),
199
176
  durationMs: zod.z.number().int().min(0).optional(),
200
- // `alt`/`waveform` are copied from the stored embed; keep the same bounds
201
- // so a view can never carry a larger payload than the record allows.
177
+ // `alt` is copied from the stored embed; keep the same bounds so a view can
178
+ // never carry a larger payload than the record allows.
202
179
  alt: zod.z.string().max(1e4).optional(),
203
180
  waveform: zod.z.array(zod.z.number().int().min(0).max(100)).max(1e3).optional(),
204
181
  /** Lifted from the transcript enrichment record; absent until transcription completes. */
205
- transcript: TimedTranscriptSchema.optional()
182
+ transcript: TimedTranscriptSchema.optional(),
183
+ /**
184
+ * Per-stage audio-processing status (denoise / trim / transcribe /
185
+ * waveform), when the app opted into processing. Absent otherwise. A
186
+ * `pending` stage means the client should poll (or re-render) for the
187
+ * result — including a stage that returns to `pending` after having been
188
+ * `ready`, which is how a recompute surfaces. Once a byte-mutating stage
189
+ * completes, `url`/`durationMs`/`waveform` above already resolve to the
190
+ * processed audio variant. See `types/processing.ts`.
191
+ */
192
+ processing: ProcessingViewSchema.optional()
206
193
  });
207
194
  var AudioPostRecordSchema = zod.z.object({
208
195
  /** Storage id (rkey/doc id). */
209
196
  id: zod.z.string(),
197
+ /**
198
+ * Content CID of the canonical lexicon record (CIDv1, dag-cbor, sha2-256
199
+ * — the AT Protocol record-CID rule). Computed at write time over the
200
+ * lexicon projection (public fields only, NOT the storage/tenancy fields
201
+ * below), so StrongRefs built from it are verifiable content addresses.
202
+ */
203
+ cid: zod.z.string(),
210
204
  // --- Tenancy + facets (storage-indexed; NOT in the lexicon) ---
211
205
  /** Origin app that created this record — the multi-tenant isolation key. */
212
206
  originAppId: zod.z.string(),
@@ -224,9 +218,24 @@ var AudioPostRecordSchema = zod.z.object({
224
218
  * opened the branch. Set on replies (deduped, 1–2 ids); absent on prompts
225
219
  * (a prompt's repliers are the app's audience policy, not a fixed pair).
226
220
  * Inherited down the branch so reply gating is an O(1) field check, never a
227
- * thread walk. See `specs/antiphony-data-model.md` §6 "Reply gating".
221
+ * thread walk.
228
222
  */
229
223
  threadParticipants: zod.z.array(zod.z.string()).optional(),
224
+ /**
225
+ * Author of this reply's thread ROOT (the prompt) — the reply's recipient,
226
+ * the person whose "replies to me" feed it lands in. Denormalized at write
227
+ * time so "replies whose root author is X" is a cheap composite-index query
228
+ * (see `queryByRootAuthor`). Set on replies (`kind === 'reply'`); absent on
229
+ * prompts. Storage-layer facet, NOT in the public lexicon or the record CID.
230
+ */
231
+ rootAuthorId: zod.z.string().optional(),
232
+ /**
233
+ * Async audio-processing state (transcribe / denoise), present iff the app
234
+ * opted into processing on create. Mutated by the processing worker after
235
+ * the post is created — storage-layer, NOT in the lexicon or the record
236
+ * CID. See `types/processing.ts`.
237
+ */
238
+ processing: ProcessingStateSchema.optional(),
230
239
  // --- Lexicon fields (public contract) ---
231
240
  /** User-authored text (bsky-semantic). May be empty for pure-audio posts. NEVER the transcript. */
232
241
  text: zod.z.string().max(3e3),
@@ -245,10 +254,11 @@ var AudioPostRecordSchema = zod.z.object({
245
254
  }).refine(
246
255
  // `kind` is denormalized from `reply` presence at write time; enforce the
247
256
  // invariant so an inconsistent record can't be written or read silently.
248
- // A reply has `reply` and no `title`; a prompt has neither a `reply`.
249
- (r) => r.kind === "reply" ? !!r.reply && r.title === void 0 : !r.reply,
257
+ // A reply has `reply`, no `title`, and a stamped `rootAuthorId` (its
258
+ // recipient facet); a prompt has none of those.
259
+ (r) => r.kind === "reply" ? !!r.reply && r.title === void 0 && r.rootAuthorId !== void 0 : !r.reply && r.rootAuthorId === void 0,
250
260
  {
251
- message: "kind must match reply presence: 'reply' \u21D2 reply set & no title; 'prompt' \u21D2 no reply",
261
+ message: "kind must match reply presence: 'reply' \u21D2 reply set, no title, rootAuthorId set; 'prompt' \u21D2 no reply, no rootAuthorId",
252
262
  path: ["kind"]
253
263
  }
254
264
  );
@@ -291,9 +301,13 @@ var PostRecordPublicSchema = zod.z.object({
291
301
  var AudioPostViewSchema = zod.z.object({
292
302
  /** at:// URI (or internal ref) identifying the post. */
293
303
  uri: zod.z.string(),
294
- cid: zod.z.string().optional(),
304
+ /** Content CID of the canonical record (see `AudioPostRecordSchema.cid`). */
305
+ cid: zod.z.string(),
295
306
  kind: zod.z.enum(["prompt", "reply"]),
296
- author: ProfileViewBasicSchema,
307
+ /** The acting actor's app-scoped id — an opaque attribution ref, not a profile. */
308
+ authorId: zod.z.string(),
309
+ /** The author's app-asserted AT Protocol DID, when the caller provided one. */
310
+ authorDid: zod.z.string().optional(),
297
311
  record: PostRecordPublicSchema,
298
312
  /** Hydrated audio embed (signed URL + lifted transcript). */
299
313
  embed: AudioEmbedViewSchema.optional(),