@oxy.so/contracts 1.0.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 (147) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +16 -0
  3. package/dist/cjs/.tsbuildinfo +1 -0
  4. package/dist/cjs/accountGraph.js +489 -0
  5. package/dist/cjs/agency.js +439 -0
  6. package/dist/cjs/browserHub.js +215 -0
  7. package/dist/cjs/civic.js +163 -0
  8. package/dist/cjs/commonsSignIn.js +59 -0
  9. package/dist/cjs/deviceBoot.js +50 -0
  10. package/dist/cjs/deviceDirectory.js +189 -0
  11. package/dist/cjs/devicePairing.js +138 -0
  12. package/dist/cjs/deviceSession.js +164 -0
  13. package/dist/cjs/emailAgentContext.js +32 -0
  14. package/dist/cjs/followGraph.js +28 -0
  15. package/dist/cjs/identity.js +258 -0
  16. package/dist/cjs/inboxPush.js +24 -0
  17. package/dist/cjs/index.js +618 -0
  18. package/dist/cjs/inference/accountBilling.js +334 -0
  19. package/dist/cjs/inference/aliaModelRelease.js +262 -0
  20. package/dist/cjs/inference/attribution.js +106 -0
  21. package/dist/cjs/inference/catalogue.js +487 -0
  22. package/dist/cjs/inference/entitlement.js +217 -0
  23. package/dist/cjs/inference/errors.js +309 -0
  24. package/dist/cjs/inference/identifiers.js +224 -0
  25. package/dist/cjs/inference/inbox.js +105 -0
  26. package/dist/cjs/inference/modelDocumentation.js +433 -0
  27. package/dist/cjs/inference/money.js +188 -0
  28. package/dist/cjs/inference/priceVersion.js +110 -0
  29. package/dist/cjs/inference/providerConnection.js +455 -0
  30. package/dist/cjs/inference/request.js +477 -0
  31. package/dist/cjs/inference/routingPolicy.js +318 -0
  32. package/dist/cjs/inference/streamEvents.js +258 -0
  33. package/dist/cjs/inference/usage.js +329 -0
  34. package/dist/cjs/inference/version.js +105 -0
  35. package/dist/cjs/keyRecovery.js +91 -0
  36. package/dist/cjs/keyRotation.js +75 -0
  37. package/dist/cjs/links.js +68 -0
  38. package/dist/cjs/moderationReputation.js +298 -0
  39. package/dist/cjs/oauth.js +66 -0
  40. package/dist/cjs/oxyRecordTypes.js +71 -0
  41. package/dist/cjs/protocol.js +53 -0
  42. package/dist/cjs/recommendations.js +168 -0
  43. package/dist/cjs/reputation.js +297 -0
  44. package/dist/cjs/sessionStatus.js +121 -0
  45. package/dist/cjs/transparency.js +89 -0
  46. package/dist/cjs/updates.js +252 -0
  47. package/dist/cjs/userInvalidation.js +89 -0
  48. package/dist/cjs/userResponse.js +245 -0
  49. package/dist/cjs/username.js +290 -0
  50. package/dist/cjs/webauthn.js +71 -0
  51. package/dist/esm/.tsbuildinfo +1 -0
  52. package/dist/esm/accountGraph.js +480 -0
  53. package/dist/esm/agency.js +436 -0
  54. package/dist/esm/browserHub.js +212 -0
  55. package/dist/esm/civic.js +160 -0
  56. package/dist/esm/commonsSignIn.js +56 -0
  57. package/dist/esm/deviceBoot.js +47 -0
  58. package/dist/esm/deviceDirectory.js +186 -0
  59. package/dist/esm/devicePairing.js +135 -0
  60. package/dist/esm/deviceSession.js +161 -0
  61. package/dist/esm/emailAgentContext.js +29 -0
  62. package/dist/esm/followGraph.js +27 -0
  63. package/dist/esm/identity.js +255 -0
  64. package/dist/esm/inboxPush.js +21 -0
  65. package/dist/esm/index.js +172 -0
  66. package/dist/esm/inference/accountBilling.js +331 -0
  67. package/dist/esm/inference/aliaModelRelease.js +259 -0
  68. package/dist/esm/inference/attribution.js +103 -0
  69. package/dist/esm/inference/catalogue.js +484 -0
  70. package/dist/esm/inference/entitlement.js +214 -0
  71. package/dist/esm/inference/errors.js +306 -0
  72. package/dist/esm/inference/identifiers.js +221 -0
  73. package/dist/esm/inference/inbox.js +102 -0
  74. package/dist/esm/inference/modelDocumentation.js +430 -0
  75. package/dist/esm/inference/money.js +185 -0
  76. package/dist/esm/inference/priceVersion.js +107 -0
  77. package/dist/esm/inference/providerConnection.js +452 -0
  78. package/dist/esm/inference/request.js +474 -0
  79. package/dist/esm/inference/routingPolicy.js +315 -0
  80. package/dist/esm/inference/streamEvents.js +255 -0
  81. package/dist/esm/inference/usage.js +326 -0
  82. package/dist/esm/inference/version.js +102 -0
  83. package/dist/esm/keyRecovery.js +88 -0
  84. package/dist/esm/keyRotation.js +72 -0
  85. package/dist/esm/links.js +65 -0
  86. package/dist/esm/moderationReputation.js +295 -0
  87. package/dist/esm/oauth.js +63 -0
  88. package/dist/esm/oxyRecordTypes.js +68 -0
  89. package/dist/esm/protocol.js +50 -0
  90. package/dist/esm/recommendations.js +165 -0
  91. package/dist/esm/reputation.js +293 -0
  92. package/dist/esm/sessionStatus.js +118 -0
  93. package/dist/esm/transparency.js +86 -0
  94. package/dist/esm/updates.js +249 -0
  95. package/dist/esm/userInvalidation.js +85 -0
  96. package/dist/esm/userResponse.js +240 -0
  97. package/dist/esm/username.js +283 -0
  98. package/dist/esm/webauthn.js +68 -0
  99. package/dist/types/.tsbuildinfo +1 -0
  100. package/dist/types/accountGraph.d.ts +378 -0
  101. package/dist/types/agency.d.ts +2162 -0
  102. package/dist/types/browserHub.d.ts +856 -0
  103. package/dist/types/civic.d.ts +338 -0
  104. package/dist/types/commonsSignIn.d.ts +58 -0
  105. package/dist/types/deviceBoot.d.ts +74 -0
  106. package/dist/types/deviceDirectory.d.ts +1317 -0
  107. package/dist/types/devicePairing.d.ts +130 -0
  108. package/dist/types/deviceSession.d.ts +411 -0
  109. package/dist/types/emailAgentContext.d.ts +248 -0
  110. package/dist/types/followGraph.d.ts +150 -0
  111. package/dist/types/identity.d.ts +402 -0
  112. package/dist/types/inboxPush.d.ts +30 -0
  113. package/dist/types/index.d.ts +100 -0
  114. package/dist/types/inference/accountBilling.d.ts +738 -0
  115. package/dist/types/inference/aliaModelRelease.d.ts +609 -0
  116. package/dist/types/inference/attribution.d.ts +176 -0
  117. package/dist/types/inference/catalogue.d.ts +1618 -0
  118. package/dist/types/inference/entitlement.d.ts +519 -0
  119. package/dist/types/inference/errors.d.ts +242 -0
  120. package/dist/types/inference/identifiers.d.ts +182 -0
  121. package/dist/types/inference/inbox.d.ts +374 -0
  122. package/dist/types/inference/modelDocumentation.d.ts +1603 -0
  123. package/dist/types/inference/money.d.ts +185 -0
  124. package/dist/types/inference/priceVersion.d.ts +182 -0
  125. package/dist/types/inference/providerConnection.d.ts +968 -0
  126. package/dist/types/inference/request.d.ts +2800 -0
  127. package/dist/types/inference/routingPolicy.d.ts +616 -0
  128. package/dist/types/inference/streamEvents.d.ts +950 -0
  129. package/dist/types/inference/usage.d.ts +1164 -0
  130. package/dist/types/inference/version.d.ts +102 -0
  131. package/dist/types/keyRecovery.d.ts +138 -0
  132. package/dist/types/keyRotation.d.ts +103 -0
  133. package/dist/types/links.d.ts +96 -0
  134. package/dist/types/moderationReputation.d.ts +487 -0
  135. package/dist/types/oauth.d.ts +86 -0
  136. package/dist/types/oxyRecordTypes.d.ts +62 -0
  137. package/dist/types/protocol.d.ts +86 -0
  138. package/dist/types/recommendations.d.ts +542 -0
  139. package/dist/types/reputation.d.ts +457 -0
  140. package/dist/types/sessionStatus.d.ts +231 -0
  141. package/dist/types/transparency.d.ts +392 -0
  142. package/dist/types/updates.d.ts +545 -0
  143. package/dist/types/userInvalidation.d.ts +94 -0
  144. package/dist/types/userResponse.d.ts +1706 -0
  145. package/dist/types/username.d.ts +265 -0
  146. package/dist/types/webauthn.d.ts +77 -0
  147. package/package.json +87 -0
@@ -0,0 +1,252 @@
1
+ "use strict";
2
+ /**
3
+ * Oxy Updates (self-hosted expo-updates protocol) — publish/admin API contracts.
4
+ *
5
+ * SINGLE SOURCE OF TRUTH for the wire shape of the AUTHENTICATED publish/admin
6
+ * surface that the `oxy-ship` CLI and the console Updates tab call. The PUBLIC
7
+ * manifest endpoint (`GET /updates/v1/apps/:clientId/manifest`) speaks the
8
+ * expo-updates v1 protocol verbatim (multipart/mixed, signed) and is therefore
9
+ * NOT modelled here — its shape is dictated by the Expo spec, not by us.
10
+ *
11
+ * The API validates its OUTPUT against these schemas; every consumer (the ship
12
+ * CLI, the console hook) validates its INPUT against the same definitions, so
13
+ * producer and consumers cannot drift.
14
+ *
15
+ * Domain model (mirrors the Mongoose models in `@oxy.so/api`):
16
+ * - A `channel` (e.g. `production`, `preview`, `pr-123`) is a named release
17
+ * track for one application.
18
+ * - An `update` is one published bundle for a single `(channel, runtimeVersion,
19
+ * platform)`. Its `updateId` is a UUIDv4 (the client parses it as a UUID).
20
+ * The HEAD of a track is the newest `published` update for that tuple.
21
+ * - An `asset` is content-addressed by its `sha256`; assets are shared across
22
+ * updates and applications (an unchanged JS bundle is uploaded once).
23
+ *
24
+ * Platform-agnostic — zod only, no react/react-native/expo. ESM-safe (no
25
+ * `require()`).
26
+ */
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.updateRolloutPatchSchema = exports.promoteRequestSchema = exports.rollbackToEmbeddedRequestSchema = exports.rollbackRequestSchema = exports.updateListResponseSchema = exports.channelListResponseSchema = exports.channelSchema = exports.rollbackToEmbeddedEntrySchema = exports.createUpdateResponseSchema = exports.updateSchema = exports.createUpdateRequestSchema = exports.updateAssetRefSchema = exports.assetCompleteResponseSchema = exports.assetCompleteResultItemSchema = exports.assetCompleteRequestSchema = exports.assetInitResponseSchema = exports.assetUploadTicketSchema = exports.assetInitRequestSchema = exports.assetInitItemSchema = exports.rolloutPercentSchema = exports.runtimeVersionSchema = exports.channelNameSchema = exports.sha256HexSchema = exports.updateAssetStatusSchema = exports.updateStatusSchema = exports.updatePlatformSchema = void 0;
29
+ const zod_1 = require("zod");
30
+ /* -------------------------------------------------------------------------- */
31
+ /* Shared primitives */
32
+ /* -------------------------------------------------------------------------- */
33
+ /** The two platforms the expo-updates protocol addresses. */
34
+ exports.updatePlatformSchema = zod_1.z.enum(['ios', 'android']);
35
+ /** Lifecycle of a single published update row. */
36
+ exports.updateStatusSchema = zod_1.z.enum(['published', 'superseded', 'rolled_back']);
37
+ /** Upload lifecycle of a content-addressed asset. */
38
+ exports.updateAssetStatusSchema = zod_1.z.enum(['pending', 'uploaded']);
39
+ /** Lowercase-hex SHA-256 content hash (64 hex chars). */
40
+ exports.sha256HexSchema = zod_1.z
41
+ .string()
42
+ .regex(/^[a-f0-9]{64}$/, 'sha256 must be 64 lowercase hex characters');
43
+ /**
44
+ * A channel name. Constrained to a URL/path-safe slug so it can appear in the
45
+ * `expo-channel-name` header and be used CI-friendly for `pr-<n>` tracks. No
46
+ * colon (BullMQ/id safety) and no slash (path safety).
47
+ */
48
+ exports.channelNameSchema = zod_1.z
49
+ .string()
50
+ .min(1)
51
+ .max(100)
52
+ .regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/, 'channel name must be a URL-safe slug');
53
+ /** A runtime version string (expo-updates `runtimeVersion`, e.g. an appVersion). */
54
+ exports.runtimeVersionSchema = zod_1.z.string().min(1).max(255);
55
+ /** A rollout percentage: 0 rolls out to nobody, 100 to everybody. */
56
+ exports.rolloutPercentSchema = zod_1.z.number().int().min(0).max(100);
57
+ /* -------------------------------------------------------------------------- */
58
+ /* Assets: init + complete */
59
+ /* -------------------------------------------------------------------------- */
60
+ /**
61
+ * One asset the client intends to upload as part of an update. `sha256` is the
62
+ * content hash (dedup key); `contentType` and `size` describe the object the
63
+ * server will accept at the presigned URL.
64
+ */
65
+ exports.assetInitItemSchema = zod_1.z.object({
66
+ sha256: exports.sha256HexSchema,
67
+ contentType: zod_1.z.string().min(1).max(255),
68
+ size: zod_1.z.number().int().positive(),
69
+ });
70
+ /**
71
+ * `POST /updates/v1/assets/init` request. Declares the full asset set of an
72
+ * update; the server replies with a presigned PUT for every asset it does NOT
73
+ * already hold (content-addressed dedup — unchanged assets are never re-uploaded).
74
+ */
75
+ exports.assetInitRequestSchema = zod_1.z.object({
76
+ applicationId: zod_1.z.string().min(1),
77
+ assets: zod_1.z.array(exports.assetInitItemSchema).min(1).max(2000),
78
+ });
79
+ /** One presigned upload the client must PUT its bytes to before completing. */
80
+ exports.assetUploadTicketSchema = zod_1.z.object({
81
+ sha256: exports.sha256HexSchema,
82
+ /** Presigned S3 PUT URL. The client PUTs the exact bytes here. */
83
+ uploadUrl: zod_1.z.string(),
84
+ /** The S3 key the object will live at (`public/updates/assets/<sha256>`). */
85
+ storageKey: zod_1.z.string(),
86
+ /** The Content-Type the presigned URL was signed for; echo it on the PUT. */
87
+ contentType: zod_1.z.string(),
88
+ /**
89
+ * The Cache-Control the presigned URL was signed for; the client MUST send it
90
+ * verbatim on the PUT so the SigV4 signature matches and the stored object
91
+ * carries the long immutable cache header (assets are content-addressed).
92
+ */
93
+ cacheControl: zod_1.z.string(),
94
+ /** Base64 SHA-256 value required in the presigned PUT's checksum header. */
95
+ checksumSHA256: zod_1.z.string(),
96
+ });
97
+ /**
98
+ * `POST /updates/v1/assets/init` response. `missing` holds a presigned upload
99
+ * for each asset the server does not yet have; `existing` lists the sha256s it
100
+ * already holds (the client skips those).
101
+ */
102
+ exports.assetInitResponseSchema = zod_1.z.object({
103
+ missing: zod_1.z.array(exports.assetUploadTicketSchema),
104
+ existing: zod_1.z.array(exports.sha256HexSchema),
105
+ });
106
+ /**
107
+ * `POST /updates/v1/assets/complete` request. Sent after the client has PUT
108
+ * every `missing` asset. The server HEADs each object and flips it to
109
+ * `uploaded`; an object that is absent or size-mismatched is rejected.
110
+ */
111
+ exports.assetCompleteRequestSchema = zod_1.z.object({
112
+ applicationId: zod_1.z.string().min(1),
113
+ sha256s: zod_1.z.array(exports.sha256HexSchema).min(1).max(2000),
114
+ });
115
+ /** Per-asset verification outcome from `assets/complete`. */
116
+ exports.assetCompleteResultItemSchema = zod_1.z.object({
117
+ sha256: exports.sha256HexSchema,
118
+ status: exports.updateAssetStatusSchema,
119
+ size: zod_1.z.number().int().nonnegative(),
120
+ });
121
+ exports.assetCompleteResponseSchema = zod_1.z.object({
122
+ assets: zod_1.z.array(exports.assetCompleteResultItemSchema),
123
+ });
124
+ /* -------------------------------------------------------------------------- */
125
+ /* Create update */
126
+ /* -------------------------------------------------------------------------- */
127
+ /**
128
+ * A single asset reference inside a create-update request. `key` is the
129
+ * expo-export asset key (the md5-basename the client uses to look the asset up
130
+ * and skip embedded ones); `fileExtension` is the suggested on-disk extension.
131
+ */
132
+ exports.updateAssetRefSchema = zod_1.z.object({
133
+ sha256: exports.sha256HexSchema,
134
+ /** expo-export asset key (md5 basename) — how app code references the asset. */
135
+ key: zod_1.z.string().min(1),
136
+ contentType: zod_1.z.string().min(1),
137
+ /** Suggested file extension including the leading dot (e.g. `.js`, `.png`). */
138
+ fileExtension: zod_1.z.string().optional(),
139
+ });
140
+ /**
141
+ * `POST /updates/v1/updates` request. Publishes one bundle for one platform to a
142
+ * channel (the channel is created on demand — CI-friendly for `pr-<n>`). All
143
+ * referenced assets MUST already be `uploaded` (via init/complete). `extra`
144
+ * MUST carry `expoClient` so `Constants.expoConfig` works after an OTA update.
145
+ */
146
+ exports.createUpdateRequestSchema = zod_1.z.object({
147
+ applicationId: zod_1.z.string().min(1),
148
+ channel: exports.channelNameSchema,
149
+ runtimeVersion: exports.runtimeVersionSchema,
150
+ platform: exports.updatePlatformSchema,
151
+ /** The launch (entry-point) asset — its `fileExtension` is ignored by clients. */
152
+ launchAsset: exports.updateAssetRefSchema,
153
+ assets: zod_1.z.array(exports.updateAssetRefSchema),
154
+ /**
155
+ * Opaque `extra` blob embedded verbatim in the signed manifest. MUST contain
156
+ * `expoClient` (the public expo config) so `Constants.expoConfig` resolves
157
+ * after an OTA update; MAY carry other third-party config.
158
+ */
159
+ extra: zod_1.z
160
+ .object({ expoClient: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()) })
161
+ .catchall(zod_1.z.unknown()),
162
+ /** String→string metadata dict; filtered client-side via manifest filters. */
163
+ metadata: zod_1.z.record(zod_1.z.string(), zod_1.z.string()).optional(),
164
+ /** Initial rollout percentage (default 100 — full rollout). */
165
+ rolloutPercent: exports.rolloutPercentSchema.optional(),
166
+ /** Git commit the bundle was built from (audit / console display). */
167
+ gitCommit: zod_1.z.string().max(100).optional(),
168
+ /** Git branch the bundle was built from (audit / console display). */
169
+ gitBranch: zod_1.z.string().max(200).optional(),
170
+ /** Human-readable publish message (console display). */
171
+ message: zod_1.z.string().max(500).optional(),
172
+ });
173
+ exports.updateSchema = zod_1.z.object({
174
+ id: zod_1.z.string(),
175
+ applicationId: zod_1.z.string(),
176
+ channel: zod_1.z.string(),
177
+ runtimeVersion: zod_1.z.string(),
178
+ platform: exports.updatePlatformSchema,
179
+ status: exports.updateStatusSchema,
180
+ rolloutPercent: exports.rolloutPercentSchema,
181
+ launchAssetSha256: exports.sha256HexSchema,
182
+ assetSha256s: zod_1.z.array(exports.sha256HexSchema),
183
+ gitCommit: zod_1.z.string().optional(),
184
+ gitBranch: zod_1.z.string().optional(),
185
+ message: zod_1.z.string().optional(),
186
+ promotedFromUpdateId: zod_1.z.string().optional(),
187
+ createdAt: zod_1.z.string(),
188
+ updatedAt: zod_1.z.string(),
189
+ });
190
+ exports.createUpdateResponseSchema = zod_1.z.object({
191
+ update: exports.updateSchema,
192
+ });
193
+ exports.rollbackToEmbeddedEntrySchema = zod_1.z.object({
194
+ runtimeVersion: zod_1.z.string(),
195
+ platform: exports.updatePlatformSchema,
196
+ commitTime: zod_1.z.string(),
197
+ });
198
+ exports.channelSchema = zod_1.z.object({
199
+ id: zod_1.z.string(),
200
+ applicationId: zod_1.z.string(),
201
+ name: zod_1.z.string(),
202
+ rollbacksToEmbedded: zod_1.z.array(exports.rollbackToEmbeddedEntrySchema),
203
+ createdAt: zod_1.z.string(),
204
+ updatedAt: zod_1.z.string(),
205
+ });
206
+ exports.channelListResponseSchema = zod_1.z.object({
207
+ channels: zod_1.z.array(exports.channelSchema),
208
+ });
209
+ exports.updateListResponseSchema = zod_1.z.object({
210
+ updates: zod_1.z.array(exports.updateSchema),
211
+ });
212
+ /* -------------------------------------------------------------------------- */
213
+ /* Rollback / rollback-to-embedded / promote / rollout */
214
+ /* -------------------------------------------------------------------------- */
215
+ /**
216
+ * `POST /updates/v1/channels/:channel/rollback` request. Marks the current head
217
+ * for `(runtimeVersion, platform)` `rolled_back` so the previous published
218
+ * update (if any) becomes head again. Nothing is deleted.
219
+ */
220
+ exports.rollbackRequestSchema = zod_1.z.object({
221
+ applicationId: zod_1.z.string().min(1),
222
+ runtimeVersion: exports.runtimeVersionSchema,
223
+ platform: exports.updatePlatformSchema,
224
+ });
225
+ /**
226
+ * `POST /updates/v1/channels/:channel/rollback-to-embedded` request. Records a
227
+ * `rollBackToEmbedded` directive so clients on this `(runtimeVersion, platform)`
228
+ * fall back to the update embedded in their binary.
229
+ */
230
+ exports.rollbackToEmbeddedRequestSchema = zod_1.z.object({
231
+ applicationId: zod_1.z.string().min(1),
232
+ runtimeVersion: exports.runtimeVersionSchema,
233
+ platform: exports.updatePlatformSchema,
234
+ });
235
+ /**
236
+ * `POST /updates/v1/channels/:channel/promote` request. Promotes an existing
237
+ * update (by `updateId`) into the target channel by creating a NEW update (new
238
+ * UUID) pointing at the SAME assets. `toChannel` defaults to the path channel.
239
+ */
240
+ exports.promoteRequestSchema = zod_1.z.object({
241
+ applicationId: zod_1.z.string().min(1),
242
+ updateId: zod_1.z.string().min(1),
243
+ /** Target channel to promote into. Defaults to the path `:channel`. */
244
+ toChannel: exports.channelNameSchema.optional(),
245
+ /** Rollout percentage for the promoted update (default 100). */
246
+ rolloutPercent: exports.rolloutPercentSchema.optional(),
247
+ });
248
+ /** `PATCH /updates/v1/updates/:updateId` request — adjust a rollout in place. */
249
+ exports.updateRolloutPatchSchema = zod_1.z.object({
250
+ applicationId: zod_1.z.string().min(1),
251
+ rolloutPercent: exports.rolloutPercentSchema,
252
+ });
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ /**
3
+ * Canonical contract for the Oxy user-invalidation broadcast.
4
+ *
5
+ * Oxy owns identity, but consumers cache it: Mention keeps a Redis summary per
6
+ * post author, and every backend using `@oxy.so/core` holds the SDK's own GET
7
+ * response cache. Both go stale the moment a profile is edited, and neither has
8
+ * any way to find out — the writer is a different process in a different repo.
9
+ * This is the signal that tells them.
10
+ *
11
+ * The channel name and the payload shape are wire contracts between oxy-api (the
12
+ * publisher) and every consuming backend (the subscribers), so they live here
13
+ * rather than in either side. A hand-typed copy of the channel name fails as
14
+ * "the invalidation never arrives" — silently, because pub/sub has no delivery
15
+ * receipt and a message nobody is listening for is indistinguishable from a
16
+ * message nobody sent.
17
+ *
18
+ * DELIVERY IS AT-MOST-ONCE, AND THAT IS THE DESIGN. Every consumer's cache still
19
+ * carries its own TTL, so a dropped message degrades to exactly the behaviour
20
+ * before this signal existed and never to something worse. That property is what
21
+ * makes a bare Redis PUBLISH sufficient here and an outbox, retries, delivery
22
+ * receipts and payload signatures unnecessary. Do not treat a received event as
23
+ * authoritative for anything except "re-read this user from Oxy".
24
+ *
25
+ * PRIVACY — the payload carries NO user data, only an id, a reason and a
26
+ * timestamp. The channel rides the shared Valkey that every Oxy backend can
27
+ * subscribe to, so anything placed on it is readable by every service in the
28
+ * ecosystem. Never add a name, handle, email, avatar or any profile field: a
29
+ * subscriber that wants the new values re-reads them from Oxy through its normal
30
+ * authenticated path, where the usual authorization applies.
31
+ *
32
+ * Platform-agnostic — zod only, no react/react-native/expo.
33
+ */
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.oxyUserInvalidationEventSchema = exports.OXY_PUBLISHED_USER_CHANGE_REASONS = exports.OXY_USER_CHANGE_REASONS = exports.OXY_USER_INVALIDATION_CHANNEL = void 0;
36
+ exports.isPublishedOxyUserChangeReason = isPublishedOxyUserChangeReason;
37
+ const zod_1 = require("zod");
38
+ /** Redis pub/sub channel carrying user-invalidation events. */
39
+ exports.OXY_USER_INVALIDATION_CHANNEL = 'oxy:user:invalidate';
40
+ /**
41
+ * Why a user record changed, as classified by the writer in oxy-api.
42
+ *
43
+ * - `profile` — anything a consumer renders or caches as IDENTITY: display name,
44
+ * username, avatar, bio, verification, federation fields, account status. This
45
+ * is the DEFAULT for every writer, so a site that forgets to classify itself
46
+ * over-invalidates (correct, marginally slower) rather than under-invalidates
47
+ * (silently wrong). Keep that asymmetry if you add a reason.
48
+ * - `graph` — follow-edge churn only (follower/following counts). High frequency,
49
+ * and bulk follow/unfollow moves up to 200 edges in one call. Nothing renders
50
+ * identity from it and a stale count is harmless to ranking, so it is NOT
51
+ * broadcast — see {@link OXY_PUBLISHED_USER_CHANGE_REASONS}.
52
+ */
53
+ exports.OXY_USER_CHANGE_REASONS = ['profile', 'graph'];
54
+ /**
55
+ * The reasons that are actually put on the wire.
56
+ *
57
+ * A reason absent from this list is a local cache eviction in oxy-api and
58
+ * nothing more: no message is published at all, rather than a message every
59
+ * subscriber receives and discards. The distinction matters at bulk-follow
60
+ * scale, where the discarded variant is a 200-message burst on a channel every
61
+ * Oxy backend is subscribed to.
62
+ *
63
+ * This is deliberately a shared list rather than a check inside the publisher:
64
+ * a subscriber needs to know what it can receive, and the schema below rejects
65
+ * anything else, so publisher and subscriber cannot drift into disagreeing about
66
+ * which events exist. Adding a reason therefore forces an explicit decision about
67
+ * whether it broadcasts.
68
+ */
69
+ exports.OXY_PUBLISHED_USER_CHANGE_REASONS = ['profile'];
70
+ /** Whether a change of this kind is broadcast to consumers at all. */
71
+ function isPublishedOxyUserChangeReason(reason) {
72
+ return exports.OXY_PUBLISHED_USER_CHANGE_REASONS.includes(reason);
73
+ }
74
+ /**
75
+ * A single user-invalidation event.
76
+ *
77
+ * `at` is the publisher's epoch-ms clock, carried for diagnosis (measuring
78
+ * end-to-end propagation, spotting a wedged subscriber) — never for ordering or
79
+ * conflict resolution. Two Oxy tasks publish from unsynchronised clocks, and the
80
+ * event says only "re-read this user", which is idempotent and order-independent.
81
+ */
82
+ exports.oxyUserInvalidationEventSchema = zod_1.z.object({
83
+ /** The Oxy user whose record changed. */
84
+ userId: zod_1.z.string().min(1),
85
+ /** Why it changed. Only broadcast reasons appear on the wire. */
86
+ reason: zod_1.z.enum(exports.OXY_PUBLISHED_USER_CHANGE_REASONS),
87
+ /** Publisher's epoch-ms timestamp. Diagnostic only. */
88
+ at: zod_1.z.number().int().nonnegative(),
89
+ });
@@ -0,0 +1,245 @@
1
+ "use strict";
2
+ /**
3
+ * Canonical API user-response contracts.
4
+ *
5
+ * SINGLE SOURCE OF TRUTH for the wire shape of every user object the API emits
6
+ * and every consumer (the auth app, services, accounts) parses. The API
7
+ * validates its OUTPUT against these schemas; web/RN consumers validate their
8
+ * INPUT against the same schemas. Because there is exactly one definition, the
9
+ * producer and the consumers cannot drift — the class of bugs that motivated
10
+ * this module (the auth app's local Zod schema requiring `name` to be a plain
11
+ * string, dropping every account that had a structured name) is impossible.
12
+ *
13
+ * This package (`@oxy.so/contracts`) is the dedicated, zero-dependency home for
14
+ * these contracts so the backend (`@oxy.so/api`) and the client SDKs
15
+ * (`@oxy.so/core`, `@oxy.so/services`) can all depend on it without
16
+ * the backend having to depend on a client SDK to obtain its schemas.
17
+ *
18
+ * Faithful to the producers:
19
+ * - `packages/api/src/utils/userTransform.ts` `formatUserResponse` — the
20
+ * canonical serialization used by login/signup, device sessions, etc.
21
+ * Emits `id` (NOT `_id`), forwards `username` verbatim (may be absent), and
22
+ * emits `name` as the structured `{ first, last, full, displayName }`
23
+ * subdocument.
24
+ * - `packages/api/src/models/User.ts` — `NameSchema` (`first`/`last` default
25
+ * `''`; `full` and `displayName` are Mongoose VIRTUALS. Formatted API
26
+ * responses compose both fields, while raw-document responses may omit the
27
+ * virtuals if the query did not materialise them.
28
+ *
29
+ * Platform-agnostic — zod only, no react/react-native/expo. ESM-safe (no
30
+ * `require()`).
31
+ */
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.deviceLinkedSessionsResponseSchema = exports.deviceLinkedSessionSchema = exports.currentUserResponseSchema = exports.userProfileUpdateSchema = exports.userResponseSchema = exports.themePreferenceSchema = exports.userRelationshipSchema = exports.userNameSchema = void 0;
34
+ exports.resolveUserId = resolveUserId;
35
+ exports.safeParseContract = safeParseContract;
36
+ const zod_1 = require("zod");
37
+ const identity_1 = require("./identity");
38
+ const accountGraph_1 = require("./accountGraph");
39
+ exports.userNameSchema = zod_1.z
40
+ .object({
41
+ first: zod_1.z.string().optional(),
42
+ last: zod_1.z.string().optional(),
43
+ full: zod_1.z.string().optional(),
44
+ displayName: zod_1.z.string().optional(),
45
+ })
46
+ .passthrough();
47
+ exports.userRelationshipSchema = zod_1.z.object({
48
+ isFollowing: zod_1.z.boolean(),
49
+ followsYou: zod_1.z.boolean(),
50
+ });
51
+ exports.themePreferenceSchema = zod_1.z.object({
52
+ mode: zod_1.z.enum(['light', 'dark', 'system']),
53
+ colorPreset: zod_1.z.string(),
54
+ });
55
+ /**
56
+ * The canonical user object emitted by `formatUserResponse`.
57
+ *
58
+ * `id` is present on formatted user DTOs. `name.displayName` is OPTIONAL on the
59
+ * contract — the API still synthesizes a default today, but consumers must not
60
+ * assume it is present and should fall back to a handle when it is absent. The
61
+ * rest is forwarded from the user document and may be absent depending on the query's
62
+ * `.select(...)`/`.lean()` projection. Both `id` and `_id` are accepted because
63
+ * some raw-document responses carry `_id` instead of `id`; resolve the
64
+ * identifier with {@link resolveUserId}.
65
+ *
66
+ * `.passthrough()` keeps the large tail of profile fields
67
+ * (`privacySettings`, `locations`, `links`, `linksMetadata`, `bio`,
68
+ * `description`, `languages`, `verified`, timestamps, …) available to callers
69
+ * that need them without enumerating every nested shape here — the load-bearing
70
+ * identity/display fields are the ones we pin precisely.
71
+ */
72
+ exports.userResponseSchema = zod_1.z
73
+ .object({
74
+ /** MongoDB ObjectId as a string. Present on `formatUserResponse` output. */
75
+ id: zod_1.z.string().optional(),
76
+ /** Raw-document id (e.g. `GET /users/me`). Present when `id` is not. */
77
+ _id: zod_1.z.string().optional(),
78
+ publicKey: zod_1.z.string().optional(),
79
+ username: zod_1.z.string().optional(),
80
+ email: zod_1.z.string().optional(),
81
+ phone: zod_1.z.string().optional(),
82
+ address: zod_1.z.string().optional(),
83
+ birthday: zod_1.z.string().optional(),
84
+ /** Avatar file id (string) or null. */
85
+ avatar: zod_1.z.string().nullable().optional(),
86
+ /** Named Bloom color preset (e.g. `"blue"`) or null. */
87
+ color: zod_1.z.string().nullable().optional(),
88
+ name: exports.userNameSchema,
89
+ verified: zod_1.z.boolean().optional(),
90
+ /**
91
+ * The account's languages as full BCP-47 locales (`language-REGION`,
92
+ * e.g. `es-ES`, `en-US`, `pt-BR`), ordered with the PRIMARY (UI) locale
93
+ * first. `languages[0]` is the primary locale — there is no singular
94
+ * `language` field.
95
+ */
96
+ languages: zod_1.z.array(zod_1.z.string()).optional(),
97
+ /**
98
+ * The account's self-sovereign identifier
99
+ * (`did:web:<FEDERATION_DOMAIN>:u:<userId>`). Surfaced as a `User`
100
+ * virtual; present on formatted DTOs once the identity layer is live.
101
+ */
102
+ did: zod_1.z.string().optional(),
103
+ /**
104
+ * Proven domain-ownership badges. Each is a {@link verifiedDomainSchema}
105
+ * entry; present only when the account has verified at least one domain.
106
+ */
107
+ verifiedDomains: zod_1.z.array(identity_1.verifiedDomainSchema).optional(),
108
+ /**
109
+ * Account-graph classification — what KIND of account this is.
110
+ *
111
+ * ORTHOGONAL to `type` (`local` / `federated` / `agent` / `automated`),
112
+ * which says where the account lives and how it is driven; the two
113
+ * coexist and neither substitutes for the other. A `channel` is a
114
+ * publishing identity nobody can act as, so a consumer that renders
115
+ * authored content reads THIS to tell a channel's post from a person's.
116
+ *
117
+ * Optional because a DTO produced from a source that never carried the
118
+ * column omits it; absent should be read as `personal`, the column's
119
+ * default, not as unknown.
120
+ */
121
+ kind: accountGraph_1.accountKindSchema.optional(),
122
+ /**
123
+ * What this account is about — the field a profile screen RENDERS.
124
+ *
125
+ * **Ordered, primary first.** `accountCategories[0]` is the primary
126
+ * category; there is deliberately no sibling `primaryCategory` field,
127
+ * because two representations of one fact can disagree (see rule 2 in
128
+ * `accountGraph.ts`). Nothing downstream may sort, de-duplicate or
129
+ * otherwise reorder this array.
130
+ *
131
+ * **Ids, never labels.** Each element is a stable slug; the visible text
132
+ * comes from the reader's own translation catalogue, keyed
133
+ * `accounts.accountCategory.<id>`. A label on the wire would paint every
134
+ * profile in the language of whoever picked it.
135
+ *
136
+ * Absent when the account has none — which is every `personal` account,
137
+ * and any non-personal one that has not chosen. A renderer reads
138
+ * `user.accountCategories ?? []`.
139
+ */
140
+ accountCategories: accountGraph_1.accountCategoriesSchema.optional(),
141
+ /**
142
+ * The authenticated viewer's relationship to this profile. Present ONLY
143
+ * on single-profile fetches (`GET /profiles/username/:username`,
144
+ * `GET /users/:userId`) when the request is authenticated; OMITTED for
145
+ * anonymous requests and for the bulk `POST /users/by-ids` fan-out.
146
+ */
147
+ relationship: exports.userRelationshipSchema.optional(),
148
+ /**
149
+ * Portable theme preference. Rides the self/session payload (cold boot),
150
+ * so it is present on the current-user DTO (`GET /users/me`,
151
+ * `GET /session/user/:sessionId`) and absent until the user sets it.
152
+ */
153
+ themePreference: exports.themePreferenceSchema.optional(),
154
+ })
155
+ .passthrough();
156
+ exports.userProfileUpdateSchema = zod_1.z
157
+ .object({
158
+ name: zod_1.z
159
+ .object({
160
+ first: zod_1.z.string().optional(),
161
+ last: zod_1.z.string().optional(),
162
+ /**
163
+ * Explicit display name, stored rather than composed. Wins over
164
+ * `first`/`last` when set; send `''` to clear it and fall back
165
+ * to the composed pair.
166
+ */
167
+ displayName: zod_1.z.string().optional(),
168
+ })
169
+ .optional(),
170
+ username: zod_1.z.string().optional(),
171
+ email: zod_1.z.string().optional(),
172
+ avatar: zod_1.z.string().optional(),
173
+ color: zod_1.z.string().nullable().optional(),
174
+ bio: zod_1.z.string().optional(),
175
+ description: zod_1.z.string().optional(),
176
+ phone: zod_1.z.string().optional(),
177
+ address: zod_1.z.string().optional(),
178
+ birthday: zod_1.z.string().optional(),
179
+ locations: zod_1.z.array(zod_1.z.unknown()).optional(),
180
+ links: zod_1.z.array(zod_1.z.string()).optional(),
181
+ linksMetadata: zod_1.z
182
+ .array(zod_1.z.object({
183
+ url: zod_1.z.string(),
184
+ title: zod_1.z.string().optional(),
185
+ description: zod_1.z.string().optional(),
186
+ image: zod_1.z.string().optional(),
187
+ id: zod_1.z.string().optional(),
188
+ }))
189
+ .optional(),
190
+ /**
191
+ * Ordered account locales (`language-REGION`), primary first. Replaces
192
+ * the eliminated singular `language`; `languages[0]` is the primary UI
193
+ * locale.
194
+ */
195
+ languages: zod_1.z.array(zod_1.z.string()).optional(),
196
+ accountExpiresAfterInactivityDays: zod_1.z.number().nullable().optional(),
197
+ notificationPreferences: zod_1.z.record(zod_1.z.unknown()).optional(),
198
+ userPreferences: zod_1.z.record(zod_1.z.unknown()).optional(),
199
+ privacySettings: zod_1.z.record(zod_1.z.unknown()).optional(),
200
+ /**
201
+ * Portable theme preference. Written through the same `PUT /users/me`
202
+ * settings-update path as `languages`/`userPreferences`.
203
+ */
204
+ themePreference: exports.themePreferenceSchema.optional(),
205
+ })
206
+ .passthrough();
207
+ /**
208
+ * Resolve the canonical user id from a {@link UserResponse}, accepting either
209
+ * the `formatUserResponse` `id` field or the raw-document `_id` field.
210
+ */
211
+ function resolveUserId(user) {
212
+ return user.id ?? user._id;
213
+ }
214
+ /**
215
+ * Wire shape of `GET /users/me` — the API success envelope (`{ data: <user> }`)
216
+ * wrapping the current-user DTO. Some older producers use `_id` instead of
217
+ * `id`; resolve via {@link resolveUserId}. The display name still lives under
218
+ * `name.displayName`.
219
+ */
220
+ exports.currentUserResponseSchema = zod_1.z.object({
221
+ data: exports.userResponseSchema,
222
+ });
223
+ /**
224
+ * One entry of `GET /session/device/sessions/:sessionId` — the deduplicated
225
+ * accounts signed in on this physical device (one per user, most recent
226
+ * session). Backs the multi-account chooser. The embedded user mirrors
227
+ * `formatUserResponse`; it is nullable on slots that lost their user document.
228
+ */
229
+ exports.deviceLinkedSessionSchema = zod_1.z.object({
230
+ sessionId: zod_1.z.string(),
231
+ isCurrent: zod_1.z.boolean().optional(),
232
+ user: exports.userResponseSchema.nullable().optional(),
233
+ });
234
+ /** Wire shape of `GET /session/device/sessions/:sessionId` (an array). */
235
+ exports.deviceLinkedSessionsResponseSchema = zod_1.z.array(exports.deviceLinkedSessionSchema);
236
+ /**
237
+ * Safely parse a value against a contract schema. Returns the parsed (typed)
238
+ * value, or `null` when validation fails — the same ergonomics the auth app's
239
+ * local `safeParse` provided, now sourced from the contracts package so the
240
+ * parse helper and the schemas live together.
241
+ */
242
+ function safeParseContract(schema, data) {
243
+ const result = schema.safeParse(data);
244
+ return result.success ? result.data : null;
245
+ }