@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,489 @@
1
+ "use strict";
2
+ /**
3
+ * Account graph wire contracts — the account-kind vocabulary, the account
4
+ * category taxonomy, and the create-account input.
5
+ *
6
+ * `accountCategories` classifies a NON-PERSONAL account — what it is about, what
7
+ * it does — without polluting `User.kind`. See the block above
8
+ * {@link ACCOUNT_CATEGORY_IDS} for the four rules that govern it.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.createAccountRequestSchema = exports.ACCOUNT_CATEGORY_KINDS = exports.accountCategoriesSchema = exports.MAX_ACCOUNT_CATEGORIES = exports.SELECTABLE_ACCOUNT_CATEGORY_IDS = exports.RETIRED_ACCOUNT_CATEGORY_IDS = exports.accountCategoryIdSchema = exports.ACCOUNT_CATEGORY_IDS = exports.childAccountKindSchema = exports.CHILD_ACCOUNT_KINDS = exports.accountKindSchema = exports.ACCOUNT_KINDS = void 0;
12
+ exports.isDelegatedActAsEligibleKind = isDelegatedActAsEligibleKind;
13
+ exports.isOperatorSwitchTargetKind = isOperatorSwitchTargetKind;
14
+ exports.isAccountKind = isAccountKind;
15
+ exports.isSelectableAccountCategoryId = isSelectableAccountCategoryId;
16
+ exports.newlyAddedRetiredCategories = newlyAddedRetiredCategories;
17
+ exports.kindAcceptsAccountCategories = kindAcceptsAccountCategories;
18
+ const zod_1 = require("zod");
19
+ const username_1 = require("./username");
20
+ /**
21
+ * The union is spelled out above and the array proves coverage BOTH ways
22
+ * (`satisfies` here, the `Gap` alias below) — the same shape this package's
23
+ * `ACCOUNT_CATEGORY_IDS` / `TRUST_TIERS` pairs use, and the one
24
+ * `db/schema/users.ts` mirrors to keep the `users_kind_check` CHECK honest.
25
+ *
26
+ * Deriving the union from the array instead would cost nothing here and be paid
27
+ * by consumers: `kind` travels into `@oxy.so/services` on every device-directory
28
+ * context (`deviceContextSchema.kind` → `DeviceContext` → the switcher rows),
29
+ * and an indexed-access type is materially more expensive to check there than a
30
+ * literal union.
31
+ */
32
+ exports.ACCOUNT_KINDS = [
33
+ 'personal',
34
+ 'organization',
35
+ 'project',
36
+ 'bot',
37
+ 'channel',
38
+ ];
39
+ exports.accountKindSchema = zod_1.z.enum(exports.ACCOUNT_KINDS);
40
+ exports.CHILD_ACCOUNT_KINDS = [
41
+ 'organization',
42
+ 'project',
43
+ 'bot',
44
+ 'channel',
45
+ ];
46
+ exports.childAccountKindSchema = zod_1.z.enum(exports.CHILD_ACCOUNT_KINDS);
47
+ /**
48
+ * Whether an account of this kind may be the SUBJECT OF A DELEGATION — an
49
+ * application acting as it on some person's authority. `POST /internal/accounts/
50
+ * :id/service-switch` and the OAuth delegated subject both gate on this.
51
+ *
52
+ * It is NOT the question an account switcher asks. See
53
+ * {@link isOperatorSwitchTargetKind}, and the note below on why the difference
54
+ * is `bot`.
55
+ *
56
+ * Two kinds are refused, for opposite reasons:
57
+ *
58
+ * - `personal` is a human login, so assuming it would be impersonation.
59
+ * - `channel` is a CONTENT identity, not an operating one. A channel exists so
60
+ * that posts can be authored BY it; it is never a seat anybody occupies. Its
61
+ * operators act on it through their own membership, and an application
62
+ * publishes to it with its own credential. Refusing act-as is what makes
63
+ * "no login, ever" structural rather than incidental: no session can be
64
+ * minted whose subject is a channel, so no bearer exists that could add an
65
+ * auth method to one (every auth-method write resolves its target from the
66
+ * authenticated subject, never from a parameter).
67
+ *
68
+ * Consumers must gate on this predicate rather than testing `kind === 'personal'`,
69
+ * which silently admits every kind added after it was written.
70
+ */
71
+ function isDelegatedActAsEligibleKind(kind) {
72
+ return kind === 'organization' || kind === 'project' || kind === 'bot';
73
+ }
74
+ /**
75
+ * Whether a PERSON may switch into an account of this kind — become it, in an
76
+ * account switcher, for the rest of their session.
77
+ *
78
+ * ## Why this is not the same question as {@link isDelegatedActAsEligibleKind}
79
+ *
80
+ * The two differ on exactly one kind, `bot`, and that difference is the whole
81
+ * reason both exist.
82
+ *
83
+ * **A bot is not something you become. It is something that operates on your
84
+ * behalf.** Its whole purpose is to act while nobody is present: an application
85
+ * holds a credential, names the human whose authority it borrows, and speaks as
86
+ * the bot. That is delegation, and it is what
87
+ * {@link isDelegatedActAsEligibleKind} admits it for.
88
+ *
89
+ * Handing a person the bot's seat instead inverts that. It puts a human inside
90
+ * the identity that exists to act without one, and it does so on the human's own
91
+ * device, next to their personal login — which is precisely what happened: a
92
+ * `bot` account held a live session on a person's device, offered to them by a
93
+ * switcher that had asked the delegation question by mistake.
94
+ *
95
+ * `channel` is refused here as well, for the reason set out above, and
96
+ * `personal` because assuming somebody else's login is impersonation.
97
+ *
98
+ * ## This is the narrower predicate, deliberately
99
+ *
100
+ * Everything a person may become, a service may also act as; the reverse does
101
+ * not hold. A caller that is unsure which question it is asking wants THIS one:
102
+ * being wrong here withholds an affordance, while being wrong the other way
103
+ * hands out a seat.
104
+ */
105
+ function isOperatorSwitchTargetKind(kind) {
106
+ return kind === 'organization' || kind === 'project';
107
+ }
108
+ /**
109
+ * Narrow an unknown value to an {@link AccountKind}.
110
+ *
111
+ * The user-DTO serializers read from structurally-permissive `unknown` sources
112
+ * (a Drizzle row, a Mongo document, an already-formatted object), so each one
113
+ * would otherwise hand-roll this check and they would drift on what counts.
114
+ */
115
+ function isAccountKind(value) {
116
+ return typeof value === 'string' && exports.ACCOUNT_KINDS.includes(value);
117
+ }
118
+ // ===========================================================================
119
+ // Account categories
120
+ //
121
+ // Four rules govern this taxonomy. Each one is here because the obvious
122
+ // alternative fails silently rather than loudly.
123
+ //
124
+ // 1. AN ID IS AN OPAQUE, IMMUTABLE SLUG — the LABEL is not stored anywhere.
125
+ // Every value below is an identifier that no rename may ever change. The
126
+ // human-readable label lives in each client's own translation catalogue,
127
+ // keyed by the id. Re-labelling `agency` from "Real estate agency" to
128
+ // "Agency" is therefore a one-line locale edit that touches no row; moving
129
+ // the label into the column or the DTO would make the same edit a data
130
+ // migration, and would pin every reader to the language of whoever chose it.
131
+ //
132
+ // 2. THE PRIMARY CATEGORY IS THE FIRST ELEMENT of an ordered list. Not a
133
+ // separate flag, and not a second field: a flag admits two primaries and
134
+ // admits none, and ordering makes both unrepresentable. The cost is that
135
+ // ORDER IS DATA — anything that re-serializes, sorts or de-duplicates this
136
+ // list can change which category is primary without erroring, so no layer
137
+ // between the column and the client may reorder it.
138
+ //
139
+ // 3. THE VOCABULARY IS APPEND-ONLY. `ACCOUNT_CATEGORY_IDS` may gain ids and may
140
+ // never lose one, because rows already carry the ids it holds. Withdrawing a
141
+ // category means listing it in {@link RETIRED_ACCOUNT_CATEGORY_IDS}, which
142
+ // removes it from the picker while leaving it readable and re-writable. The
143
+ // database enforces the same asymmetry: its CHECK is re-evaluated on EVERY
144
+ // update to a row, so narrowing the allowed set makes an unrelated write —
145
+ // saving a bio — fail on any account that had picked the withdrawn value.
146
+ // Measured on a real Postgres, `NOT VALID` included; it does not help.
147
+ //
148
+ // 4. THE SET IS CLOSED. An id nobody can validate is an id no client can render:
149
+ // the label comes from a translation key derived from the id, so an invented
150
+ // value paints as a blank or a raw slug in every app at once. Closing it also
151
+ // costs nothing that rule 3 does not already charge — adding a category needs
152
+ // a migration to widen the CHECK whether or not the enum exists, so an open
153
+ // list would buy back no work, only the validation.
154
+ // ===========================================================================
155
+ /**
156
+ * Every account category, by stable id.
157
+ *
158
+ * Grouped by comment for readability only; the storage, the wire and the picker
159
+ * all treat this as one flat list. `other` is the escape hatch for an account
160
+ * that fits nothing here.
161
+ *
162
+ * TO ADD ONE: append an id (lowercase ASCII, `snake_case`) here, publish
163
+ * `@oxy.so/contracts`, then ship a migration that widens
164
+ * `users_account_categories_check` — never edit an existing migration — and add
165
+ * an `accounts.accountCategory.<id>` label to each client's locales.
166
+ *
167
+ * TO WITHDRAW ONE: leave the id here and add it to
168
+ * {@link RETIRED_ACCOUNT_CATEGORY_IDS}. See rule 3 above.
169
+ */
170
+ exports.ACCOUNT_CATEGORY_IDS = [
171
+ // ---- media & public information -----------------------------------------
172
+ 'news',
173
+ 'politics',
174
+ // ---- business & economy --------------------------------------------------
175
+ 'business',
176
+ 'startup',
177
+ 'finance',
178
+ 'crypto',
179
+ 'marketplace',
180
+ 'retail',
181
+ // The four ids the single-valued `organizationCategory` field used to hold,
182
+ // carried forward VERBATIM. Their labels may be rewritten freely; their ids
183
+ // may not, because live rows hold them.
184
+ 'real_estate',
185
+ 'agency',
186
+ 'landlord',
187
+ 'cooperative',
188
+ 'architecture',
189
+ // ---- technology ----------------------------------------------------------
190
+ 'technology',
191
+ 'software',
192
+ 'ai',
193
+ 'security',
194
+ 'automation',
195
+ // ---- knowledge -----------------------------------------------------------
196
+ 'science',
197
+ 'education',
198
+ 'books',
199
+ // ---- health --------------------------------------------------------------
200
+ 'health',
201
+ 'fitness',
202
+ // ---- sport & play --------------------------------------------------------
203
+ 'sports',
204
+ 'gaming',
205
+ // ---- culture & entertainment ---------------------------------------------
206
+ //
207
+ // There is deliberately NO generic `entertainment` here, and re-adding one is
208
+ // a regression rather than a gap. It is the only id this list ever carried
209
+ // that was dominated by its own specifics — `film`, `music`, `gaming` and
210
+ // `comedy` all exist — and a generic drawer sitting beside its four
211
+ // concretions collects the lazy pick, which degrades the data for all four at
212
+ // once: the accounts that would have said `film` say `entertainment` instead,
213
+ // and `film` stops meaning what it meant.
214
+ //
215
+ // The other overlaps in this vocabulary are NOT the same case and must not be
216
+ // merged on this reasoning: `sports`/`fitness`, `art`/`photography`,
217
+ // `business`/`startup`, `finance`/`crypto`, `home_garden`/`diy` and
218
+ // `technology`/`software`/`security` are genuinely different audiences, and
219
+ // with a cap of four the granularity is cheap.
220
+ 'music',
221
+ 'film',
222
+ 'podcast',
223
+ 'art',
224
+ 'photography',
225
+ 'comedy',
226
+ // ---- everyday life -------------------------------------------------------
227
+ 'food',
228
+ 'travel',
229
+ 'fashion',
230
+ 'home_garden',
231
+ 'diy',
232
+ 'automotive',
233
+ 'animals',
234
+ 'family',
235
+ // ---- society -------------------------------------------------------------
236
+ 'nonprofit',
237
+ 'government',
238
+ 'community',
239
+ 'activism',
240
+ 'environment',
241
+ 'religion',
242
+ // ---- fallback ------------------------------------------------------------
243
+ 'other',
244
+ ];
245
+ /**
246
+ * Accepts EVERY id, withdrawn ones included — see rule 3.
247
+ *
248
+ * A schema that rejected a withdrawn id would 400 the whole request whenever a
249
+ * client round-trips the categories it was served, so an account that had
250
+ * picked one could no longer save its bio either. That is the same failure the
251
+ * nullable `bio` / `avatar` fix addressed, wearing a different hat.
252
+ */
253
+ exports.accountCategoryIdSchema = zod_1.z.enum(exports.ACCOUNT_CATEGORY_IDS);
254
+ /**
255
+ * Ids withdrawn from the picker. Empty today.
256
+ *
257
+ * A withdrawn id keeps working everywhere it is already stored: it validates,
258
+ * it survives a round-trip save, it still renders from its label key, and it
259
+ * stays PRIMARY if it was primary. Nothing rewrites a stored list — a read-time
260
+ * or migration-time demotion would silently replace a choice its owner made,
261
+ * which is precisely what stable ids exist to prevent. The owner drops it on
262
+ * their next edit; until then it is honoured.
263
+ *
264
+ * What withdrawal changes is only this: the id leaves
265
+ * {@link SELECTABLE_ACCOUNT_CATEGORY_IDS}, so no picker offers it, and
266
+ * {@link newlyAddedRetiredCategories} refuses to let a write ADD it to an
267
+ * account that did not already have it.
268
+ */
269
+ exports.RETIRED_ACCOUNT_CATEGORY_IDS = [];
270
+ /** Whether a category may still be OFFERED. A stored one is readable either way. */
271
+ function isSelectableAccountCategoryId(id) {
272
+ return !exports.RETIRED_ACCOUNT_CATEGORY_IDS.includes(id);
273
+ }
274
+ /** The ids a picker may offer, in declaration order. */
275
+ exports.SELECTABLE_ACCOUNT_CATEGORY_IDS = exports.ACCOUNT_CATEGORY_IDS.filter(isSelectableAccountCategoryId);
276
+ /**
277
+ * Which of `next` are withdrawn ids the account did not already carry — i.e.
278
+ * the ones a write must be refused for.
279
+ *
280
+ * `retired` is a parameter rather than a module read so the rule can be
281
+ * exercised against a non-empty set while the production one is empty; a test
282
+ * over `RETIRED_ACCOUNT_CATEGORY_IDS` alone would pass vacuously today and stay
283
+ * passing if the rule were deleted.
284
+ */
285
+ function newlyAddedRetiredCategories(next, previous, retired) {
286
+ return next.filter((id) => retired.includes(id) && !previous.includes(id));
287
+ }
288
+ /**
289
+ * How many categories one account may carry.
290
+ *
291
+ * Four, not "as many as you like". Three reasons, in the order they bind:
292
+ *
293
+ * - The primary has to MEAN something. At ten categories the first element
294
+ * reads as a sort artifact rather than a choice, and rule 2 above is the
295
+ * entire mechanism by which a primary exists.
296
+ * - The profile RENDERS them as a row of chips; four labels of this length is
297
+ * what fits a phone-width profile header before the row wraps or truncates.
298
+ * - Four is enough to place a genuinely compound account without a tag cloud:
299
+ * a housing cooperative that is also a non-profit serving a local community
300
+ * spends `cooperative`, `nonprofit`, `community`, `real_estate` — and is the
301
+ * most compound real example in the ecosystem.
302
+ *
303
+ * One constant, read by the wire schema, the database CHECK and the picker, so
304
+ * changing it is one edit plus a migration.
305
+ */
306
+ exports.MAX_ACCOUNT_CATEGORIES = 4;
307
+ /**
308
+ * An account's categories on the wire. ORDER IS MEANINGFUL — index 0 is the
309
+ * primary (rule 2).
310
+ *
311
+ * A duplicate is REJECTED rather than silently collapsed. De-duplicating would
312
+ * rewrite the caller's list, and any rewrite of this list can move which id sits
313
+ * at index 0 — so the one repair available here is the one that would break the
314
+ * property the list exists to carry. A duplicate only ever comes from a client
315
+ * bug, and a 400 naming the index is how that bug gets found.
316
+ */
317
+ exports.accountCategoriesSchema = zod_1.z
318
+ .array(exports.accountCategoryIdSchema)
319
+ .max(exports.MAX_ACCOUNT_CATEGORIES)
320
+ .superRefine((ids, ctx) => {
321
+ const seen = new Set();
322
+ ids.forEach((id, index) => {
323
+ if (seen.has(id)) {
324
+ ctx.addIssue({
325
+ code: zod_1.z.ZodIssueCode.custom,
326
+ message: `Duplicate account category "${id}"`,
327
+ path: [index],
328
+ });
329
+ }
330
+ seen.add(id);
331
+ });
332
+ });
333
+ /**
334
+ * Kinds that may carry categories: every kind EXCEPT `personal`.
335
+ *
336
+ * A person has interests, not a sector — and their interests are not a
337
+ * classification anybody else gets to read off their profile. Spelled out
338
+ * positively, like {@link isDelegatedActAsEligibleKind} and for the same reason: a `kind
339
+ * !== 'personal'` test silently admits every kind invented after it was
340
+ * written, whereas this list forces whoever adds one to decide.
341
+ */
342
+ exports.ACCOUNT_CATEGORY_KINDS = [
343
+ 'organization',
344
+ 'project',
345
+ 'bot',
346
+ 'channel',
347
+ ];
348
+ /**
349
+ * Whether an account of this kind may carry categories.
350
+ *
351
+ * The API refuses the write and the `users_account_categories_kind_check`
352
+ * constraint makes it unrepresentable; both derive from
353
+ * {@link ACCOUNT_CATEGORY_KINDS}, so they cannot disagree.
354
+ */
355
+ function kindAcceptsAccountCategories(kind) {
356
+ return exports.ACCOUNT_CATEGORY_KINDS.includes(kind ?? '');
357
+ }
358
+ /**
359
+ * An account's name on the create/update wire.
360
+ *
361
+ * `displayName` is EXPLICIT and stored, not derived. `first`/`last` model a
362
+ * human name, and composing a display string from them is right for a person —
363
+ * but a non-personal account has a TITLE, not a given and family name. Without
364
+ * this field the only way to name a channel "Notas de Nate" was to put the whole
365
+ * title in `first`, which renders correctly by accident while recording it as
366
+ * somebody's given name.
367
+ *
368
+ * When present it wins over the composed `first`/`last` (see the API's
369
+ * `composeDisplayName`, which already preferred an explicit value — only the
370
+ * storage for one was missing).
371
+ */
372
+ const accountNameSchema = zod_1.z
373
+ .object({
374
+ first: zod_1.z.string().trim().max(100).optional(),
375
+ last: zod_1.z.string().trim().max(100).optional(),
376
+ displayName: zod_1.z.string().trim().max(100).optional(),
377
+ })
378
+ .optional();
379
+ /**
380
+ * POST /accounts — create a non-personal account under the caller's tree.
381
+ *
382
+ * No cross-field refinement guards `accountCategories`, and that is not an
383
+ * omission: `kind` here is a CHILD kind, and every child kind is in
384
+ * {@link ACCOUNT_CATEGORY_KINDS}, so `personal` is already unrepresentable on
385
+ * this route. The refinement the single-valued predecessor needed disappeared
386
+ * along with the restriction that made it necessary. A child kind that does NOT
387
+ * accept categories would break that reasoning silently, so
388
+ * `__tests__/accountGraph.test.ts` asserts the two lists agree.
389
+ */
390
+ exports.createAccountRequestSchema = zod_1.z.object({
391
+ parentAccountId: zod_1.z.string().trim().min(1).optional(),
392
+ kind: exports.childAccountKindSchema,
393
+ /**
394
+ * The SAME policy a person's handle is held to. `users.username` is one unique
395
+ * index, so a managed account may not reserve a name a person could not ask
396
+ * for — and this route's predecessor (`.min(1).max(100)` here, `^[\w.-]+$`
397
+ * with no ceiling in the service) is how a one-character or dotted or
398
+ * 100-character handle became reachable for bots alone.
399
+ *
400
+ * A `bot` is held to that AND to the label its handle must end in. That half
401
+ * cannot live on this field — it depends on `kind`, a sibling — so it is in the
402
+ * `superRefine` below, which reports its issue against this path.
403
+ */
404
+ username: username_1.usernameSchema,
405
+ name: accountNameSchema,
406
+ bio: zod_1.z.string().trim().max(500).optional(),
407
+ avatar: zod_1.z.string().optional(),
408
+ description: zod_1.z.string().trim().max(1000).optional(),
409
+ /**
410
+ * A named color preset KEY (`"blue"`, `"mint"`, …), never a hex value.
411
+ *
412
+ * Here at CREATION for the reason `isPrivateAccount` is, in miniature: for a
413
+ * managed account the color is a visual identity, and an account that is
414
+ * discoverable without one and acquires it on a second request is a face that
415
+ * changes by itself. One statement, one row, born looking like what its owner
416
+ * chose.
417
+ *
418
+ * The VALUE is checked in the API rather than here. The vocabulary is
419
+ * `USER_COLOR_PRESETS`, which is declared next to the `users_color_check` CHECK
420
+ * that is rendered from it — pinning the list a second time in this package
421
+ * would be a second source of truth for what the database accepts, and the two
422
+ * would drift apart silently. What this shape does is keep an over-long or
423
+ * non-string value from reaching the service at all.
424
+ */
425
+ color: zod_1.z.string().trim().max(32).optional(),
426
+ /** Ordered, PRIMARY FIRST — see rule 2 above {@link ACCOUNT_CATEGORY_IDS}. */
427
+ accountCategories: exports.accountCategoriesSchema.optional(),
428
+ /**
429
+ * Create the account already opted OUT of discovery.
430
+ *
431
+ * ## Why this belongs at CREATION and not only on the privacy route
432
+ *
433
+ * Every account is born discoverable: the column defaults to `false` and
434
+ * nothing on the create path wrote it, so a new account appears in people
435
+ * search the instant it exists. For a human signing themselves up that is the
436
+ * right default and it is NOT changed here. For an account a program creates
437
+ * on someone's behalf — an agent, an unlaunched project, an organization for
438
+ * something not yet announced — it publishes the thing before its owner ever
439
+ * decided to.
440
+ *
441
+ * The alternative is a second call right after create, which is a window in
442
+ * which the account IS public, and a window whose closing depends on a second
443
+ * request succeeding. A field here has neither: one statement, one row, born
444
+ * in the state the caller asked for.
445
+ *
446
+ * ## It reuses the existing flag deliberately
447
+ *
448
+ * This is `privacy_is_private_account`, the same one `PUT /users/:id/privacy`
449
+ * toggles — not a new "published" column. A second visibility flag would be a
450
+ * second source of truth for one question, and the two would disagree.
451
+ *
452
+ * Inherited semantics, stated because reusing a flag means inheriting ALL of
453
+ * it: the account is kept out of people search, out of the follow-graph lists
454
+ * (`followers` / `following` / `mutuals`), out of `/similar` and out of the
455
+ * recommendation candidate pools, and its non-public, non-unlisted media
456
+ * becomes follower-gated. It does NOT hide the profile from someone who knows
457
+ * the handle, and it carries NO follow-approval flow — following is immediate
458
+ * and unilateral whatever this says, so nothing here creates a request queue
459
+ * nobody attends.
460
+ *
461
+ * ## Not conditioned on `kind`, on purpose
462
+ *
463
+ * The same reasoning as `accountCategories` above: this object does not
464
+ * refine on kind, and an unlaunched organization has exactly the problem an
465
+ * unpublished agent does. The discovery predicate never reads `kind`, so the
466
+ * remedy must not either.
467
+ */
468
+ isPrivateAccount: zod_1.z.boolean().optional(),
469
+ }).superRefine((request, ctx) => {
470
+ // The ONE place `kind` and `username` arrive in the same object, so it is the
471
+ // only place a wire schema CAN apply the per-kind half of the policy: a bot's
472
+ // handle must end in `bot`. Not a second rule — it asks
473
+ // `usernameSchemaForAccountKind`, the same declaration the service asks.
474
+ //
475
+ // It is here rather than only in the API because this schema is exported for
476
+ // CLIENTS: an agent-creation flow that validates its request and is then 400ed
477
+ // by the server is the "propose, then refuse" defect the minimum length
478
+ // already caused once. The service check stays regardless — it also governs
479
+ // renames and the service-provisioned channel route, where the kind comes from
480
+ // the stored row and never from this object.
481
+ const parsed = (0, username_1.usernameSchemaForAccountKind)(request.kind).safeParse(request.username);
482
+ if (!parsed.success) {
483
+ ctx.addIssue({
484
+ code: zod_1.z.ZodIssueCode.custom,
485
+ path: ['username'],
486
+ message: parsed.error.issues[0].message,
487
+ });
488
+ }
489
+ });