@oxy.so/contracts 4.16.0 → 4.21.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 (135) hide show
  1. package/dist/cjs/accountAccess.js +72 -0
  2. package/dist/cjs/accountEmail.js +12 -34
  3. package/dist/cjs/accountGraph.js +22 -320
  4. package/dist/cjs/agency.js +112 -1
  5. package/dist/cjs/agentKeys.js +11 -13
  6. package/dist/cjs/applicationRoles.js +211 -0
  7. package/dist/cjs/client/accountEmail.js +48 -0
  8. package/dist/cjs/client/accountGraph.js +323 -0
  9. package/dist/cjs/client/agentKeys.js +77 -0
  10. package/dist/cjs/client/check.js +475 -0
  11. package/dist/cjs/client/deviceBoot.js +22 -0
  12. package/dist/cjs/client/deviceDirectory.js +51 -0
  13. package/dist/cjs/client/deviceSession.js +63 -0
  14. package/dist/cjs/client/externalIdentity.js +19 -0
  15. package/dist/cjs/client/identity.js +45 -0
  16. package/dist/cjs/client/identityLink.js +55 -0
  17. package/dist/cjs/client/identityProof.js +109 -0
  18. package/dist/cjs/client/index.js +211 -0
  19. package/dist/cjs/client/organizations.js +114 -0
  20. package/dist/cjs/client/products.js +153 -0
  21. package/dist/cjs/client/recommendations.js +33 -0
  22. package/dist/cjs/client/reputation.js +41 -0
  23. package/dist/cjs/client/safety.js +196 -0
  24. package/dist/cjs/client/sessionStatus.js +23 -0
  25. package/dist/cjs/client/signIn.js +159 -0
  26. package/dist/cjs/client/userResponse.js +15 -0
  27. package/dist/cjs/client/username.js +173 -0
  28. package/dist/cjs/deviceSession.js +3 -16
  29. package/dist/cjs/identityLink.js +13 -34
  30. package/dist/cjs/identityProof.js +18 -106
  31. package/dist/cjs/index.js +13 -3
  32. package/dist/cjs/organizations.js +21 -41
  33. package/dist/cjs/reputation.js +3 -34
  34. package/dist/cjs/safety.js +430 -1
  35. package/dist/cjs/serviceToken.js +14 -0
  36. package/dist/cjs/signIn.js +33 -111
  37. package/dist/cjs/username.js +23 -158
  38. package/dist/esm/accountAccess.js +69 -0
  39. package/dist/esm/accountEmail.js +4 -32
  40. package/dist/esm/accountGraph.js +4 -309
  41. package/dist/esm/agency.js +111 -0
  42. package/dist/esm/agentKeys.js +4 -10
  43. package/dist/esm/applicationRoles.js +207 -0
  44. package/dist/esm/client/accountEmail.js +45 -0
  45. package/dist/esm/client/accountGraph.js +314 -0
  46. package/dist/esm/client/agentKeys.js +74 -0
  47. package/dist/esm/client/check.js +436 -0
  48. package/dist/esm/client/deviceBoot.js +19 -0
  49. package/dist/esm/client/deviceDirectory.js +48 -0
  50. package/dist/esm/client/deviceSession.js +60 -0
  51. package/dist/esm/client/externalIdentity.js +16 -0
  52. package/dist/esm/client/identity.js +42 -0
  53. package/dist/esm/client/identityLink.js +50 -0
  54. package/dist/esm/client/identityProof.js +104 -0
  55. package/dist/esm/client/index.js +37 -0
  56. package/dist/esm/client/organizations.js +111 -0
  57. package/dist/esm/client/products.js +150 -0
  58. package/dist/esm/client/recommendations.js +30 -0
  59. package/dist/esm/client/reputation.js +37 -0
  60. package/dist/esm/client/safety.js +193 -0
  61. package/dist/esm/client/sessionStatus.js +20 -0
  62. package/dist/esm/client/signIn.js +153 -0
  63. package/dist/esm/client/userResponse.js +12 -0
  64. package/dist/esm/client/username.js +167 -0
  65. package/dist/esm/deviceSession.js +1 -15
  66. package/dist/esm/identityLink.js +4 -27
  67. package/dist/esm/identityProof.js +4 -99
  68. package/dist/esm/index.js +4 -1
  69. package/dist/esm/organizations.js +4 -31
  70. package/dist/esm/reputation.js +1 -32
  71. package/dist/esm/safety.js +423 -0
  72. package/dist/esm/serviceToken.js +11 -0
  73. package/dist/esm/signIn.js +4 -96
  74. package/dist/esm/username.js +4 -148
  75. package/dist/types/accountAccess.d.ts +128 -0
  76. package/dist/types/accountEmail.d.ts +4 -36
  77. package/dist/types/accountGraph.d.ts +12 -208
  78. package/dist/types/agency.d.ts +351 -26
  79. package/dist/types/agentKeys.d.ts +14 -12
  80. package/dist/types/applicationRoles.d.ts +438 -0
  81. package/dist/types/client/accountEmail.d.ts +44 -0
  82. package/dist/types/client/accountGraph.d.ts +203 -0
  83. package/dist/types/client/agentKeys.d.ts +113 -0
  84. package/dist/types/client/check.d.ts +187 -0
  85. package/dist/types/client/deviceBoot.d.ts +6 -0
  86. package/dist/types/client/deviceDirectory.d.ts +134 -0
  87. package/dist/types/client/deviceSession.d.ts +91 -0
  88. package/dist/types/client/externalIdentity.d.ts +14 -0
  89. package/dist/types/client/identity.d.ts +8 -0
  90. package/dist/types/client/identityLink.d.ts +21 -0
  91. package/dist/types/client/identityProof.d.ts +73 -0
  92. package/dist/types/client/index.d.ts +37 -0
  93. package/dist/types/client/organizations.d.ts +167 -0
  94. package/dist/types/client/products.d.ts +145 -0
  95. package/dist/types/client/recommendations.d.ts +27 -0
  96. package/dist/types/client/reputation.d.ts +28 -0
  97. package/dist/types/client/safety.d.ts +213 -0
  98. package/dist/types/client/sessionStatus.d.ts +19 -0
  99. package/dist/types/client/signIn.d.ts +107 -0
  100. package/dist/types/client/userResponse.d.ts +7 -0
  101. package/dist/types/client/username.d.ts +126 -0
  102. package/dist/types/deviceDirectory.d.ts +28 -28
  103. package/dist/types/deviceSession.d.ts +5 -16
  104. package/dist/types/email/messages.d.ts +2 -2
  105. package/dist/types/externalIdentity.d.ts +15 -15
  106. package/dist/types/identityLink.d.ts +3 -11
  107. package/dist/types/identityProof.d.ts +2 -65
  108. package/dist/types/index.d.ts +5 -2
  109. package/dist/types/inference/accountBilling.d.ts +2 -2
  110. package/dist/types/inference/catalogue.d.ts +22 -22
  111. package/dist/types/inference/decisions.d.ts +10 -10
  112. package/dist/types/inference/embeddings.d.ts +16 -16
  113. package/dist/types/inference/errors.d.ts +8 -8
  114. package/dist/types/inference/inbox.d.ts +8 -8
  115. package/dist/types/inference/privateAutoExecution.d.ts +6 -6
  116. package/dist/types/inference/providerConnection.d.ts +4 -4
  117. package/dist/types/inference/realtime.d.ts +92 -92
  118. package/dist/types/inference/request.d.ts +48 -48
  119. package/dist/types/inference/streamEvents.d.ts +16 -16
  120. package/dist/types/inference/usage.d.ts +4 -4
  121. package/dist/types/oauth.d.ts +2 -2
  122. package/dist/types/organizations.d.ts +8 -10
  123. package/dist/types/products/access.d.ts +4 -4
  124. package/dist/types/products/billing.d.ts +3 -0
  125. package/dist/types/reputation.d.ts +1 -11
  126. package/dist/types/safety.d.ts +2374 -2
  127. package/dist/types/serviceToken.d.ts +11 -0
  128. package/dist/types/sessionStatus.d.ts +2 -2
  129. package/dist/types/signIn.d.ts +11 -94
  130. package/dist/types/userResponse.d.ts +22 -22
  131. package/dist/types/username.d.ts +1 -96
  132. package/package.json +24 -20
  133. package/dist/cjs/.tsbuildinfo +0 -1
  134. package/dist/esm/.tsbuildinfo +0 -1
  135. package/dist/types/.tsbuildinfo +0 -1
@@ -10,239 +10,12 @@ import { z } from 'zod';
10
10
  import { avatarCharacterSchema } from './avatarCharacter.js';
11
11
  import { ORGANIZATION_TYPES, ORGANIZATION_NAME_MAX_LENGTH, organizationNameSchema, } from './organizations.js';
12
12
  import { usernameSchema, usernameSchemaForAccountKind } from './username.js';
13
- /**
14
- * The union is spelled out above and the array proves coverage BOTH ways
15
- * (`satisfies` here, the `Gap` alias below) — the same shape this package's
16
- * `ACCOUNT_CATEGORY_IDS` / `TRUST_TIERS` pairs use, and the one
17
- * `db/schema/users.ts` mirrors to keep the `users_kind_check` CHECK honest.
18
- *
19
- * Deriving the union from the array instead would cost nothing here and be paid
20
- * by consumers: `kind` travels into `@oxy.so/services` on every device-directory
21
- * context (`deviceContextSchema.kind` → `DeviceContext` → the switcher rows),
22
- * and an indexed-access type is materially more expensive to check there than a
23
- * literal union.
24
- */
25
- export const ACCOUNT_KINDS = [
26
- 'personal',
27
- 'organization',
28
- 'project',
29
- 'bot',
30
- 'channel',
31
- ];
13
+ // The Zod-free half of this contract lives in `./client/` (`@oxy.so/contracts/client`);
14
+ // it is re-exported here so the root entry is unchanged.
15
+ import { ACCOUNT_KINDS, CHILD_ACCOUNT_KINDS, ACCOUNT_CATEGORY_IDS, MAX_ACCOUNT_CATEGORIES, } from './client/accountGraph.js';
16
+ export { ACCOUNT_KINDS, CHILD_ACCOUNT_KINDS, isDelegatedActAsEligibleKind, isOperatorSwitchTargetKind, isAccountKind, ACCOUNT_CATEGORY_IDS, RETIRED_ACCOUNT_CATEGORY_IDS, isSelectableAccountCategoryId, SELECTABLE_ACCOUNT_CATEGORY_IDS, newlyAddedRetiredCategories, MAX_ACCOUNT_CATEGORIES, ACCOUNT_CATEGORY_KINDS, kindAcceptsAccountCategories, } from './client/accountGraph.js';
32
17
  export const accountKindSchema = z.enum(ACCOUNT_KINDS);
33
- export const CHILD_ACCOUNT_KINDS = [
34
- 'organization',
35
- 'project',
36
- 'bot',
37
- 'channel',
38
- ];
39
18
  export const childAccountKindSchema = z.enum(CHILD_ACCOUNT_KINDS);
40
- /**
41
- * Whether an account of this kind may be the SUBJECT OF A DELEGATION — an
42
- * application acting as it on some person's authority. `POST /internal/accounts/
43
- * :id/service-switch` and the OAuth delegated subject both gate on this.
44
- *
45
- * It is NOT the question an account switcher asks. See
46
- * {@link isOperatorSwitchTargetKind}, and the note below on why the difference
47
- * is `bot`.
48
- *
49
- * Two kinds are refused, for opposite reasons:
50
- *
51
- * - `personal` is a human login, so assuming it would be impersonation.
52
- * - `channel` is a CONTENT identity, not an operating one. A channel exists so
53
- * that posts can be authored BY it; it is never a seat anybody occupies. Its
54
- * operators act on it through their own membership, and an application
55
- * publishes to it with its own credential. Refusing act-as is what makes
56
- * "no login, ever" structural rather than incidental: no session can be
57
- * minted whose subject is a channel, so no bearer exists that could add an
58
- * auth method to one (every auth-method write resolves its target from the
59
- * authenticated subject, never from a parameter).
60
- *
61
- * Consumers must gate on this predicate rather than testing `kind === 'personal'`,
62
- * which silently admits every kind added after it was written.
63
- *
64
- * Being act-as-eligible is NOT being without an identity of one's own. A bot is
65
- * act-as-eligible AND its own actor (`accountKindActorNature` → `'agent'` in
66
- * `accountSubject.ts`): an application acting as it on a person's authority
67
- * records that person as the actor, and the bot acting with nobody operating it
68
- * records the bot. Neither ever records the bot's owner in the bot's place.
69
- */
70
- export function isDelegatedActAsEligibleKind(kind) {
71
- return kind === 'organization' || kind === 'project' || kind === 'bot';
72
- }
73
- /**
74
- * Whether a PERSON may switch into an account of this kind — become it, in an
75
- * account switcher, for the rest of their session.
76
- *
77
- * ## Why this is not the same question as {@link isDelegatedActAsEligibleKind}
78
- *
79
- * The two differ on exactly one kind, `bot`, and that difference is the whole
80
- * reason both exist.
81
- *
82
- * **A bot is not something you become. It is somebody of its own.** It is an
83
- * agent's identity, with its own resources, roles, plan and balance, and it is
84
- * the actor of what it does unoperated. A person may still act AS it on their
85
- * own authority — that is delegation, recorded with the person as actor, and it
86
- * is what {@link isDelegatedActAsEligibleKind} admits it for. What a person
87
- * never does is occupy its seat. This is a refusal of a SEAT, not a capability
88
- * restriction: nothing a bot may do depends on this predicate.
89
- *
90
- * Handing a person the bot's seat instead inverts that. It puts a human inside
91
- * the identity that exists to act without one, and it does so on the human's own
92
- * device, next to their personal login — which is precisely what happened: a
93
- * `bot` account held a live session on a person's device, offered to them by a
94
- * switcher that had asked the delegation question by mistake.
95
- *
96
- * `channel` is refused here as well, for the reason set out above, and
97
- * `personal` because assuming somebody else's login is impersonation.
98
- *
99
- * ## This is the narrower predicate, deliberately
100
- *
101
- * Everything a person may become, a service may also act as; the reverse does
102
- * not hold. A caller that is unsure which question it is asking wants THIS one:
103
- * being wrong here withholds an affordance, while being wrong the other way
104
- * hands out a seat.
105
- */
106
- export function isOperatorSwitchTargetKind(kind) {
107
- return kind === 'organization' || kind === 'project';
108
- }
109
- /**
110
- * Narrow an unknown value to an {@link AccountKind}.
111
- *
112
- * The user-DTO serializers read from structurally-permissive `unknown` sources
113
- * (a Drizzle row, a raw document, an already-formatted object), so each one
114
- * would otherwise hand-roll this check and they would drift on what counts.
115
- */
116
- export function isAccountKind(value) {
117
- return typeof value === 'string' && ACCOUNT_KINDS.includes(value);
118
- }
119
- // ===========================================================================
120
- // Account categories
121
- //
122
- // Four rules govern this taxonomy. Each one is here because the obvious
123
- // alternative fails silently rather than loudly.
124
- //
125
- // 1. AN ID IS AN OPAQUE, IMMUTABLE SLUG — the LABEL is not stored anywhere.
126
- // Every value below is an identifier that no rename may ever change. The
127
- // human-readable label lives in each client's own translation catalogue,
128
- // keyed by the id. Re-labelling `agency` from "Real estate agency" to
129
- // "Agency" is therefore a one-line locale edit that touches no row; moving
130
- // the label into the column or the DTO would make the same edit a data
131
- // migration, and would pin every reader to the language of whoever chose it.
132
- //
133
- // 2. THE PRIMARY CATEGORY IS THE FIRST ELEMENT of an ordered list. Not a
134
- // separate flag, and not a second field: a flag admits two primaries and
135
- // admits none, and ordering makes both unrepresentable. The cost is that
136
- // ORDER IS DATA — anything that re-serializes, sorts or de-duplicates this
137
- // list can change which category is primary without erroring, so no layer
138
- // between the column and the client may reorder it.
139
- //
140
- // 3. THE VOCABULARY IS APPEND-ONLY. `ACCOUNT_CATEGORY_IDS` may gain ids and may
141
- // never lose one, because rows already carry the ids it holds. Withdrawing a
142
- // category means listing it in {@link RETIRED_ACCOUNT_CATEGORY_IDS}, which
143
- // removes it from the picker while leaving it readable and re-writable. The
144
- // database enforces the same asymmetry: its CHECK is re-evaluated on EVERY
145
- // update to a row, so narrowing the allowed set makes an unrelated write —
146
- // saving a bio — fail on any account that had picked the withdrawn value.
147
- // Measured on a real Postgres, `NOT VALID` included; it does not help.
148
- //
149
- // 4. THE SET IS CLOSED. An id nobody can validate is an id no client can render:
150
- // the label comes from a translation key derived from the id, so an invented
151
- // value paints as a blank or a raw slug in every app at once. Closing it also
152
- // costs nothing that rule 3 does not already charge — adding a category needs
153
- // a migration to widen the CHECK whether or not the enum exists, so an open
154
- // list would buy back no work, only the validation.
155
- // ===========================================================================
156
- /**
157
- * Every account category, by stable id.
158
- *
159
- * Grouped by comment for readability only; the storage, the wire and the picker
160
- * all treat this as one flat list. `other` is the escape hatch for an account
161
- * that fits nothing here.
162
- *
163
- * TO ADD ONE: append an id (lowercase ASCII, `snake_case`) here, publish
164
- * `@oxy.so/contracts`, then ship a migration that widens
165
- * `users_account_categories_check` — never edit an existing migration — and add
166
- * an `accounts.accountCategory.<id>` label to each client's locales.
167
- *
168
- * TO WITHDRAW ONE: leave the id here and add it to
169
- * {@link RETIRED_ACCOUNT_CATEGORY_IDS}. See rule 3 above.
170
- */
171
- export const ACCOUNT_CATEGORY_IDS = [
172
- // ---- media & public information -----------------------------------------
173
- 'news',
174
- 'politics',
175
- // ---- business & economy --------------------------------------------------
176
- 'business',
177
- 'startup',
178
- 'finance',
179
- 'crypto',
180
- 'marketplace',
181
- 'retail',
182
- // The four ids the single-valued `organizationCategory` field used to hold,
183
- // carried forward VERBATIM. Their labels may be rewritten freely; their ids
184
- // may not, because live rows hold them.
185
- 'real_estate',
186
- 'agency',
187
- 'landlord',
188
- 'cooperative',
189
- 'architecture',
190
- // ---- technology ----------------------------------------------------------
191
- 'technology',
192
- 'software',
193
- 'ai',
194
- 'security',
195
- 'automation',
196
- // ---- knowledge -----------------------------------------------------------
197
- 'science',
198
- 'education',
199
- 'books',
200
- // ---- health --------------------------------------------------------------
201
- 'health',
202
- 'fitness',
203
- // ---- sport & play --------------------------------------------------------
204
- 'sports',
205
- 'gaming',
206
- // ---- culture & entertainment ---------------------------------------------
207
- //
208
- // There is deliberately NO generic `entertainment` here, and re-adding one is
209
- // a regression rather than a gap. It is the only id this list ever carried
210
- // that was dominated by its own specifics — `film`, `music`, `gaming` and
211
- // `comedy` all exist — and a generic drawer sitting beside its four
212
- // concretions collects the lazy pick, which degrades the data for all four at
213
- // once: the accounts that would have said `film` say `entertainment` instead,
214
- // and `film` stops meaning what it meant.
215
- //
216
- // The other overlaps in this vocabulary are NOT the same case and must not be
217
- // merged on this reasoning: `sports`/`fitness`, `art`/`photography`,
218
- // `business`/`startup`, `finance`/`crypto`, `home_garden`/`diy` and
219
- // `technology`/`software`/`security` are genuinely different audiences, and
220
- // with a cap of four the granularity is cheap.
221
- 'music',
222
- 'film',
223
- 'podcast',
224
- 'art',
225
- 'photography',
226
- 'comedy',
227
- // ---- everyday life -------------------------------------------------------
228
- 'food',
229
- 'travel',
230
- 'fashion',
231
- 'home_garden',
232
- 'diy',
233
- 'automotive',
234
- 'animals',
235
- 'family',
236
- // ---- society -------------------------------------------------------------
237
- 'nonprofit',
238
- 'government',
239
- 'community',
240
- 'activism',
241
- 'environment',
242
- 'religion',
243
- // ---- fallback ------------------------------------------------------------
244
- 'other',
245
- ];
246
19
  /**
247
20
  * Accepts EVERY id, withdrawn ones included — see rule 3.
248
21
  *
@@ -252,59 +25,6 @@ export const ACCOUNT_CATEGORY_IDS = [
252
25
  * nullable `bio` / `avatar` fix addressed, wearing a different hat.
253
26
  */
254
27
  export const accountCategoryIdSchema = z.enum(ACCOUNT_CATEGORY_IDS);
255
- /**
256
- * Ids withdrawn from the picker. Empty today.
257
- *
258
- * A withdrawn id keeps working everywhere it is already stored: it validates,
259
- * it survives a round-trip save, it still renders from its label key, and it
260
- * stays PRIMARY if it was primary. Nothing rewrites a stored list — a read-time
261
- * or migration-time demotion would silently replace a choice its owner made,
262
- * which is precisely what stable ids exist to prevent. The owner drops it on
263
- * their next edit; until then it is honoured.
264
- *
265
- * What withdrawal changes is only this: the id leaves
266
- * {@link SELECTABLE_ACCOUNT_CATEGORY_IDS}, so no picker offers it, and
267
- * {@link newlyAddedRetiredCategories} refuses to let a write ADD it to an
268
- * account that did not already have it.
269
- */
270
- export const RETIRED_ACCOUNT_CATEGORY_IDS = [];
271
- /** Whether a category may still be OFFERED. A stored one is readable either way. */
272
- export function isSelectableAccountCategoryId(id) {
273
- return !RETIRED_ACCOUNT_CATEGORY_IDS.includes(id);
274
- }
275
- /** The ids a picker may offer, in declaration order. */
276
- export const SELECTABLE_ACCOUNT_CATEGORY_IDS = ACCOUNT_CATEGORY_IDS.filter(isSelectableAccountCategoryId);
277
- /**
278
- * Which of `next` are withdrawn ids the account did not already carry — i.e.
279
- * the ones a write must be refused for.
280
- *
281
- * `retired` is a parameter rather than a module read so the rule can be
282
- * exercised against a non-empty set while the production one is empty; a test
283
- * over `RETIRED_ACCOUNT_CATEGORY_IDS` alone would pass vacuously today and stay
284
- * passing if the rule were deleted.
285
- */
286
- export function newlyAddedRetiredCategories(next, previous, retired) {
287
- return next.filter((id) => retired.includes(id) && !previous.includes(id));
288
- }
289
- /**
290
- * How many categories one account may carry.
291
- *
292
- * Four, not "as many as you like". Three reasons, in the order they bind:
293
- *
294
- * - The primary has to MEAN something. At ten categories the first element
295
- * reads as a sort artifact rather than a choice, and rule 2 above is the
296
- * entire mechanism by which a primary exists.
297
- * - The profile RENDERS them as a row of chips; four labels of this length is
298
- * what fits a phone-width profile header before the row wraps or truncates.
299
- * - Four is enough to place a genuinely compound account without a tag cloud:
300
- * a housing cooperative that is also a non-profit serving a local community
301
- * spends `cooperative`, `nonprofit`, `community`, `real_estate` — and is the
302
- * most compound real example in the ecosystem.
303
- *
304
- * One constant, read by the wire schema, the database CHECK and the picker, so
305
- * changing it is one edit plus a migration.
306
- */
307
- export const MAX_ACCOUNT_CATEGORIES = 4;
308
28
  /**
309
29
  * An account's categories on the wire. ORDER IS MEANINGFUL — index 0 is the
310
30
  * primary (rule 2).
@@ -331,31 +51,6 @@ export const accountCategoriesSchema = z
331
51
  seen.add(id);
332
52
  });
333
53
  });
334
- /**
335
- * Kinds that may carry categories: every kind EXCEPT `personal`.
336
- *
337
- * A person has interests, not a sector — and their interests are not a
338
- * classification anybody else gets to read off their profile. Spelled out
339
- * positively, like {@link isDelegatedActAsEligibleKind} and for the same reason: a `kind
340
- * !== 'personal'` test silently admits every kind invented after it was
341
- * written, whereas this list forces whoever adds one to decide.
342
- */
343
- export const ACCOUNT_CATEGORY_KINDS = [
344
- 'organization',
345
- 'project',
346
- 'bot',
347
- 'channel',
348
- ];
349
- /**
350
- * Whether an account of this kind may carry categories.
351
- *
352
- * The API refuses the write and the `users_account_categories_kind_check`
353
- * constraint makes it unrepresentable; both derive from
354
- * {@link ACCOUNT_CATEGORY_KINDS}, so they cannot disagree.
355
- */
356
- export function kindAcceptsAccountCategories(kind) {
357
- return ACCOUNT_CATEGORY_KINDS.includes(kind ?? '');
358
- }
359
54
  /**
360
55
  * An account's name on the create/update wire.
361
56
  *
@@ -140,6 +140,35 @@ export const delegationGrantSchema = z
140
140
  updatedAt: z.string().datetime(),
141
141
  })
142
142
  .strict();
143
+ /**
144
+ * The two levels an owner publishes for whoever uses their agent (ADR 0042):
145
+ * "Ver" and "Ver y actuar". A ceiling never says `draft` or `execute_on_request`:
146
+ * an agent is either allowed to act on its own or not at all, and nothing ever
147
+ * asks the person for approval.
148
+ */
149
+ export const AGENT_REQUESTER_CEILING_AUTONOMY = ['read_only', 'autonomous'];
150
+ /** What an agent may do in one app for whoever uses it; written by an operator of the bot. */
151
+ export const agentRequesterCeilingWriteSchema = z
152
+ .object({
153
+ capabilityPackages: z.array(capabilityPackageSchema),
154
+ capabilities: z.array(identifierSchema).default([]),
155
+ toolOverrides: z.array(toolGrantOverrideSchema).default([]),
156
+ limits: z.array(grantLimitSchema).default([]),
157
+ maximumAutonomy: z.enum(AGENT_REQUESTER_CEILING_AUTONOMY),
158
+ })
159
+ .strict();
160
+ export const agentRequesterCeilingSchema = agentRequesterCeilingWriteSchema
161
+ .extend({
162
+ id: identifierSchema,
163
+ agentAccountId: identifierSchema,
164
+ appId: identifierSchema,
165
+ catalog: capabilityCatalogBindingSchema,
166
+ version: z.number().int().positive(),
167
+ revokedAt: z.string().datetime().nullable(),
168
+ createdAt: z.string().datetime(),
169
+ updatedAt: z.string().datetime(),
170
+ })
171
+ .strict();
143
172
  export const automationTriggerSchema = z.discriminatedUnion('type', [
144
173
  z.object({ type: z.literal('manual') }).strict(),
145
174
  z
@@ -201,6 +230,8 @@ export const capabilityTicketClaimsSchema = z
201
230
  /** Signed only for callers that explicitly requested a pinned catalogue. */
202
231
  catalog: capabilityCatalogBindingSchema.optional(),
203
232
  grantId: identifierSchema.optional(),
233
+ /** The owner's published ceiling that bounds an agent acting for a non-owner (ADR 0042). */
234
+ ceilingId: identifierSchema.optional(),
204
235
  requesterAccountId: identifierSchema,
205
236
  ownerAccountId: identifierSchema,
206
237
  actor: executionActorRefSchema,
@@ -239,6 +270,24 @@ export const capabilityTicketClaimsSchema = z
239
270
  message: 'requester actors require their exact principal and direct read-only authority without standing grants',
240
271
  });
241
272
  }
273
+ if (claims.ceilingId !== undefined) {
274
+ if (claims.grantId !== undefined) {
275
+ context.addIssue({
276
+ code: z.ZodIssueCode.custom,
277
+ path: ['ceilingId'],
278
+ message: 'a ticket is bounded by either a grant or a ceiling, never both',
279
+ });
280
+ }
281
+ if (claims.actor.type !== 'agent' ||
282
+ claims.ownerAccountId !== claims.requesterAccountId ||
283
+ claims.resource.resourceId !== claims.resource.effectiveAccountId) {
284
+ context.addIssue({
285
+ code: z.ZodIssueCode.custom,
286
+ path: ['ceilingId'],
287
+ message: "a ceiling bounds an agent acting on the requester's own authority at an account root",
288
+ });
289
+ }
290
+ }
242
291
  if (claims.exp <= claims.iat) {
243
292
  context.addIssue({
244
293
  code: z.ZodIssueCode.custom,
@@ -300,6 +349,7 @@ export const policyDecisionSchema = z
300
349
  reason: identifierSchema,
301
350
  effectiveAutonomy: autonomyLevelSchema.optional(),
302
351
  grantId: identifierSchema.optional(),
352
+ ceilingId: identifierSchema.optional(),
303
353
  })
304
354
  .strict();
305
355
  export const auditResultSchema = z
@@ -509,6 +559,24 @@ export const catalogEventSchema = z
509
559
  resourceTypes: z.array(identifierSchema).min(1),
510
560
  })
511
561
  .strict();
562
+ /**
563
+ * A feature an agent may gate on (Alia ADR 0020 §6): the app answers whether the
564
+ * person may use it (`checkTool`) and, when using it costs something, spends it
565
+ * (`consumeTool`). Both are internal tools of the same catalogue.
566
+ */
567
+ export const catalogFeatureSchema = z
568
+ .object({
569
+ key: z
570
+ .string()
571
+ .regex(/^[a-z][a-z0-9_-]*(?:\.[a-z][a-z0-9_-]*)*$/, 'feature keys are dot-separated slugs')
572
+ .max(128),
573
+ description: nonEmptyStringSchema,
574
+ checkTool: identifierSchema,
575
+ consumeTool: identifierSchema.optional(),
576
+ })
577
+ .strict();
578
+ /** How a coordinator reaches the catalogue's internal tools. */
579
+ export const CATALOG_INTERNAL_TRANSPORTS = ['http', 'mcp'];
512
580
  export const appCapabilityCatalogSchema = z
513
581
  .object({
514
582
  schemaVersion: z.literal('1'),
@@ -564,11 +632,54 @@ export const appCapabilityCatalogSchema = z
564
632
  })
565
633
  .strict()
566
634
  .optional(),
635
+ /**
636
+ * `mcp`: internal tools are served at `<internalBaseUrl>/_oxy/mcp`. Absent or
637
+ * `http`: each tool's `invocation` path on `internalBaseUrl`.
638
+ */
639
+ internalTransport: z.enum(CATALOG_INTERNAL_TRANSPORTS).optional(),
640
+ features: z.array(catalogFeatureSchema).optional(),
567
641
  tools: z.array(catalogToolSchema),
568
642
  events: z.array(catalogEventSchema),
569
643
  })
570
644
  .strict()
571
645
  .superRefine((catalog, context) => {
646
+ const toolsByName = new Map(catalog.tools.map((tool) => [tool.name, tool]));
647
+ const featureKeys = new Set();
648
+ for (const [index, feature] of (catalog.features ?? []).entries()) {
649
+ if (featureKeys.has(feature.key)) {
650
+ context.addIssue({
651
+ code: z.ZodIssueCode.custom,
652
+ message: `Duplicate feature key: ${feature.key}`,
653
+ path: ['features', index, 'key'],
654
+ });
655
+ }
656
+ featureKeys.add(feature.key);
657
+ const check = toolsByName.get(feature.checkTool);
658
+ if (!check ||
659
+ check.effect !== 'read' ||
660
+ check.exposure.length !== 1 ||
661
+ check.exposure[0] !== 'internal') {
662
+ context.addIssue({
663
+ code: z.ZodIssueCode.custom,
664
+ message: 'checkTool must name an internal-only read tool of this catalogue',
665
+ path: ['features', index, 'checkTool'],
666
+ });
667
+ }
668
+ if (feature.consumeTool !== undefined) {
669
+ const consume = toolsByName.get(feature.consumeTool);
670
+ if (!consume ||
671
+ consume.effect === 'read' ||
672
+ consume.idempotency !== 'required' ||
673
+ consume.exposure.length !== 1 ||
674
+ consume.exposure[0] !== 'internal') {
675
+ context.addIssue({
676
+ code: z.ZodIssueCode.custom,
677
+ message: 'consumeTool must name an internal-only effectful tool that requires idempotency keys',
678
+ path: ['features', index, 'consumeTool'],
679
+ });
680
+ }
681
+ }
682
+ }
572
683
  const names = new Set();
573
684
  for (const [index, tool] of catalog.tools.entries()) {
574
685
  if (names.has(tool.name)) {
@@ -2,15 +2,10 @@
2
2
  import { z } from 'zod';
3
3
  import { reauthProofSchema } from './signIn.js';
4
4
  import { canonicalJson } from './identityProof.js';
5
- export const AGENT_PROOF_AUDIENCE = 'oxy-api/agent';
6
- export const AGENT_PROOF_TTL_MS = 60000;
7
- export const AGENT_PROOF_ACTIONS = [
8
- 'agent_signin',
9
- 'agent_enroll',
10
- 'agent_rotate',
11
- 'agent_recover',
12
- 'agent_governance',
13
- ];
5
+ // The Zod-free half of this contract lives in `./client/` (`@oxy.so/contracts/client`);
6
+ // it is re-exported here so the root entry is unchanged.
7
+ import { AGENT_PROOF_AUDIENCE, AGENT_PROOF_ACTIONS } from './client/agentKeys.js';
8
+ export { AGENT_PROOF_AUDIENCE, AGENT_PROOF_TTL_MS, AGENT_PROOF_ACTIONS, ACCOUNT_AUTH_METHODS, } from './client/agentKeys.js';
14
9
  export const agentProofClaimsSchema = z
15
10
  .object({
16
11
  version: z.literal(1),
@@ -97,7 +92,6 @@ export const agentKeyOperationResultSchema = z
97
92
  revokedMethodIds: z.array(z.string()),
98
93
  })
99
94
  .strict();
100
- export const ACCOUNT_AUTH_METHODS = ['identity', 'agent_key'];
101
95
  /** Public credential inventory. Never includes a proof, bearer or private key. */
102
96
  export const agentKeyListResponseSchema = z
103
97
  .object({