@antiphony/shared 0.1.0 → 0.3.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 (54) hide show
  1. package/dist/cjs/api-codecs.cjs +26 -650
  2. package/dist/cjs/api-codecs.d.cts +15 -233
  3. package/dist/cjs/index.cjs +29 -761
  4. package/dist/cjs/index.d.cts +4 -4
  5. package/dist/cjs/nsid.cjs +3 -10
  6. package/dist/cjs/nsid.d.cts +1 -9
  7. package/dist/cjs/types/api.cjs +11 -571
  8. package/dist/cjs/types/api.d.cts +13 -265
  9. package/dist/cjs/types/audio.cjs +26 -547
  10. package/dist/cjs/types/audio.d.cts +93 -52
  11. package/dist/cjs/types/records.cjs +3 -328
  12. package/dist/cjs/types/records.d.cts +7 -729
  13. package/dist/cjs/types/views.cjs +8 -649
  14. package/dist/cjs/types/views.d.cts +43 -9123
  15. package/dist/esm/api-codecs.d.ts +46 -264
  16. package/dist/esm/api-codecs.js +5 -5
  17. package/dist/esm/{chunk-ORMEWXMH.js → chunk-5JBD5THX.js} +1 -16
  18. package/dist/esm/{chunk-XDBKR6LW.js → chunk-6LROFBKK.js} +3 -10
  19. package/dist/esm/{chunk-F2CANZZ7.js → chunk-BNNLHRH7.js} +1 -1
  20. package/dist/esm/{chunk-BUNMR4ZO.js → chunk-F7IHVM34.js} +20 -3
  21. package/dist/esm/chunk-FDM5FDN4.js +46 -0
  22. package/dist/esm/chunk-IHIBHQBJ.js +78 -0
  23. package/dist/esm/chunk-R6SBR3IG.js +97 -0
  24. package/dist/esm/chunk-Y6HNNRVD.js +12 -0
  25. package/dist/esm/errors/index.js +1 -1
  26. package/dist/esm/index.d.ts +4 -4
  27. package/dist/esm/index.js +8 -8
  28. package/dist/esm/nsid.d.ts +1 -9
  29. package/dist/esm/nsid.js +2 -2
  30. package/dist/esm/observability/index.js +2 -2
  31. package/dist/esm/observability/report-error.js +2 -2
  32. package/dist/esm/types/api.d.ts +15 -267
  33. package/dist/esm/types/api.js +4 -5
  34. package/dist/esm/types/audio.d.ts +170 -129
  35. package/dist/esm/types/audio.js +4 -4
  36. package/dist/esm/types/blob.js +1 -1
  37. package/dist/esm/types/records.d.ts +10 -732
  38. package/dist/esm/types/records.js +2 -3
  39. package/dist/esm/types/views.d.ts +51 -9131
  40. package/dist/esm/types/views.js +3 -4
  41. package/dist/esm/utils/index.js +1 -1
  42. package/package.json +1 -1
  43. package/dist/cjs/types/channels.cjs +0 -170
  44. package/dist/cjs/types/channels.d.cts +0 -262
  45. package/dist/cjs/types/storage.cjs +0 -414
  46. package/dist/cjs/types/storage.d.cts +0 -197
  47. package/dist/esm/chunk-3WZJXJNU.js +0 -378
  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/types/channels.d.ts +0 -262
  52. package/dist/esm/types/channels.js +0 -162
  53. package/dist/esm/types/storage.d.ts +0 -197
  54. package/dist/esm/types/storage.js +0 -26
@@ -303,6 +303,15 @@ declare const AudioPostRecordSchema: z.ZodEffects<z.ZodObject<{
303
303
  orgId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
304
304
  /** Denormalized from `reply` presence: `reply` set ⇒ 'reply', else 'prompt'. */
305
305
  kind: z.ZodEnum<["prompt", "reply"]>;
306
+ /**
307
+ * Branch participant pair (author ids) for reply gating — the parties to a
308
+ * reply's sub-thread: the creator (thread-root author) + the responder who
309
+ * opened the branch. Set on replies (deduped, 1–2 ids); absent on prompts
310
+ * (a prompt's repliers are the app's audience policy, not a fixed pair).
311
+ * Inherited down the branch so reply gating is an O(1) field check, never a
312
+ * thread walk. See `specs/antiphony-data-model.md` §6 "Reply gating".
313
+ */
314
+ threadParticipants: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
306
315
  /** User-authored text (bsky-semantic). May be empty for pure-audio posts. NEVER the transcript. */
307
316
  text: z.ZodString;
308
317
  /** Optional headline; a prompt feature, not the discriminator. */
@@ -406,13 +415,12 @@ declare const AudioPostRecordSchema: z.ZodEffects<z.ZodObject<{
406
415
  }, "strip", z.ZodTypeAny, {
407
416
  id: string;
408
417
  createdAt: Date;
409
- authorId: string;
410
418
  text: string;
411
419
  originAppId: string;
420
+ authorId: string;
412
421
  kind: "prompt" | "reply";
413
- orgId?: string | null | undefined;
414
- title?: string | undefined;
415
422
  authorDid?: string | undefined;
423
+ orgId?: string | null | undefined;
416
424
  reply?: {
417
425
  root: {
418
426
  uri: string;
@@ -423,6 +431,8 @@ declare const AudioPostRecordSchema: z.ZodEffects<z.ZodObject<{
423
431
  cid: string;
424
432
  };
425
433
  } | undefined;
434
+ threadParticipants?: string[] | undefined;
435
+ title?: string | undefined;
426
436
  embed?: {
427
437
  $type: "dev.antiphony.embed.audio";
428
438
  audio: {
@@ -439,14 +449,13 @@ declare const AudioPostRecordSchema: z.ZodEffects<z.ZodObject<{
439
449
  selfLabels?: string[] | undefined;
440
450
  }, {
441
451
  id: string;
442
- authorId: string;
443
452
  text: string;
444
453
  originAppId: string;
454
+ authorId: string;
445
455
  kind: "prompt" | "reply";
446
456
  createdAt?: unknown;
447
- orgId?: string | null | undefined;
448
- title?: string | undefined;
449
457
  authorDid?: string | undefined;
458
+ orgId?: string | null | undefined;
450
459
  reply?: {
451
460
  root: {
452
461
  uri: string;
@@ -457,6 +466,8 @@ declare const AudioPostRecordSchema: z.ZodEffects<z.ZodObject<{
457
466
  cid: string;
458
467
  };
459
468
  } | undefined;
469
+ threadParticipants?: string[] | undefined;
470
+ title?: string | undefined;
460
471
  embed?: {
461
472
  $type: "dev.antiphony.embed.audio";
462
473
  audio: {
@@ -474,13 +485,12 @@ declare const AudioPostRecordSchema: z.ZodEffects<z.ZodObject<{
474
485
  }>, {
475
486
  id: string;
476
487
  createdAt: Date;
477
- authorId: string;
478
488
  text: string;
479
489
  originAppId: string;
490
+ authorId: string;
480
491
  kind: "prompt" | "reply";
481
- orgId?: string | null | undefined;
482
- title?: string | undefined;
483
492
  authorDid?: string | undefined;
493
+ orgId?: string | null | undefined;
484
494
  reply?: {
485
495
  root: {
486
496
  uri: string;
@@ -491,6 +501,8 @@ declare const AudioPostRecordSchema: z.ZodEffects<z.ZodObject<{
491
501
  cid: string;
492
502
  };
493
503
  } | undefined;
504
+ threadParticipants?: string[] | undefined;
505
+ title?: string | undefined;
494
506
  embed?: {
495
507
  $type: "dev.antiphony.embed.audio";
496
508
  audio: {
@@ -507,14 +519,13 @@ declare const AudioPostRecordSchema: z.ZodEffects<z.ZodObject<{
507
519
  selfLabels?: string[] | undefined;
508
520
  }, {
509
521
  id: string;
510
- authorId: string;
511
522
  text: string;
512
523
  originAppId: string;
524
+ authorId: string;
513
525
  kind: "prompt" | "reply";
514
526
  createdAt?: unknown;
515
- orgId?: string | null | undefined;
516
- title?: string | undefined;
517
527
  authorDid?: string | undefined;
528
+ orgId?: string | null | undefined;
518
529
  reply?: {
519
530
  root: {
520
531
  uri: string;
@@ -525,6 +536,8 @@ declare const AudioPostRecordSchema: z.ZodEffects<z.ZodObject<{
525
536
  cid: string;
526
537
  };
527
538
  } | undefined;
539
+ threadParticipants?: string[] | undefined;
540
+ title?: string | undefined;
528
541
  embed?: {
529
542
  $type: "dev.antiphony.embed.audio";
530
543
  audio: {
@@ -663,10 +676,22 @@ type ActorProfileRecord = z.infer<typeof ActorProfileRecordSchema>;
663
676
  declare const ViewerStateSchema: z.ZodObject<{
664
677
  /** True when the authenticated caller authored this post. */
665
678
  isAuthor: z.ZodDefault<z.ZodBoolean>;
679
+ /**
680
+ * Whether the caller may reply to this post (reply gating, §6). A prompt is
681
+ * repliable by any authenticated viewer (the app's audience-policy default);
682
+ * a reply only by its branch participants (`{ creator, branch responder }`).
683
+ */
684
+ canReply: z.ZodDefault<z.ZodBoolean>;
685
+ /** Why `canReply` is false, when it is (omitted when the caller can reply). */
686
+ replyDisabledReason: z.ZodOptional<z.ZodEnum<["unauthenticated", "not_a_participant"]>>;
666
687
  }, "strip", z.ZodTypeAny, {
667
688
  isAuthor: boolean;
689
+ canReply: boolean;
690
+ replyDisabledReason?: "unauthenticated" | "not_a_participant" | undefined;
668
691
  }, {
669
692
  isAuthor?: boolean | undefined;
693
+ canReply?: boolean | undefined;
694
+ replyDisabledReason?: "unauthenticated" | "not_a_participant" | undefined;
670
695
  }>;
671
696
  type ViewerState = z.infer<typeof ViewerStateSchema>;
672
697
  /**
@@ -722,7 +747,6 @@ declare const PostRecordPublicSchema: z.ZodObject<{
722
747
  }, "strip", z.ZodTypeAny, {
723
748
  createdAt: Date;
724
749
  text: string;
725
- title?: string | undefined;
726
750
  reply?: {
727
751
  root: {
728
752
  uri: string;
@@ -733,12 +757,12 @@ declare const PostRecordPublicSchema: z.ZodObject<{
733
757
  cid: string;
734
758
  };
735
759
  } | undefined;
760
+ title?: string | undefined;
736
761
  langs?: string[] | undefined;
737
762
  selfLabels?: string[] | undefined;
738
763
  }, {
739
764
  text: string;
740
765
  createdAt?: unknown;
741
- title?: string | undefined;
742
766
  reply?: {
743
767
  root: {
744
768
  uri: string;
@@ -749,6 +773,7 @@ declare const PostRecordPublicSchema: z.ZodObject<{
749
773
  cid: string;
750
774
  };
751
775
  } | undefined;
776
+ title?: string | undefined;
752
777
  langs?: string[] | undefined;
753
778
  selfLabels?: string[] | undefined;
754
779
  }>;
@@ -904,7 +929,6 @@ declare const AudioPostViewSchema: z.ZodObject<{
904
929
  }, "strip", z.ZodTypeAny, {
905
930
  createdAt: Date;
906
931
  text: string;
907
- title?: string | undefined;
908
932
  reply?: {
909
933
  root: {
910
934
  uri: string;
@@ -915,12 +939,12 @@ declare const AudioPostViewSchema: z.ZodObject<{
915
939
  cid: string;
916
940
  };
917
941
  } | undefined;
942
+ title?: string | undefined;
918
943
  langs?: string[] | undefined;
919
944
  selfLabels?: string[] | undefined;
920
945
  }, {
921
946
  text: string;
922
947
  createdAt?: unknown;
923
- title?: string | undefined;
924
948
  reply?: {
925
949
  root: {
926
950
  uri: string;
@@ -931,6 +955,7 @@ declare const AudioPostViewSchema: z.ZodObject<{
931
955
  cid: string;
932
956
  };
933
957
  } | undefined;
958
+ title?: string | undefined;
934
959
  langs?: string[] | undefined;
935
960
  selfLabels?: string[] | undefined;
936
961
  }>;
@@ -1012,30 +1037,26 @@ declare const AudioPostViewSchema: z.ZodObject<{
1012
1037
  viewer: z.ZodObject<{
1013
1038
  /** True when the authenticated caller authored this post. */
1014
1039
  isAuthor: z.ZodDefault<z.ZodBoolean>;
1040
+ /**
1041
+ * Whether the caller may reply to this post (reply gating, §6). A prompt is
1042
+ * repliable by any authenticated viewer (the app's audience-policy default);
1043
+ * a reply only by its branch participants (`{ creator, branch responder }`).
1044
+ */
1045
+ canReply: z.ZodDefault<z.ZodBoolean>;
1046
+ /** Why `canReply` is false, when it is (omitted when the caller can reply). */
1047
+ replyDisabledReason: z.ZodOptional<z.ZodEnum<["unauthenticated", "not_a_participant"]>>;
1015
1048
  }, "strip", z.ZodTypeAny, {
1016
1049
  isAuthor: boolean;
1050
+ canReply: boolean;
1051
+ replyDisabledReason?: "unauthenticated" | "not_a_participant" | undefined;
1017
1052
  }, {
1018
1053
  isAuthor?: boolean | undefined;
1054
+ canReply?: boolean | undefined;
1055
+ replyDisabledReason?: "unauthenticated" | "not_a_participant" | undefined;
1019
1056
  }>;
1020
1057
  }, "strip", z.ZodTypeAny, {
1021
1058
  uri: string;
1022
- record: {
1023
- createdAt: Date;
1024
- text: string;
1025
- title?: string | undefined;
1026
- reply?: {
1027
- root: {
1028
- uri: string;
1029
- cid: string;
1030
- };
1031
- parent: {
1032
- uri: string;
1033
- cid: string;
1034
- };
1035
- } | undefined;
1036
- langs?: string[] | undefined;
1037
- selfLabels?: string[] | undefined;
1038
- };
1059
+ kind: "prompt" | "reply";
1039
1060
  author: {
1040
1061
  id: string;
1041
1062
  handle?: string | null | undefined;
@@ -1060,9 +1081,27 @@ declare const AudioPostViewSchema: z.ZodObject<{
1060
1081
  badges?: string[] | undefined;
1061
1082
  isVerified?: boolean | undefined;
1062
1083
  };
1063
- kind: "prompt" | "reply";
1084
+ record: {
1085
+ createdAt: Date;
1086
+ text: string;
1087
+ reply?: {
1088
+ root: {
1089
+ uri: string;
1090
+ cid: string;
1091
+ };
1092
+ parent: {
1093
+ uri: string;
1094
+ cid: string;
1095
+ };
1096
+ } | undefined;
1097
+ title?: string | undefined;
1098
+ langs?: string[] | undefined;
1099
+ selfLabels?: string[] | undefined;
1100
+ };
1064
1101
  viewer: {
1065
1102
  isAuthor: boolean;
1103
+ canReply: boolean;
1104
+ replyDisabledReason?: "unauthenticated" | "not_a_participant" | undefined;
1066
1105
  };
1067
1106
  cid?: string | undefined;
1068
1107
  embed?: {
@@ -1082,23 +1121,7 @@ declare const AudioPostViewSchema: z.ZodObject<{
1082
1121
  } | undefined;
1083
1122
  }, {
1084
1123
  uri: string;
1085
- record: {
1086
- text: string;
1087
- createdAt?: unknown;
1088
- title?: string | undefined;
1089
- reply?: {
1090
- root: {
1091
- uri: string;
1092
- cid: string;
1093
- };
1094
- parent: {
1095
- uri: string;
1096
- cid: string;
1097
- };
1098
- } | undefined;
1099
- langs?: string[] | undefined;
1100
- selfLabels?: string[] | undefined;
1101
- };
1124
+ kind: "prompt" | "reply";
1102
1125
  author: {
1103
1126
  id: string;
1104
1127
  handle?: string | null | undefined;
@@ -1123,9 +1146,27 @@ declare const AudioPostViewSchema: z.ZodObject<{
1123
1146
  badges?: string[] | undefined;
1124
1147
  isVerified?: boolean | undefined;
1125
1148
  };
1126
- kind: "prompt" | "reply";
1149
+ record: {
1150
+ text: string;
1151
+ createdAt?: unknown;
1152
+ reply?: {
1153
+ root: {
1154
+ uri: string;
1155
+ cid: string;
1156
+ };
1157
+ parent: {
1158
+ uri: string;
1159
+ cid: string;
1160
+ };
1161
+ } | undefined;
1162
+ title?: string | undefined;
1163
+ langs?: string[] | undefined;
1164
+ selfLabels?: string[] | undefined;
1165
+ };
1127
1166
  viewer: {
1128
1167
  isAuthor?: boolean | undefined;
1168
+ canReply?: boolean | undefined;
1169
+ replyDisabledReason?: "unauthenticated" | "not_a_participant" | undefined;
1129
1170
  };
1130
1171
  cid?: string | undefined;
1131
1172
  embed?: {
@@ -2,18 +2,6 @@
2
2
 
3
3
  var zod = require('zod');
4
4
 
5
- // types/records.ts
6
- var BlobRefSchema = zod.z.object({
7
- /** Discriminator for AT Protocol type system */
8
- $type: zod.z.literal("blob"),
9
- /** Content Identifier (CID) or URL pointing to the blob */
10
- ref: zod.z.string(),
11
- /** MIME type of the blob (e.g., 'audio/webm') */
12
- mimeType: zod.z.string(),
13
- /** Size of the blob in bytes */
14
- size: zod.z.number()
15
- });
16
-
17
5
  // types/records.ts
18
6
  function httpsUrl() {
19
7
  return zod.z.string().trim().url().refine((u) => /^https?:\/\//i.test(u), {
@@ -57,11 +45,8 @@ var UserRecordSchema = zod.z.object({
57
45
  handle: zod.z.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/).nullable().optional(),
58
46
  /** User stated intent (e.g. "Podcaster", "Listener") */
59
47
  usageIntent: zod.z.string().nullable().optional(),
60
- /**
61
- * Domain for federated handle support.
62
- * Defaults to 'voxpop.com'.
63
- */
64
- domain: zod.z.string().default("voxpop.com"),
48
+ /** Domain for federated handle support. */
49
+ domain: zod.z.string().default("antiphony.dev"),
65
50
  /**
66
51
  * Display Name (e.g. "Brad Thorson"). Nullable: Firestore stores `null`
67
52
  * when the user clears this field via the settings form, and the schema
@@ -89,319 +74,9 @@ var UserRecordSchema = zod.z.object({
89
74
  /** Account status. Deactivated accounts retain data but are excluded from lookups. */
90
75
  status: zod.z.enum(["active", "deactivated"]).default("active"),
91
76
  /** Timestamp when the account was deactivated (soft deleted) */
92
- deactivatedAt: FirestoreTimestampSchema.optional(),
93
- /**
94
- * Denormalized org memberships — { orgId: role } for fast lookup.
95
- * Source of truth is organizations/{orgId}/members/{userId}.
96
- * Kept in sync by Cloud Function trigger.
97
- */
98
- orgMemberships: zod.z.record(zod.z.string(), zod.z.enum(["owner", "admin", "member"])).optional()
99
- });
100
- var PromptRecordSchema = zod.z.object({
101
- /** Unique Prompt ID */
102
- id: zod.z.string(),
103
- /** ID of the User who created this prompt. Always a user ID, never an org ID. @see UserRecord */
104
- authorId: zod.z.string(),
105
- /** Organization context this prompt belongs to (null = personal/no org) */
106
- orgId: zod.z.string().nullable().optional(),
107
- /** The main text of the question/prompt */
108
- title: zod.z.string().min(3),
109
- /** Optional extra context */
110
- description: zod.z.string().nullable().optional(),
111
- /** User who created this prompt (differs from authorId when org-owned) */
112
- createdBy: zod.z.string().optional(),
113
- /** URL to the recorded audio file (GCS) */
114
- audioUrl: zod.z.string().url().or(zod.z.literal("")),
115
- /** AT Protocol blob reference (future replacement for audioUrl) */
116
- audio: BlobRefSchema.optional(),
117
- /**
118
- * AT Protocol URI returned by the publisher after a successful
119
- * `repo.putRecord` against the author's PDS — e.g.
120
- * `at://did:plc:abc123/com.voxpop.audio.prompt/3kj4...`. Optional,
121
- * no migration: existing rows leave it unset. Format-validated so
122
- * malformed strings surface at the schema-parse boundary rather than
123
- * being silently stored.
124
- */
125
- atprotoUri: zod.z.string().regex(/^at:\/\/.+/).optional(),
126
- /** Server timestamp of creation */
127
- createdAt: FirestoreTimestampSchema,
128
- /**
129
- * Life-cycle status.
130
- * - `live`: Visible and accepting replies.
131
- * - `archived`: Visible but closed for new replies.
132
- * - `deleted`: Soft deleted.
133
- */
134
- status: zod.z.enum(["live", "archived", "deleted"]).default("live"),
135
- /** AI Enrichment Fields */
136
- aiStatus: zod.z.enum(["pending", "complete", "error"]).optional(),
137
- aiError: zod.z.string().optional(),
138
- aiSummary: zod.z.string().optional(),
139
- aiLabels: zod.z.array(zod.z.string()).optional(),
140
- transcription: zod.z.string().optional(),
141
- /** Pre-computed waveform peaks (normalized 0–1) for instant audio visualization */
142
- waveformPeaks: zod.z.array(zod.z.number()).optional(),
143
- /** Social Share Video Fields */
144
- socialVideoUrl: zod.z.string().url().optional(),
145
- socialVideoStoragePath: zod.z.string().optional(),
146
- socialVideoStatus: zod.z.enum(["pending", "complete", "error"]).optional(),
147
- socialVideoError: zod.z.string().optional(),
148
- /** The audio URL/path used to generate the current video (for cache invalidation) */
149
- socialVideoSourceAudio: zod.z.string().optional()
150
- });
151
- var ReplyRecordSchema = zod.z.object({
152
- /** Unique Reply ID */
153
- id: zod.z.string(),
154
- /** The Prompt being replied to. @see PromptRecord */
155
- promptId: zod.z.string(),
156
- /** The User who replied. @see UserRecord */
157
- authorId: zod.z.string(),
158
- /** URL to the recorded audio file (GCS) */
159
- audioUrl: zod.z.string().url(),
160
- /** AT Protocol blob reference (future replacement for audioUrl) */
161
- audio: BlobRefSchema.optional(),
162
- /** Server timestamp of creation */
163
- createdAt: FirestoreTimestampSchema,
164
- /**
165
- * Life-cycle status.
166
- * - `live`: Visible and accepting replies.
167
- * - `archived`: Visible but closed for new replies.
168
- * - `deleted`: Soft deleted.
169
- */
170
- status: zod.z.enum(["live", "archived", "deleted"]).default("live"),
171
- /**
172
- * Pre-computed waveform peaks (normalized 0–1) for instant audio
173
- * visualization. **Stays on canonical** (does NOT move to the
174
- * enrichment doc) — produced by a plain ffmpeg pass at ingestion,
175
- * not an AI step; a self-hoster without paid-tier AI still needs
176
- * this on every reply for the audio player. See
177
- * `specs/ai-enrichment-split.md` § 5.
178
- */
179
- waveformPeaks: zod.z.array(zod.z.number()).optional(),
180
- /**
181
- * Duration of the audio in seconds, computed server-side from
182
- * ffmpeg. **Stays on canonical** (same reasoning as `waveformPeaks`
183
- * above — ingestion ffmpeg output, not an AI enrichment). See
184
- * `specs/ai-enrichment-split.md` § 5.
185
- */
186
- audioDurationSec: zod.z.number().optional()
187
- });
188
- var ReplyEnrichmentRecordSchema = zod.z.object({
189
- id: zod.z.string(),
190
- /** Private notes by the prompt author about this reply. */
191
- notes: zod.z.string().optional(),
192
- // === AI-enrichment fields (sole source of truth post Stage 4) ===
193
- //
194
- // These lifted off canonical in Stage 4 of `specs/ai-enrichment-split.md`.
195
- // Writers (`functions/`) route AI updates here via the split-write
196
- // helper in `functions/src/services/replyEnrichmentDualWrite.ts`;
197
- // readers source them via the hydrator's enrichment branch (see
198
- // `packages/core/services/hydration.ts`).
199
- // --- AI core (Gemini-generated) ---
200
- aiStatus: zod.z.enum(["pending", "complete", "error", "skipped_too_short"]).optional(),
201
- aiError: zod.z.string().optional(),
202
- aiSummary: zod.z.string().optional(),
203
- aiLabels: zod.z.array(zod.z.string()).optional(),
204
- transcription: zod.z.string().optional(),
205
- sentiment: zod.z.enum(["Positive", "Negative", "Neutral"]).optional(),
206
- /** Must match the widened enum in `ReplyRecordSchema.energyLevel`. */
207
- energyLevel: zod.z.enum(["High", "Low", "Neutral"]).optional(),
208
- engagementScore: zod.z.number().min(1).max(10).optional(),
209
- // --- Voice isolation (ElevenLabs, paid tier) ---
210
- /** Noise-reduced audio URL — replaces `audioUrl` for downstream players when present. PUBLIC. */
211
- enhancedAudioUrl: zod.z.string().url().optional(),
212
- /** Storage path companion to `enhancedAudioUrl` — private. */
213
- enhancedStoragePath: zod.z.string().optional(),
214
- // --- Social-share video (paid tier) ---
215
- //
216
- // All `socialVideo*` fields are creator-only. The URL points at the
217
- // generated artifact (a video file the creator can download and post
218
- // to social media); the reply detail page does NOT render it.
219
- socialVideoUrl: zod.z.string().url().optional(),
220
- socialVideoStoragePath: zod.z.string().optional(),
221
- socialVideoStatus: zod.z.enum(["pending", "complete", "error"]).optional(),
222
- socialVideoError: zod.z.string().optional(),
223
- /** The audio URL/path used to generate the current video (for cache invalidation). */
224
- socialVideoSourceAudio: zod.z.string().optional()
225
- });
226
- var PersonEnrichmentRecordSchema = zod.z.object({
227
- /** Composite key `${viewerUid}_${targetUid}`. */
228
- id: zod.z.string(),
229
- /** Per-viewer CRM notes about the target. */
230
- notes: zod.z.string().optional(),
231
- /** Per-viewer freeform tags about the target. */
232
- tags: zod.z.array(zod.z.string()).optional(),
233
- // === Identity-merge: alias set ===
234
- //
235
- // Other identifiers the viewer has declared to be the SAME person as
236
- // this entry's `targetUid`. vCard/URI-aligned: each entry is a
237
- // scheme-prefixed identifier, so one field generalizes across networks
238
- // AND across the future contact-sync import (which carries phone/email):
239
- // "uid:<firebaseUid>" | "did:plc:…" | "handle:bob.bsky.social"
240
- // | "tel:+15551234567" | "mailto:x@example.com"
241
- // Mirrors vCard 4.0's `UID` + `CLIENTPIDMAP`/`PID` source-tracking
242
- // (RFC 6350) — the standard answer to "the same person from multiple
243
- // sources". See `specs/people-lexicon-prior-art.md`. The People
244
- // read-path collapses any replier whose "uid:<uid>" appears here into
245
- // this primary entry. Empty until the merge feature is used.
246
- aliases: zod.z.array(zod.z.string()).optional(),
247
- lastUpdated: FirestoreTimestampSchema.optional()
248
- });
249
- var SipEnrichmentSchema = zod.z.object({
250
- sipUri: zod.z.string(),
251
- sipUsername: zod.z.string(),
252
- sipSecret: zod.z.string(),
253
- provider: zod.z.enum(["twilio", "plivo", "internal"])
254
- });
255
- var CallForwardingConfigSchema = zod.z.object({
256
- /** User's personal phone number (E.164) */
257
- phoneNumber: zod.z.string(),
258
- /** Line type from Twilio Lookup v2 */
259
- lineType: zod.z.string().nullable(),
260
- /** Carrier name from Twilio Lookup v2 */
261
- carrier: zod.z.string().nullable(),
262
- /** free = shared Twilio number (ForwardedFrom routing), paid = dedicated number */
263
- tier: zod.z.enum(["free", "paid"]),
264
- /** The VoxPop Twilio number calls forward to (E.164) */
265
- voxpopNumber: zod.z.string(),
266
- /** Twilio Phone Number SID (paid tier only) */
267
- twilioNumberSid: zod.z.string().nullable().optional(),
268
- /** Forwarding verification state */
269
- verificationStatus: zod.z.enum(["pending", "verifying", "verified", "failed"]).default("pending"),
270
- /** Last verification attempt timestamp */
271
- lastVerificationAt: FirestoreTimestampSchema.optional(),
272
- /** Number of verification attempts */
273
- verificationAttempts: zod.z.number().default(0),
274
- /** Reason for verification failure */
275
- failureReason: zod.z.string().nullable().optional(),
276
- /** Whether call forwarding is active */
277
- enabled: zod.z.boolean().default(false),
278
- createdAt: FirestoreTimestampSchema,
279
- updatedAt: FirestoreTimestampSchema
280
- });
281
- var ConnectorTypeSchema = zod.z.enum(["telephony"]);
282
- var ConnectorStatusSchema = zod.z.object({
283
- /** Coarse, generic lifecycle the control-plane UI can render without
284
- * connector-specific knowledge. */
285
- state: zod.z.enum(["unconfigured", "pending", "active", "error", "disabled"]).default("unconfigured"),
286
- detail: zod.z.string().nullable().optional(),
287
- /** Opaque, connector-specific status detail (e.g. telephony's
288
- * `{ verificationStatus, verificationAttempts, failureReason }`). Connector-
289
- * owned like the rest of `status` — never user-writable. Nullable so a doc
290
- * that stored `data: null` round-trips without a parse error. */
291
- data: zod.z.record(zod.z.unknown()).nullable().optional(),
292
- updatedAt: FirestoreTimestampSchema.optional()
293
- });
294
- var ConnectorConfigRecordSchema = zod.z.object({
295
- connectorType: ConnectorTypeSchema,
296
- /** The owning user. Org-scoping (ownerId as an orgId) is a later step. */
297
- ownerId: zod.z.string(),
298
- /** Opaque, connector-specific blob. Core does not interpret this. */
299
- settings: zod.z.record(zod.z.unknown()).default({}),
300
- /** Reference into a secret store (e.g. Secret Manager key). Never a raw secret. */
301
- secretRef: zod.z.string().nullable().optional(),
302
- enabled: zod.z.boolean().default(false),
303
- status: ConnectorStatusSchema.default({ state: "unconfigured" }),
304
- createdAt: FirestoreTimestampSchema,
305
- updatedAt: FirestoreTimestampSchema
306
- });
307
- var ScreeningRuleRecordSchema = zod.z.object({
308
- id: zod.z.string(),
309
- ownerId: zod.z.string(),
310
- /** The caller number this rule matches (E.164). Format-enforced at the
311
- * record level so any writer (API, future contact-sync/callback) can't
312
- * persist a malformed number. */
313
- e164: zod.z.string().regex(/^\+[1-9]\d{6,14}$/, "Must be an E.164 phone number"),
314
- /** Display label, e.g. "Mom" / "Delta Airlines". null = unlabeled. */
315
- label: zod.z.string().nullable().optional(),
316
- /** `allow` = ring through; `screen` = async voicemail. */
317
- action: zod.z.enum(["allow", "screen"]),
318
- /** Provenance. `manual` = user-created; the others are future writers. */
319
- source: zod.z.enum(["manual", "contact-sync", "callback"]),
320
- /** null = permanent; a date = self-expiring exception. */
321
- expiresAt: FirestoreTimestampSchema.nullable().optional(),
322
- createdAt: FirestoreTimestampSchema
323
- });
324
- var OrganizationRecordSchema = zod.z.object({
325
- id: zod.z.string(),
326
- /** Display name of the organization */
327
- name: zod.z.string().min(3).max(50),
328
- /**
329
- * Unique handle for the organization (e.g. voxpop.com/@mypodcast).
330
- * Used for public URLs.
331
- */
332
- slug: zod.z.string().min(3).max(30).regex(/^[a-z0-9-]+$/),
333
- /** URL to avatar/logo */
334
- avatarUrl: zod.z.string().url().optional(),
335
- /** URL to the podcast RSS feed */
336
- rssFeedUrl: zod.z.string().url().optional(),
337
- /** External website URL */
338
- websiteUrl: httpsUrl().optional(),
339
- /** Description or tagline */
340
- description: zod.z.string().optional(),
341
- /** Owner ID (User ID) */
342
- ownerId: zod.z.string(),
343
- /** Server timestamp of creation */
344
- createdAt: FirestoreTimestampSchema,
345
- /** Verified domain (e.g., "acme.com") — enables auto-join, enterprise features */
346
- domain: zod.z.string().nullable().optional(),
347
- /** Whether the domain has been verified via DNS TXT record */
348
- domainVerified: zod.z.boolean().default(false),
349
- /** DNS verification token (stored server-side until verification completes) */
350
- domainVerificationToken: zod.z.string().optional(),
351
- /** Billing email — required for paid orgs, where invoices go */
352
- billingEmail: zod.z.string().email().nullable().optional(),
353
- /**
354
- * Tier determines isolation and feature set. Orgs are meant to be paid
355
- * (business or enterprise); free/pro exist on the enum for the individual track.
356
- * Default is provisionally `business` (the entry paid tier) — it's assigned at
357
- * creation with no subscription behind it yet, so it stays provisional until billing
358
- * promotes/reconciles the org. See docs/tech-debt.md "Org tier billing reconciliation".
359
- */
360
- tier: zod.z.enum(["free", "pro", "business", "enterprise"]).default("business"),
361
- /** Stripe Customer ID */
362
- stripeCustomerId: zod.z.string().optional(),
363
- /** Subscription Status */
364
- subscriptionStatus: zod.z.enum(["active", "trialing", "past_due", "canceled", "unpaid"]).optional()
365
- });
366
- var OrganizationMemberRecordSchema = zod.z.object({
367
- orgId: zod.z.string(),
368
- userId: zod.z.string(),
369
- /** Role in the organization */
370
- role: zod.z.enum(["owner", "admin", "member"]),
371
- /** Server timestamp of joining */
372
- joinedAt: FirestoreTimestampSchema,
373
- invitedBy: zod.z.string().optional()
374
- });
375
- var OrgInviteRecordSchema = zod.z.object({
376
- id: zod.z.string(),
377
- orgId: zod.z.string(),
378
- /** Email the invite was sent to */
379
- email: zod.z.string().email(),
380
- /** Role to assign on acceptance */
381
- role: zod.z.enum(["admin", "member"]),
382
- /** User ID of who sent the invite */
383
- invitedBy: zod.z.string(),
384
- /** Invite lifecycle status */
385
- status: zod.z.enum(["pending", "accepted", "expired", "revoked"]).default("pending"),
386
- /** Server timestamp of creation */
387
- createdAt: FirestoreTimestampSchema,
388
- /** When this invite expires */
389
- expiresAt: FirestoreTimestampSchema
77
+ deactivatedAt: FirestoreTimestampSchema.optional()
390
78
  });
391
79
 
392
- exports.CallForwardingConfigSchema = CallForwardingConfigSchema;
393
- exports.ConnectorConfigRecordSchema = ConnectorConfigRecordSchema;
394
- exports.ConnectorStatusSchema = ConnectorStatusSchema;
395
- exports.ConnectorTypeSchema = ConnectorTypeSchema;
396
80
  exports.FirestoreTimestampSchema = FirestoreTimestampSchema;
397
- exports.OrgInviteRecordSchema = OrgInviteRecordSchema;
398
- exports.OrganizationMemberRecordSchema = OrganizationMemberRecordSchema;
399
- exports.OrganizationRecordSchema = OrganizationRecordSchema;
400
- exports.PersonEnrichmentRecordSchema = PersonEnrichmentRecordSchema;
401
- exports.PromptRecordSchema = PromptRecordSchema;
402
- exports.ReplyEnrichmentRecordSchema = ReplyEnrichmentRecordSchema;
403
- exports.ReplyRecordSchema = ReplyRecordSchema;
404
- exports.ScreeningRuleRecordSchema = ScreeningRuleRecordSchema;
405
- exports.SipEnrichmentSchema = SipEnrichmentSchema;
406
81
  exports.UserRecordSchema = UserRecordSchema;
407
82
  exports.httpsUrl = httpsUrl;