@antiphony/shared 0.1.0 → 0.4.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 (59) hide show
  1. package/README.md +4 -4
  2. package/dist/cjs/api-codecs.cjs +90 -787
  3. package/dist/cjs/api-codecs.d.cts +117 -290
  4. package/dist/cjs/index.cjs +102 -947
  5. package/dist/cjs/index.d.cts +89 -7
  6. package/dist/cjs/nsid.cjs +8 -18
  7. package/dist/cjs/nsid.d.cts +8 -17
  8. package/dist/cjs/types/audio.cjs +77 -662
  9. package/dist/cjs/types/audio.d.cts +285 -203
  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 -370
  13. package/dist/cjs/types/records.d.cts +1 -825
  14. package/dist/esm/api-codecs.d.ts +117 -290
  15. package/dist/esm/api-codecs.js +5 -6
  16. package/dist/esm/{chunk-ORMEWXMH.js → chunk-5JBD5THX.js} +1 -16
  17. package/dist/esm/{chunk-F2CANZZ7.js → chunk-BNNLHRH7.js} +1 -1
  18. package/dist/esm/chunk-D655OH2I.js +35 -0
  19. package/dist/esm/chunk-RUUHUNZ6.js +37 -0
  20. package/dist/esm/chunk-SBYNIQRZ.js +23 -0
  21. package/dist/esm/chunk-SMK4OZNU.js +17 -0
  22. package/dist/esm/{chunk-BUNMR4ZO.js → chunk-WL2GSPGC.js} +81 -12
  23. package/dist/esm/errors/index.js +1 -1
  24. package/dist/esm/index.d.ts +89 -7
  25. package/dist/esm/index.js +7 -9
  26. package/dist/esm/nsid.d.ts +8 -17
  27. package/dist/esm/nsid.js +2 -2
  28. package/dist/esm/observability/index.js +2 -2
  29. package/dist/esm/observability/report-error.js +2 -2
  30. package/dist/esm/types/audio.d.ts +285 -203
  31. package/dist/esm/types/audio.js +4 -5
  32. package/dist/esm/types/blob.d.ts +21 -21
  33. package/dist/esm/types/blob.js +2 -2
  34. package/dist/esm/types/records.d.ts +1 -825
  35. package/dist/esm/types/records.js +2 -3
  36. package/dist/esm/utils/index.js +1 -1
  37. package/package.json +90 -26
  38. package/dist/cjs/types/api.cjs +0 -726
  39. package/dist/cjs/types/api.d.cts +0 -494
  40. package/dist/cjs/types/channels.cjs +0 -170
  41. package/dist/cjs/types/channels.d.cts +0 -262
  42. package/dist/cjs/types/storage.cjs +0 -414
  43. package/dist/cjs/types/storage.d.cts +0 -197
  44. package/dist/cjs/types/views.cjs +0 -820
  45. package/dist/cjs/types/views.d.cts +0 -9806
  46. package/dist/esm/chunk-3WZJXJNU.js +0 -378
  47. package/dist/esm/chunk-5EHV73BA.js +0 -20
  48. package/dist/esm/chunk-7V44CPRR.js +0 -132
  49. package/dist/esm/chunk-EA4OONDV.js +0 -380
  50. package/dist/esm/chunk-TIZRJORN.js +0 -24
  51. package/dist/esm/chunk-XDBKR6LW.js +0 -32
  52. package/dist/esm/types/api.d.ts +0 -494
  53. package/dist/esm/types/api.js +0 -5
  54. package/dist/esm/types/channels.d.ts +0 -262
  55. package/dist/esm/types/channels.js +0 -162
  56. package/dist/esm/types/storage.d.ts +0 -197
  57. package/dist/esm/types/storage.js +0 -26
  58. package/dist/esm/types/views.d.ts +0 -9806
  59. package/dist/esm/types/views.js +0 -4
@@ -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,655 +45,21 @@ var FirestoreTimestampSchema = zod.z.union([
48
45
  }
49
46
  return date;
50
47
  });
48
+ var ProcessingStageStatusSchema = zod.z.enum(["pending", "ready", "failed", "skipped"]);
51
49
  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
- /**
59
- * Domain for federated handle support.
60
- * Defaults to 'voxpop.com'.
61
- */
62
- domain: zod.z.string().default("voxpop.com"),
63
- /**
64
- * Display Name (e.g. "Brad Thorson"). Nullable: Firestore stores `null`
65
- * when the user clears this field via the settings form, and the schema
66
- * must match storage reality or `UserRecordSchema.parse` (in
67
- * `getUserRecordByUid`) will throw.
68
- */
69
- displayName: zod.z.string().max(50).nullable().optional(),
70
- /** Short bio/description — nullable for the same reason as displayName. */
71
- bio: zod.z.string().max(160).nullable().optional(),
72
- /** URL to avatar image — nullable for the same reason as displayName. */
73
- avatarUrl: zod.z.string().url().nullable().optional(),
74
- /** Optional personal website surfaced on the public profile. */
75
- website: httpsUrl().nullable().optional(),
76
- /** Up to 5 additional public links (label + URL) shown under the bio. */
77
- links: zod.z.array(zod.z.object({
78
- label: zod.z.string().min(1).max(40),
79
- url: httpsUrl()
80
- })).max(5).optional(),
81
- /** When true and a Bluesky identity is linked, surfaces it on the public profile. */
82
- showBlueskyPublicly: zod.z.boolean().optional(),
83
- /** Server timestamp of creation */
84
- createdAt: FirestoreTimestampSchema,
85
- /** Individual account tier — free or creator_pro */
86
- tier: zod.z.enum(["free", "creator_pro"]).default("free"),
87
- /** Account status. Deactivated accounts retain data but are excluded from lookups. */
88
- status: zod.z.enum(["active", "deactivated"]).default("active"),
89
- /** Timestamp when the account was deactivated (soft deleted) */
90
- deactivatedAt: FirestoreTimestampSchema.optional(),
91
- /**
92
- * Denormalized org memberships — { orgId: role } for fast lookup.
93
- * Source of truth is organizations/{orgId}/members/{userId}.
94
- * Kept in sync by Cloud Function trigger.
95
- */
96
- orgMemberships: zod.z.record(zod.z.string(), zod.z.enum(["owner", "admin", "member"])).optional()
97
- });
98
- var PromptRecordSchema = zod.z.object({
99
- /** Unique Prompt ID */
100
- id: zod.z.string(),
101
- /** ID of the User who created this prompt. Always a user ID, never an org ID. @see UserRecord */
102
- authorId: zod.z.string(),
103
- /** Organization context this prompt belongs to (null = personal/no org) */
104
- orgId: zod.z.string().nullable().optional(),
105
- /** The main text of the question/prompt */
106
- title: zod.z.string().min(3),
107
- /** Optional extra context */
108
- description: zod.z.string().nullable().optional(),
109
- /** User who created this prompt (differs from authorId when org-owned) */
110
- createdBy: zod.z.string().optional(),
111
- /** URL to the recorded audio file (GCS) */
112
- audioUrl: zod.z.string().url().or(zod.z.literal("")),
113
- /** AT Protocol blob reference (future replacement for audioUrl) */
114
- audio: BlobRefSchema.optional(),
115
- /**
116
- * AT Protocol URI returned by the publisher after a successful
117
- * `repo.putRecord` against the author's PDS — e.g.
118
- * `at://did:plc:abc123/com.voxpop.audio.prompt/3kj4...`. Optional,
119
- * no migration: existing rows leave it unset. Format-validated so
120
- * malformed strings surface at the schema-parse boundary rather than
121
- * being silently stored.
122
- */
123
- atprotoUri: zod.z.string().regex(/^at:\/\/.+/).optional(),
124
- /** Server timestamp of creation */
125
- createdAt: FirestoreTimestampSchema,
126
- /**
127
- * Life-cycle status.
128
- * - `live`: Visible and accepting replies.
129
- * - `archived`: Visible but closed for new replies.
130
- * - `deleted`: Soft deleted.
131
- */
132
- status: zod.z.enum(["live", "archived", "deleted"]).default("live"),
133
- /** AI Enrichment Fields */
134
- aiStatus: zod.z.enum(["pending", "complete", "error"]).optional(),
135
- aiError: zod.z.string().optional(),
136
- aiSummary: zod.z.string().optional(),
137
- aiLabels: zod.z.array(zod.z.string()).optional(),
138
- transcription: zod.z.string().optional(),
139
- /** Pre-computed waveform peaks (normalized 0–1) for instant audio visualization */
140
- waveformPeaks: zod.z.array(zod.z.number()).optional(),
141
- /** Social Share Video Fields */
142
- socialVideoUrl: zod.z.string().url().optional(),
143
- socialVideoStoragePath: zod.z.string().optional(),
144
- socialVideoStatus: zod.z.enum(["pending", "complete", "error"]).optional(),
145
- socialVideoError: zod.z.string().optional(),
146
- /** The audio URL/path used to generate the current video (for cache invalidation) */
147
- socialVideoSourceAudio: zod.z.string().optional()
148
- });
149
- var ReplyRecordSchema = zod.z.object({
150
- /** Unique Reply ID */
151
- id: zod.z.string(),
152
- /** The Prompt being replied to. @see PromptRecord */
153
- promptId: zod.z.string(),
154
- /** The User who replied. @see UserRecord */
155
- authorId: zod.z.string(),
156
- /** URL to the recorded audio file (GCS) */
157
- audioUrl: zod.z.string().url(),
158
- /** AT Protocol blob reference (future replacement for audioUrl) */
159
- audio: BlobRefSchema.optional(),
160
- /** Server timestamp of creation */
161
- createdAt: FirestoreTimestampSchema,
162
- /**
163
- * Life-cycle status.
164
- * - `live`: Visible and accepting replies.
165
- * - `archived`: Visible but closed for new replies.
166
- * - `deleted`: Soft deleted.
167
- */
168
- status: zod.z.enum(["live", "archived", "deleted"]).default("live"),
169
- /**
170
- * Pre-computed waveform peaks (normalized 0–1) for instant audio
171
- * visualization. **Stays on canonical** (does NOT move to the
172
- * enrichment doc) — produced by a plain ffmpeg pass at ingestion,
173
- * not an AI step; a self-hoster without paid-tier AI still needs
174
- * this on every reply for the audio player. See
175
- * `specs/ai-enrichment-split.md` § 5.
176
- */
177
- waveformPeaks: zod.z.array(zod.z.number()).optional(),
178
- /**
179
- * Duration of the audio in seconds, computed server-side from
180
- * ffmpeg. **Stays on canonical** (same reasoning as `waveformPeaks`
181
- * above — ingestion ffmpeg output, not an AI enrichment). See
182
- * `specs/ai-enrichment-split.md` § 5.
183
- */
184
- audioDurationSec: zod.z.number().optional()
185
- });
186
- zod.z.object({
187
- id: zod.z.string(),
188
- /** Private notes by the prompt author about this reply. */
189
- notes: zod.z.string().optional(),
190
- // === AI-enrichment fields (sole source of truth post Stage 4) ===
191
- //
192
- // These lifted off canonical in Stage 4 of `specs/ai-enrichment-split.md`.
193
- // Writers (`functions/`) route AI updates here via the split-write
194
- // helper in `functions/src/services/replyEnrichmentDualWrite.ts`;
195
- // readers source them via the hydrator's enrichment branch (see
196
- // `packages/core/services/hydration.ts`).
197
- // --- AI core (Gemini-generated) ---
198
- aiStatus: zod.z.enum(["pending", "complete", "error", "skipped_too_short"]).optional(),
199
- aiError: zod.z.string().optional(),
200
- aiSummary: zod.z.string().optional(),
201
- aiLabels: zod.z.array(zod.z.string()).optional(),
202
- transcription: zod.z.string().optional(),
203
- sentiment: zod.z.enum(["Positive", "Negative", "Neutral"]).optional(),
204
- /** Must match the widened enum in `ReplyRecordSchema.energyLevel`. */
205
- energyLevel: zod.z.enum(["High", "Low", "Neutral"]).optional(),
206
- engagementScore: zod.z.number().min(1).max(10).optional(),
207
- // --- Voice isolation (ElevenLabs, paid tier) ---
208
- /** Noise-reduced audio URL — replaces `audioUrl` for downstream players when present. PUBLIC. */
209
- enhancedAudioUrl: zod.z.string().url().optional(),
210
- /** Storage path companion to `enhancedAudioUrl` — private. */
211
- enhancedStoragePath: zod.z.string().optional(),
212
- // --- Social-share video (paid tier) ---
213
- //
214
- // All `socialVideo*` fields are creator-only. The URL points at the
215
- // generated artifact (a video file the creator can download and post
216
- // to social media); the reply detail page does NOT render it.
217
- socialVideoUrl: zod.z.string().url().optional(),
218
- socialVideoStoragePath: zod.z.string().optional(),
219
- socialVideoStatus: zod.z.enum(["pending", "complete", "error"]).optional(),
220
- socialVideoError: zod.z.string().optional(),
221
- /** The audio URL/path used to generate the current video (for cache invalidation). */
222
- socialVideoSourceAudio: zod.z.string().optional()
223
- });
224
- zod.z.object({
225
- /** Composite key `${viewerUid}_${targetUid}`. */
226
- id: zod.z.string(),
227
- /** Per-viewer CRM notes about the target. */
228
- notes: zod.z.string().optional(),
229
- /** Per-viewer freeform tags about the target. */
230
- tags: zod.z.array(zod.z.string()).optional(),
231
- // === Identity-merge: alias set ===
232
- //
233
- // Other identifiers the viewer has declared to be the SAME person as
234
- // this entry's `targetUid`. vCard/URI-aligned: each entry is a
235
- // scheme-prefixed identifier, so one field generalizes across networks
236
- // AND across the future contact-sync import (which carries phone/email):
237
- // "uid:<firebaseUid>" | "did:plc:…" | "handle:bob.bsky.social"
238
- // | "tel:+15551234567" | "mailto:x@example.com"
239
- // Mirrors vCard 4.0's `UID` + `CLIENTPIDMAP`/`PID` source-tracking
240
- // (RFC 6350) — the standard answer to "the same person from multiple
241
- // sources". See `specs/people-lexicon-prior-art.md`. The People
242
- // read-path collapses any replier whose "uid:<uid>" appears here into
243
- // this primary entry. Empty until the merge feature is used.
244
- aliases: zod.z.array(zod.z.string()).optional(),
245
- lastUpdated: FirestoreTimestampSchema.optional()
246
- });
247
- zod.z.object({
248
- sipUri: zod.z.string(),
249
- sipUsername: zod.z.string(),
250
- sipSecret: zod.z.string(),
251
- provider: zod.z.enum(["twilio", "plivo", "internal"])
252
- });
253
- zod.z.object({
254
- /** User's personal phone number (E.164) */
255
- phoneNumber: zod.z.string(),
256
- /** Line type from Twilio Lookup v2 */
257
- lineType: zod.z.string().nullable(),
258
- /** Carrier name from Twilio Lookup v2 */
259
- carrier: zod.z.string().nullable(),
260
- /** free = shared Twilio number (ForwardedFrom routing), paid = dedicated number */
261
- tier: zod.z.enum(["free", "paid"]),
262
- /** The VoxPop Twilio number calls forward to (E.164) */
263
- voxpopNumber: zod.z.string(),
264
- /** Twilio Phone Number SID (paid tier only) */
265
- twilioNumberSid: zod.z.string().nullable().optional(),
266
- /** Forwarding verification state */
267
- verificationStatus: zod.z.enum(["pending", "verifying", "verified", "failed"]).default("pending"),
268
- /** Last verification attempt timestamp */
269
- lastVerificationAt: FirestoreTimestampSchema.optional(),
270
- /** Number of verification attempts */
271
- verificationAttempts: zod.z.number().default(0),
272
- /** Reason for verification failure */
273
- failureReason: zod.z.string().nullable().optional(),
274
- /** Whether call forwarding is active */
275
- enabled: zod.z.boolean().default(false),
276
- createdAt: FirestoreTimestampSchema,
277
- updatedAt: FirestoreTimestampSchema
278
- });
279
- var ConnectorTypeSchema = zod.z.enum(["telephony"]);
280
- var ConnectorStatusSchema = zod.z.object({
281
- /** Coarse, generic lifecycle the control-plane UI can render without
282
- * connector-specific knowledge. */
283
- state: zod.z.enum(["unconfigured", "pending", "active", "error", "disabled"]).default("unconfigured"),
284
- detail: zod.z.string().nullable().optional(),
285
- /** Opaque, connector-specific status detail (e.g. telephony's
286
- * `{ verificationStatus, verificationAttempts, failureReason }`). Connector-
287
- * owned like the rest of `status` — never user-writable. Nullable so a doc
288
- * that stored `data: null` round-trips without a parse error. */
289
- data: zod.z.record(zod.z.unknown()).nullable().optional(),
290
- updatedAt: FirestoreTimestampSchema.optional()
291
- });
292
- zod.z.object({
293
- connectorType: ConnectorTypeSchema,
294
- /** The owning user. Org-scoping (ownerId as an orgId) is a later step. */
295
- ownerId: zod.z.string(),
296
- /** Opaque, connector-specific blob. Core does not interpret this. */
297
- settings: zod.z.record(zod.z.unknown()).default({}),
298
- /** Reference into a secret store (e.g. Secret Manager key). Never a raw secret. */
299
- secretRef: zod.z.string().nullable().optional(),
300
- enabled: zod.z.boolean().default(false),
301
- status: ConnectorStatusSchema.default({ state: "unconfigured" }),
302
- createdAt: FirestoreTimestampSchema,
50
+ transcribe: zod.z.boolean().optional(),
51
+ denoise: zod.z.boolean().optional()
52
+ });
53
+ var ProcessingStateSchema = zod.z.object({
54
+ transcribe: ProcessingStageStatusSchema.optional(),
55
+ denoise: ProcessingStageStatusSchema.optional(),
56
+ /** Content CID of the denoised audio variant, once `denoise === 'ready'`. */
57
+ denoisedBlobCid: zod.z.string().optional(),
303
58
  updatedAt: FirestoreTimestampSchema
304
59
  });
305
- zod.z.object({
306
- id: zod.z.string(),
307
- ownerId: zod.z.string(),
308
- /** The caller number this rule matches (E.164). Format-enforced at the
309
- * record level so any writer (API, future contact-sync/callback) can't
310
- * persist a malformed number. */
311
- e164: zod.z.string().regex(/^\+[1-9]\d{6,14}$/, "Must be an E.164 phone number"),
312
- /** Display label, e.g. "Mom" / "Delta Airlines". null = unlabeled. */
313
- label: zod.z.string().nullable().optional(),
314
- /** `allow` = ring through; `screen` = async voicemail. */
315
- action: zod.z.enum(["allow", "screen"]),
316
- /** Provenance. `manual` = user-created; the others are future writers. */
317
- source: zod.z.enum(["manual", "contact-sync", "callback"]),
318
- /** null = permanent; a date = self-expiring exception. */
319
- expiresAt: FirestoreTimestampSchema.nullable().optional(),
320
- createdAt: FirestoreTimestampSchema
321
- });
322
- var OrganizationRecordSchema = zod.z.object({
323
- id: zod.z.string(),
324
- /** Display name of the organization */
325
- name: zod.z.string().min(3).max(50),
326
- /**
327
- * Unique handle for the organization (e.g. voxpop.com/@mypodcast).
328
- * Used for public URLs.
329
- */
330
- slug: zod.z.string().min(3).max(30).regex(/^[a-z0-9-]+$/),
331
- /** URL to avatar/logo */
332
- avatarUrl: zod.z.string().url().optional(),
333
- /** URL to the podcast RSS feed */
334
- rssFeedUrl: zod.z.string().url().optional(),
335
- /** External website URL */
336
- websiteUrl: httpsUrl().optional(),
337
- /** Description or tagline */
338
- description: zod.z.string().optional(),
339
- /** Owner ID (User ID) */
340
- ownerId: zod.z.string(),
341
- /** Server timestamp of creation */
342
- createdAt: FirestoreTimestampSchema,
343
- /** Verified domain (e.g., "acme.com") — enables auto-join, enterprise features */
344
- domain: zod.z.string().nullable().optional(),
345
- /** Whether the domain has been verified via DNS TXT record */
346
- domainVerified: zod.z.boolean().default(false),
347
- /** DNS verification token (stored server-side until verification completes) */
348
- domainVerificationToken: zod.z.string().optional(),
349
- /** Billing email — required for paid orgs, where invoices go */
350
- billingEmail: zod.z.string().email().nullable().optional(),
351
- /**
352
- * Tier determines isolation and feature set. Orgs are meant to be paid
353
- * (business or enterprise); free/pro exist on the enum for the individual track.
354
- * Default is provisionally `business` (the entry paid tier) — it's assigned at
355
- * creation with no subscription behind it yet, so it stays provisional until billing
356
- * promotes/reconciles the org. See docs/tech-debt.md "Org tier billing reconciliation".
357
- */
358
- tier: zod.z.enum(["free", "pro", "business", "enterprise"]).default("business"),
359
- /** Stripe Customer ID */
360
- stripeCustomerId: zod.z.string().optional(),
361
- /** Subscription Status */
362
- subscriptionStatus: zod.z.enum(["active", "trialing", "past_due", "canceled", "unpaid"]).optional()
363
- });
364
- var OrganizationMemberRecordSchema = zod.z.object({
365
- orgId: zod.z.string(),
366
- userId: zod.z.string(),
367
- /** Role in the organization */
368
- role: zod.z.enum(["owner", "admin", "member"]),
369
- /** Server timestamp of joining */
370
- joinedAt: FirestoreTimestampSchema,
371
- invitedBy: zod.z.string().optional()
372
- });
373
- var OrgInviteRecordSchema = zod.z.object({
374
- id: zod.z.string(),
375
- orgId: zod.z.string(),
376
- /** Email the invite was sent to */
377
- email: zod.z.string().email(),
378
- /** Role to assign on acceptance */
379
- role: zod.z.enum(["admin", "member"]),
380
- /** User ID of who sent the invite */
381
- invitedBy: zod.z.string(),
382
- /** Invite lifecycle status */
383
- status: zod.z.enum(["pending", "accepted", "expired", "revoked"]).default("pending"),
384
- /** Server timestamp of creation */
385
- createdAt: FirestoreTimestampSchema,
386
- /** When this invite expires */
387
- expiresAt: FirestoreTimestampSchema
388
- });
389
- var ProfileViewBasicSchema = zod.z.object({
390
- id: zod.z.string(),
391
- handle: zod.z.string().nullable().optional(),
392
- // `displayName` and `bio` are `.nullable()` — Firestore stores `null` for
393
- // empty values on these fields (see users-dependencies.ts), and Zod's
394
- // `.optional()` alone rejects `null`. This hit dashboard rendering on the
395
- // Phase 3.1 HTTP cutover, when transport-layer Zod validation started
396
- // actually exercising the wire shape. Consumers already use truthy
397
- // checks / `??` / `||`, so widening the type to include `null` is safe.
398
- displayName: zod.z.string().nullable().optional(),
399
- avatarUrl: zod.z.string().nullable().optional(),
400
- bio: zod.z.string().nullable().optional(),
401
- /** Personal website link surfaced on the public profile. */
402
- website: zod.z.string().nullable().optional(),
403
- /** Public links (label + URL) shown under the bio. */
404
- links: zod.z.array(zod.z.object({
405
- label: zod.z.string(),
406
- url: zod.z.string()
407
- })).optional(),
408
- /**
409
- * AT Protocol identity, surfaced on the public profile only when the user
410
- * opts in (`UserRecord.showBlueskyPublicly === true`). Projection happens
411
- * in the user dependency layer; this schema simply allows the field.
412
- */
413
- bluesky: zod.z.object({
414
- handle: zod.z.string(),
415
- did: zod.z.string()
416
- }).optional(),
417
- stats: zod.z.object({
418
- followers: zod.z.number().default(0),
419
- following: zod.z.number().default(0),
420
- prompts: zod.z.number().default(0)
421
- }).optional(),
422
- badges: zod.z.array(zod.z.string()).optional(),
423
- isVerified: zod.z.boolean().optional(),
424
- createdAt: FirestoreTimestampSchema.optional()
425
- });
426
- var ProfileViewDetailedSchema = ProfileViewBasicSchema.extend({
427
- /** AT Protocol Identity link */
428
- bluesky: zod.z.object({
429
- handle: zod.z.string(),
430
- did: zod.z.string()
431
- }).optional(),
432
- usageIntent: zod.z.string().nullable().optional(),
433
- /** Hydrated RSS Data (fetched from sub-collection) */
434
- rssSummary: zod.z.object({
435
- title: zod.z.string().optional(),
436
- description: zod.z.string().optional(),
437
- items: zod.z.array(zod.z.object({
438
- title: zod.z.string().optional(),
439
- link: zod.z.string().optional(),
440
- content: zod.z.string().optional(),
441
- pubDate: zod.z.string().optional()
442
- })).optional(),
443
- lastFetchedAt: FirestoreTimestampSchema.optional()
444
- }).optional(),
445
- promptAudioUrl: zod.z.string().optional(),
446
- /** @deprecated Use stats.prompts from ProfileViewBasic instead */
447
- totalPrompts: zod.z.number().optional(),
448
- totalReplies: zod.z.number().optional(),
449
- favoritePromptId: zod.z.string().optional()
450
- });
451
- var ProfileViewSelfSchema = ProfileViewDetailedSchema.extend({
452
- phoneNumber: zod.z.string().nullable().optional(),
453
- email: zod.z.string().optional(),
454
- lastSeenAt: FirestoreTimestampSchema.optional(),
455
- lastActiveAt: FirestoreTimestampSchema.optional(),
456
- unreadReplyCount: zod.z.number().default(0),
457
- newReplierCount: zod.z.number().default(0),
458
- /**
459
- * Account tier from UserRecord — surfaced on the self profile so the
460
- * client can gate paid features (e.g. the Performance dashboard tab).
461
- * Optional for legacy docs without the field; consumers should treat
462
- * missing as `'free'`.
463
- */
464
- tier: zod.z.enum(["free", "creator_pro"]).optional(),
465
- /**
466
- * Surfaces the linked Bluesky identity (handle + DID) on the public profile
467
- * when true. Persisted on UserRecord; exposed in self/detailed views so the
468
- * settings form can render the toggle's current state.
469
- */
470
- showBlueskyPublicly: zod.z.boolean().optional(),
471
- settings: zod.z.object({
472
- notifications: zod.z.boolean().optional(),
473
- theme: zod.z.string().optional()
474
- }).optional()
475
- });
476
- var ProfileViewAdminSchema = ProfileViewSelfSchema.extend({
477
- blockedUsers: zod.z.array(zod.z.string()).optional(),
478
- followers: zod.z.array(zod.z.string()).optional(),
479
- following: zod.z.array(zod.z.string()).optional(),
480
- reportCount: zod.z.number().optional(),
481
- isBanned: zod.z.boolean().optional()
482
- });
483
- var ProfileViewSchema = ProfileViewAdminSchema;
484
- var PromptViewSchema = zod.z.object({
485
- /** AT Protocol URI (e.g. at://did:plc.../app.../123) */
486
- uri: zod.z.string().optional(),
487
- /** IPFS Content ID */
488
- cid: zod.z.string().optional(),
489
- /** The raw prompt data */
490
- record: PromptRecordSchema,
491
- /** The hydrated author profile */
492
- author: ProfileViewSchema,
493
- /** Total number of replies */
494
- replyCount: zod.z.number().default(0),
495
- likeCount: zod.z.number().default(0),
496
- updatedAt: FirestoreTimestampSchema.optional(),
497
- lastReplyAt: FirestoreTimestampSchema.optional(),
498
- tags: zod.z.array(zod.z.string()).optional(),
499
- visibility: zod.z.enum(["public", "private", "unlisted", "archived"]).default("public"),
500
- analytics: zod.z.object({
501
- views: zod.z.number().default(0),
502
- listens: zod.z.number().default(0),
503
- /**
504
- * Mean engagementScore (1–10) across this prompt's `status: 'live'`
505
- * replies that completed AI enrichment. `null` when no such replies
506
- * exist (0 would alias a valid bottom-of-range score). Derived in
507
- * hydration from the prompt doc's `engagementScoreSum / Count`.
508
- */
509
- avgEngagementScore: zod.z.number().nullable().optional(),
510
- /**
511
- * Counts of `status: 'live'` AI-enriched replies grouped by sentiment.
512
- * Keys lowercase by convention; source enum
513
- * (`Positive | Neutral | Negative`) is mapped at the write site.
514
- */
515
- sentimentBreakdown: zod.z.object({
516
- positive: zod.z.number(),
517
- neutral: zod.z.number(),
518
- negative: zod.z.number()
519
- }).optional()
520
- }).optional(),
521
- moderation: zod.z.object({
522
- flagged: zod.z.boolean().default(false),
523
- reason: zod.z.string().optional()
524
- }).optional(),
525
- // AI Enrichment Fields (Hydrated from Record but hidden from Record Schema)
526
- aiLabels: zod.z.array(zod.z.string()).optional(),
527
- aiSummary: zod.z.string().optional(),
528
- aiStatus: zod.z.enum(["pending", "complete", "error"]).optional(),
529
- aiError: zod.z.string().optional(),
530
- transcription: zod.z.string().optional()
531
- });
532
- var PromptViewPublicSchema = PromptViewSchema.omit({
533
- analytics: true,
534
- moderation: true,
535
- aiLabels: true,
536
- aiSummary: true,
537
- aiStatus: true,
538
- aiError: true,
539
- transcription: true
540
- }).extend({
541
- author: ProfileViewBasicSchema
542
- });
543
- var ReplyViewSchema = zod.z.object({
544
- record: ReplyRecordSchema,
545
- author: ProfileViewSchema,
546
- recipient: ProfileViewSchema,
547
- /** GCS Storage Path for audio file */
548
- storagePath: zod.z.string().optional(),
549
- duration: zod.z.number().optional(),
550
- updatedAt: FirestoreTimestampSchema.optional(),
551
- isRead: zod.z.boolean(),
552
- readBy: zod.z.array(zod.z.string()).default([]),
553
- isDeleted: zod.z.boolean().default(false),
554
- reactions: zod.z.record(zod.z.string(), zod.z.number()).optional(),
555
- moderation: zod.z.object({
556
- flagged: zod.z.boolean().default(false),
557
- reason: zod.z.string().optional()
558
- }).optional(),
559
- // AI Enrichment Fields (Hydrated from Record but hidden from Record Schema)
560
- aiLabels: zod.z.array(zod.z.string()).optional(),
561
- aiSummary: zod.z.string().optional(),
562
- aiStatus: zod.z.enum(["pending", "complete", "error", "skipped_too_short"]).optional(),
563
- aiError: zod.z.string().optional(),
564
- transcription: zod.z.string().optional(),
565
- sentiment: zod.z.enum(["Positive", "Negative", "Neutral"]).optional(),
566
- /** Must match the widened enum in `ReplyRecordSchema.energyLevel`. */
567
- energyLevel: zod.z.enum(["High", "Low", "Neutral"]).optional(),
568
- engagementScore: zod.z.number().min(1).max(10).optional(),
569
- // === Voice-isolation enrichment (lifted; see specs/ai-enrichment-split.md) ===
570
- //
571
- // `enhancedAudioUrl` is the only one of these the public view keeps —
572
- // it replaces `audioUrl` for downstream players when present.
573
- // `enhancedStoragePath` is private (storage paths are server-side
574
- // bookkeeping).
575
- enhancedAudioUrl: zod.z.string().url().optional(),
576
- enhancedStoragePath: zod.z.string().optional(),
577
- // === Social-share video enrichment (lifted; creator-only) ===
578
- //
579
- // All `socialVideo*` fields are stripped by `toReplyViewPublic` —
580
- // the URL points at a generated artifact the creator may share to
581
- // social media, NOT at content the reply page should render. Status /
582
- // error / source-audio are job-tracking state.
583
- socialVideoUrl: zod.z.string().url().optional(),
584
- socialVideoStoragePath: zod.z.string().optional(),
585
- socialVideoStatus: zod.z.enum(["pending", "complete", "error"]).optional(),
586
- socialVideoError: zod.z.string().optional(),
587
- socialVideoSourceAudio: zod.z.string().optional(),
588
- /** @private Confirmed listener phone number (never exposed publicly) */
589
- listenerPhoneNumber: zod.z.string().regex(/^\+[1-9]\d{1,14}$/).optional(),
590
- /**
591
- * @private Per-viewer private notes about this reply (CRM enrichment).
592
- * Lifted into the view from `enrichments/replies/{id}.notes` by the
593
- * hydrator when the viewer is the prompt author. Never populated for
594
- * non-author viewers; defensively stripped by `toReplyViewPublic`.
595
- */
596
- notes: zod.z.string().optional()
597
- });
598
- var ReplyViewPublicSchema = ReplyViewSchema.omit({
599
- listenerPhoneNumber: true,
600
- notes: true,
601
- // AI cluster — only `transcription` survives.
602
- aiStatus: true,
603
- aiError: true,
604
- aiSummary: true,
605
- aiLabels: true,
606
- sentiment: true,
607
- energyLevel: true,
608
- engagementScore: true,
609
- // Voice isolation — keep enhancedAudioUrl, strip the path.
610
- enhancedStoragePath: true,
611
- // Social-video — entire cluster is creator-only.
612
- socialVideoUrl: true,
613
- socialVideoStoragePath: true,
614
- socialVideoStatus: true,
615
- socialVideoError: true,
616
- socialVideoSourceAudio: true
617
- });
618
- var OrganizationViewSchema = zod.z.object({
619
- record: OrganizationRecordSchema,
620
- memberCount: zod.z.number().default(1),
621
- currentUserRole: zod.z.enum(["owner", "admin", "member"]).optional()
622
- });
623
- zod.z.object({
624
- record: OrganizationMemberRecordSchema,
625
- profile: ProfileViewBasicSchema
626
- });
627
- zod.z.object({
628
- record: OrgInviteRecordSchema,
629
- /** Display name of the user who sent the invite */
630
- inviterName: zod.z.string().optional(),
631
- /** Name of the organization */
632
- orgName: zod.z.string()
633
- });
634
- zod.z.object({
635
- promptId: zod.z.string(),
636
- repliers: zod.z.record(zod.z.string(), zod.z.object({
637
- firstReplyAt: FirestoreTimestampSchema,
638
- lastReplyAt: FirestoreTimestampSchema,
639
- replyCount: zod.z.number()
640
- }))
641
- });
642
- PromptViewSchema.extend({
643
- replies: zod.z.array(ReplyViewSchema)
644
- });
645
- var ReplierSchema = zod.z.object({
646
- handle: zod.z.string(),
647
- /** ISO date string */
648
- lastReplyDate: zod.z.string(),
649
- /** ISO date string */
650
- firstReplyAt: zod.z.string(),
651
- totalReplies: zod.z.number()
652
- });
653
- zod.z.discriminatedUnion("type", [
654
- // Public endpoint (`GET /resolve/:handle`) — project to the basic shape so
655
- // PII/admin fields (email, phoneNumber, settings, blockedUsers, …) never
656
- // cross the public API boundary. See `toProfileViewBasic`.
657
- zod.z.object({ type: zod.z.literal("user"), profile: ProfileViewBasicSchema }),
658
- zod.z.object({ type: zod.z.literal("org"), org: OrganizationViewSchema })
659
- ]);
660
- zod.z.object({
661
- // Public endpoint (`GET /users/:handle/profile`) — basic shape only, so the
662
- // owner's PII/admin fields never serialize to anonymous callers.
663
- profileUser: ProfileViewBasicSchema,
664
- // Each prompt's nested `author` is the profile owner, hydrated from the
665
- // wide admin profile (`getPromptsForUser` prefetches via getUserDataByUid).
666
- // Use the PUBLIC prompt/reply shapes so that nested author PII (email,
667
- // phoneNumber, settings, …) and owner-only prompt enrichment never leak.
668
- allPromptsWithReplies: zod.z.array(PromptViewPublicSchema.extend({
669
- replies: zod.z.array(ReplyViewPublicSchema)
670
- })),
671
- repliers: zod.z.array(ReplierSchema)
672
- });
673
- var RssSummarySchema = zod.z.object({
674
- title: zod.z.string().optional(),
675
- description: zod.z.string().optional(),
676
- image: zod.z.string().optional(),
677
- link: zod.z.string().optional(),
678
- items: zod.z.array(zod.z.object({
679
- title: zod.z.string().optional(),
680
- link: zod.z.string().optional(),
681
- content: zod.z.string().optional(),
682
- pubDate: zod.z.string().optional()
683
- })).optional(),
684
- lastFetchedAt: FirestoreTimestampSchema.optional()
685
- });
686
- zod.z.object({
687
- org: OrganizationViewSchema,
688
- prompts: zod.z.array(PromptViewSchema),
689
- rssSummary: RssSummarySchema.nullable()
690
- });
691
- zod.z.object({
692
- profile: ProfileViewBasicSchema,
693
- totalReplies: zod.z.number(),
694
- /** ISO date string */
695
- lastReplyDate: zod.z.string(),
696
- /** ISO date string */
697
- firstReplyAt: zod.z.string(),
698
- /** Phone number for anonymous repliers (from Firebase Auth, only visible to prompt author) */
699
- phoneNumber: zod.z.string().optional()
60
+ var ProcessingViewSchema = zod.z.object({
61
+ transcribe: ProcessingStageStatusSchema.optional(),
62
+ denoise: ProcessingStageStatusSchema.optional()
700
63
  });
701
64
 
702
65
  // types/audio.ts
@@ -710,7 +73,7 @@ var ReplyRefSchema = zod.z.object({
710
73
  });
711
74
  var AudioEmbedSchema = zod.z.object({
712
75
  $type: zod.z.literal("dev.antiphony.embed.audio"),
713
- /** The audio bytes as a content-addressed storage ref (CID/path). */
76
+ /** The audio bytes as a content-addressed blob ref (`ref.$link` = CID). */
714
77
  audio: BlobRefSchema,
715
78
  /** Duration in MILLISECONDS (platform-wide unit; not seconds). */
716
79
  durationMs: zod.z.number().int().min(0).optional(),
@@ -740,11 +103,26 @@ var AudioEmbedViewSchema = zod.z.object({
740
103
  alt: zod.z.string().max(1e4).optional(),
741
104
  waveform: zod.z.array(zod.z.number().int().min(0).max(100)).max(1e3).optional(),
742
105
  /** Lifted from the transcript enrichment record; absent until transcription completes. */
743
- transcript: TimedTranscriptSchema.optional()
106
+ transcript: TimedTranscriptSchema.optional(),
107
+ /**
108
+ * Per-stage audio-processing status (transcribe / denoise), when the app
109
+ * opted into processing on create. Absent otherwise. A `pending` stage
110
+ * means the client should poll (or re-render) for the result. When
111
+ * `denoise === 'ready'`, `url` above already resolves to the cleaned
112
+ * audio variant. See `types/processing.ts`.
113
+ */
114
+ processing: ProcessingViewSchema.optional()
744
115
  });
745
116
  var AudioPostRecordSchema = zod.z.object({
746
117
  /** Storage id (rkey/doc id). */
747
118
  id: zod.z.string(),
119
+ /**
120
+ * Content CID of the canonical lexicon record (CIDv1, dag-cbor, sha2-256
121
+ * — the AT Protocol record-CID rule). Computed at write time over the
122
+ * lexicon projection (public fields only, NOT the storage/tenancy fields
123
+ * below), so StrongRefs built from it are verifiable content addresses.
124
+ */
125
+ cid: zod.z.string(),
748
126
  // --- Tenancy + facets (storage-indexed; NOT in the lexicon) ---
749
127
  /** Origin app that created this record — the multi-tenant isolation key. */
750
128
  originAppId: zod.z.string(),
@@ -756,6 +134,30 @@ var AudioPostRecordSchema = zod.z.object({
756
134
  orgId: zod.z.string().nullable().optional(),
757
135
  /** Denormalized from `reply` presence: `reply` set ⇒ 'reply', else 'prompt'. */
758
136
  kind: zod.z.enum(["prompt", "reply"]),
137
+ /**
138
+ * Branch participant pair (author ids) for reply gating — the parties to a
139
+ * reply's sub-thread: the creator (thread-root author) + the responder who
140
+ * opened the branch. Set on replies (deduped, 1–2 ids); absent on prompts
141
+ * (a prompt's repliers are the app's audience policy, not a fixed pair).
142
+ * Inherited down the branch so reply gating is an O(1) field check, never a
143
+ * thread walk.
144
+ */
145
+ threadParticipants: zod.z.array(zod.z.string()).optional(),
146
+ /**
147
+ * Author of this reply's thread ROOT (the prompt) — the reply's recipient,
148
+ * the person whose "replies to me" feed it lands in. Denormalized at write
149
+ * time so "replies whose root author is X" is a cheap composite-index query
150
+ * (see `queryByRootAuthor`). Set on replies (`kind === 'reply'`); absent on
151
+ * prompts. Storage-layer facet, NOT in the public lexicon or the record CID.
152
+ */
153
+ rootAuthorId: zod.z.string().optional(),
154
+ /**
155
+ * Async audio-processing state (transcribe / denoise), present iff the app
156
+ * opted into processing on create. Mutated by the processing worker after
157
+ * the post is created — storage-layer, NOT in the lexicon or the record
158
+ * CID. See `types/processing.ts`.
159
+ */
160
+ processing: ProcessingStateSchema.optional(),
759
161
  // --- Lexicon fields (public contract) ---
760
162
  /** User-authored text (bsky-semantic). May be empty for pure-audio posts. NEVER the transcript. */
761
163
  text: zod.z.string().max(3e3),
@@ -774,10 +176,11 @@ var AudioPostRecordSchema = zod.z.object({
774
176
  }).refine(
775
177
  // `kind` is denormalized from `reply` presence at write time; enforce the
776
178
  // invariant so an inconsistent record can't be written or read silently.
777
- // A reply has `reply` and no `title`; a prompt has neither a `reply`.
778
- (r) => r.kind === "reply" ? !!r.reply && r.title === void 0 : !r.reply,
179
+ // A reply has `reply`, no `title`, and a stamped `rootAuthorId` (its
180
+ // recipient facet); a prompt has none of those.
181
+ (r) => r.kind === "reply" ? !!r.reply && r.title === void 0 && r.rootAuthorId !== void 0 : !r.reply && r.rootAuthorId === void 0,
779
182
  {
780
- message: "kind must match reply presence: 'reply' \u21D2 reply set & no title; 'prompt' \u21D2 no reply",
183
+ message: "kind must match reply presence: 'reply' \u21D2 reply set, no title, rootAuthorId set; 'prompt' \u21D2 no reply, no rootAuthorId",
781
184
  path: ["kind"]
782
185
  }
783
186
  );
@@ -799,7 +202,15 @@ var ActorProfileRecordSchema = zod.z.object({
799
202
  });
800
203
  var ViewerStateSchema = zod.z.object({
801
204
  /** True when the authenticated caller authored this post. */
802
- isAuthor: zod.z.boolean().default(false)
205
+ isAuthor: zod.z.boolean().default(false),
206
+ /**
207
+ * Whether the caller may reply to this post (reply gating, §6). A prompt is
208
+ * repliable by any authenticated viewer (the app's audience-policy default);
209
+ * a reply only by its branch participants (`{ creator, branch responder }`).
210
+ */
211
+ canReply: zod.z.boolean().default(false),
212
+ /** Why `canReply` is false, when it is (omitted when the caller can reply). */
213
+ replyDisabledReason: zod.z.enum(["unauthenticated", "not_a_participant"]).optional()
803
214
  });
804
215
  var PostRecordPublicSchema = zod.z.object({
805
216
  text: zod.z.string(),
@@ -812,9 +223,13 @@ var PostRecordPublicSchema = zod.z.object({
812
223
  var AudioPostViewSchema = zod.z.object({
813
224
  /** at:// URI (or internal ref) identifying the post. */
814
225
  uri: zod.z.string(),
815
- cid: zod.z.string().optional(),
226
+ /** Content CID of the canonical record (see `AudioPostRecordSchema.cid`). */
227
+ cid: zod.z.string(),
816
228
  kind: zod.z.enum(["prompt", "reply"]),
817
- author: ProfileViewBasicSchema,
229
+ /** The acting actor's app-scoped id — an opaque attribution ref, not a profile. */
230
+ authorId: zod.z.string(),
231
+ /** The author's app-asserted AT Protocol DID, when the caller provided one. */
232
+ authorDid: zod.z.string().optional(),
818
233
  record: PostRecordPublicSchema,
819
234
  /** Hydrated audio embed (signed URL + lifted transcript). */
820
235
  embed: AudioEmbedViewSchema.optional(),