@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
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ /**
3
+ * Wire contract for `GET /internal/accounts/:accountId/access/:userId`: Oxy
4
+ * answers a trusted first-party service's question "may this person act FOR
5
+ * that account, and with what?" with no session of that person in the request.
6
+ *
7
+ * It exists for the account Oxy never lets anybody act AS: a `channel`
8
+ * ({@link isDelegatedActAsEligibleKind} refuses it). An app publishes to a
9
+ * channel with ITS OWN credential, on behalf of a member Oxy vouches for here,
10
+ * so nobody holds a bearer that speaks as the channel. A long-running import
11
+ * (Oxy Move) has only a service token and `X-Oxy-User-Id`, which the
12
+ * user-facing `GET /accounts/:id/members` refuses; this is its answer.
13
+ *
14
+ * Needs a service token whose application is platform-trusted AND holds the
15
+ * privileged {@link ACCOUNTS_MEMBERSHIP_READ_SCOPE}. The answer is always 200:
16
+ * `access` is `null` for no active membership, an archived account, an unknown
17
+ * account and an unclaimed directory account alike. Callers must treat every
18
+ * other status as a refusal (fail closed), and must not cache the answer longer
19
+ * than the action it authorizes.
20
+ *
21
+ * Platform-agnostic — zod only.
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.accountAccessVerdictSchema = exports.accountAccessGrantSchema = exports.accountAccessSourceSchema = exports.ACCOUNT_ACCESS_SOURCES = exports.ACCOUNTS_MEMBERSHIP_READ_SCOPE = void 0;
25
+ const zod_1 = require("zod");
26
+ const accountGraph_1 = require("./accountGraph");
27
+ /**
28
+ * The scope the endpoint requires.
29
+ *
30
+ * Privileged (staff-granted). Channel membership is sensitive because a channel
31
+ * is anonymous: its members are the people behind a voice that is published
32
+ * without their names, and "is this person a member of that channel" is
33
+ * exactly the fact the channel exists to withhold.
34
+ */
35
+ exports.ACCOUNTS_MEMBERSHIP_READ_SCOPE = 'accounts:membership:read';
36
+ /**
37
+ * Where the access comes from.
38
+ *
39
+ * - `self` — the person IS the account (a personal account asked about itself).
40
+ * - `direct` — an active membership row on the account.
41
+ * - `inherited` — an active, cascading membership on an ancestor account.
42
+ */
43
+ exports.ACCOUNT_ACCESS_SOURCES = ['self', 'direct', 'inherited'];
44
+ exports.accountAccessSourceSchema = zod_1.z.enum(exports.ACCOUNT_ACCESS_SOURCES);
45
+ /**
46
+ * One person's EFFECTIVE access over one account.
47
+ *
48
+ * `role` and `permissions` are strings rather than closed enums on purpose: the
49
+ * role and permission vocabularies are the API's, they grow, and a consumer
50
+ * pinned to an older contracts release must keep reading new ones. Gate on
51
+ * `permissions` (the role's baseline adjusted by per-member grants and
52
+ * revokes), never on `role`.
53
+ */
54
+ exports.accountAccessGrantSchema = zod_1.z.object({
55
+ role: zod_1.z.string().min(1),
56
+ permissions: zod_1.z.array(zod_1.z.string().min(1)),
57
+ /** Only an active membership grants access, so this is the one value. */
58
+ status: zod_1.z.literal('active'),
59
+ source: exports.accountAccessSourceSchema,
60
+ });
61
+ exports.accountAccessVerdictSchema = zod_1.z
62
+ .object({
63
+ accountId: zod_1.z.string().min(1),
64
+ /** `null` when no such account exists. */
65
+ kind: accountGraph_1.accountKindSchema.nullable(),
66
+ /** `null` means: this person may not act for this account. */
67
+ access: exports.accountAccessGrantSchema.nullable(),
68
+ })
69
+ .refine((verdict) => verdict.access === null || verdict.kind !== null, {
70
+ message: 'An account that does not exist grants no access',
71
+ path: ['access'],
72
+ });
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.EMAIL_VERIFICATION_ERROR_CODES = exports.emailVerificationConfirmResponseSchema = exports.emailVerificationConfirmRequestSchema = exports.emailVerificationStartResponseSchema = exports.emailVerificationStartRequestSchema = exports.emailTicketSchema = exports.emailAddressSchema = exports.EMAIL_TICKET_TTL_MS = exports.EMAIL_CODE_MAX_ATTEMPTS = exports.EMAIL_CODE_TTL_MS = exports.EMAIL_CODE_LENGTH = exports.EMAIL_VERIFICATION_PURPOSES = void 0;
3
+ exports.emailVerificationConfirmResponseSchema = exports.emailVerificationConfirmRequestSchema = exports.emailVerificationStartResponseSchema = exports.emailVerificationStartRequestSchema = exports.emailTicketSchema = exports.emailAddressSchema = exports.EMAIL_VERIFICATION_ERROR_CODES = exports.EMAIL_TICKET_TTL_MS = exports.EMAIL_CODE_MAX_ATTEMPTS = exports.EMAIL_CODE_TTL_MS = exports.EMAIL_CODE_LENGTH = exports.EMAIL_VERIFICATION_PURPOSES = void 0;
4
4
  /**
5
5
  * Email codes and tickets (ADR 0030).
6
6
  *
@@ -16,22 +16,16 @@ exports.EMAIL_VERIFICATION_ERROR_CODES = exports.emailVerificationConfirmRespons
16
16
  * (`reauth`) use the same code, through their own routes (`signIn.ts`).
17
17
  */
18
18
  const zod_1 = require("zod");
19
- /**
20
- * - `signup`: above.
21
- * - `signin`: the code (and link) of an email sign-in (`POST /auth/signin/email/start`).
22
- * - `reauth`: a signed-in person proving it is them before a sensitive step
23
- * (`POST /users/me/reauth/email`): a password, an authenticator, deleting the
24
- * account, linking Commons.
25
- */
26
- exports.EMAIL_VERIFICATION_PURPOSES = ['signup', 'signin', 'reauth'];
27
- /** Digits in a code. */
28
- exports.EMAIL_CODE_LENGTH = 6;
29
- /** How long a code can be confirmed. */
30
- exports.EMAIL_CODE_TTL_MS = 10 * 60 * 1000;
31
- /** Wrong codes before a verification is spent and a new code is needed. */
32
- exports.EMAIL_CODE_MAX_ATTEMPTS = 5;
33
- /** How long a confirmed code's ticket can be spent. */
34
- exports.EMAIL_TICKET_TTL_MS = 15 * 60 * 1000;
19
+ // The Zod-free half of this contract lives in `./client/` (`@oxy.so/contracts/client`);
20
+ // it is re-exported here so the root entry is unchanged.
21
+ const accountEmail_1 = require("./client/accountEmail");
22
+ var accountEmail_2 = require("./client/accountEmail");
23
+ Object.defineProperty(exports, "EMAIL_VERIFICATION_PURPOSES", { enumerable: true, get: function () { return accountEmail_2.EMAIL_VERIFICATION_PURPOSES; } });
24
+ Object.defineProperty(exports, "EMAIL_CODE_LENGTH", { enumerable: true, get: function () { return accountEmail_2.EMAIL_CODE_LENGTH; } });
25
+ Object.defineProperty(exports, "EMAIL_CODE_TTL_MS", { enumerable: true, get: function () { return accountEmail_2.EMAIL_CODE_TTL_MS; } });
26
+ Object.defineProperty(exports, "EMAIL_CODE_MAX_ATTEMPTS", { enumerable: true, get: function () { return accountEmail_2.EMAIL_CODE_MAX_ATTEMPTS; } });
27
+ Object.defineProperty(exports, "EMAIL_TICKET_TTL_MS", { enumerable: true, get: function () { return accountEmail_2.EMAIL_TICKET_TTL_MS; } });
28
+ Object.defineProperty(exports, "EMAIL_VERIFICATION_ERROR_CODES", { enumerable: true, get: function () { return accountEmail_2.EMAIL_VERIFICATION_ERROR_CODES; } });
35
29
  /** An email address as the API stores it: trimmed and lowercase. */
36
30
  exports.emailAddressSchema = zod_1.z.string().trim().toLowerCase().min(3).max(254).email();
37
31
  /** An opaque one-use ticket (32 random bytes, base64url). */
@@ -54,26 +48,10 @@ exports.emailVerificationConfirmRequestSchema = zod_1.z
54
48
  code: zod_1.z
55
49
  .string()
56
50
  .trim()
57
- .regex(new RegExp(`^\\d{${exports.EMAIL_CODE_LENGTH}}$`), `code must be ${exports.EMAIL_CODE_LENGTH} digits`),
51
+ .regex(new RegExp(`^\\d{${accountEmail_1.EMAIL_CODE_LENGTH}}$`), `code must be ${accountEmail_1.EMAIL_CODE_LENGTH} digits`),
58
52
  })
59
53
  .strict();
60
54
  exports.emailVerificationConfirmResponseSchema = zod_1.z.object({
61
55
  ticket: exports.emailTicketSchema,
62
56
  expiresAt: zod_1.z.number().int().positive(),
63
57
  });
64
- /**
65
- * Stable error codes (`error.code` in the API error body). Clients map these
66
- * through their localization, never the English message.
67
- */
68
- exports.EMAIL_VERIFICATION_ERROR_CODES = {
69
- /** The code is wrong, or its verification expired or was spent. */
70
- codeInvalid: 'EMAIL_CODE_INVALID',
71
- /** Too many wrong codes: request a new one. */
72
- tooManyAttempts: 'EMAIL_CODE_TOO_MANY_ATTEMPTS',
73
- /** The ticket is unknown, expired, spent, or for another email or purpose. */
74
- ticketInvalid: 'EMAIL_TICKET_INVALID',
75
- /** A sign-up without a confirmed email. */
76
- ticketRequired: 'EMAIL_TICKET_REQUIRED',
77
- /** This server cannot send mail. */
78
- unavailable: 'EMAIL_UNAVAILABLE',
79
- };
@@ -8,250 +8,30 @@
8
8
  * {@link ACCOUNT_CATEGORY_IDS} for the four rules that govern it.
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.createAccountRequestSchema = exports.accountNameSchema = 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;
11
+ exports.createAccountRequestSchema = exports.accountNameSchema = exports.accountCategoriesSchema = exports.accountCategoryIdSchema = exports.childAccountKindSchema = exports.accountKindSchema = exports.kindAcceptsAccountCategories = exports.ACCOUNT_CATEGORY_KINDS = exports.MAX_ACCOUNT_CATEGORIES = exports.newlyAddedRetiredCategories = exports.SELECTABLE_ACCOUNT_CATEGORY_IDS = exports.isSelectableAccountCategoryId = exports.RETIRED_ACCOUNT_CATEGORY_IDS = exports.ACCOUNT_CATEGORY_IDS = exports.isAccountKind = exports.isOperatorSwitchTargetKind = exports.isDelegatedActAsEligibleKind = exports.CHILD_ACCOUNT_KINDS = exports.ACCOUNT_KINDS = void 0;
18
12
  const zod_1 = require("zod");
19
13
  const avatarCharacter_1 = require("./avatarCharacter");
20
14
  const organizations_1 = require("./organizations");
21
15
  const username_1 = require("./username");
22
- /**
23
- * The union is spelled out above and the array proves coverage BOTH ways
24
- * (`satisfies` here, the `Gap` alias below) — the same shape this package's
25
- * `ACCOUNT_CATEGORY_IDS` / `TRUST_TIERS` pairs use, and the one
26
- * `db/schema/users.ts` mirrors to keep the `users_kind_check` CHECK honest.
27
- *
28
- * Deriving the union from the array instead would cost nothing here and be paid
29
- * by consumers: `kind` travels into `@oxy.so/services` on every device-directory
30
- * context (`deviceContextSchema.kind` → `DeviceContext` → the switcher rows),
31
- * and an indexed-access type is materially more expensive to check there than a
32
- * literal union.
33
- */
34
- exports.ACCOUNT_KINDS = [
35
- 'personal',
36
- 'organization',
37
- 'project',
38
- 'bot',
39
- 'channel',
40
- ];
41
- exports.accountKindSchema = zod_1.z.enum(exports.ACCOUNT_KINDS);
42
- exports.CHILD_ACCOUNT_KINDS = [
43
- 'organization',
44
- 'project',
45
- 'bot',
46
- 'channel',
47
- ];
48
- exports.childAccountKindSchema = zod_1.z.enum(exports.CHILD_ACCOUNT_KINDS);
49
- /**
50
- * Whether an account of this kind may be the SUBJECT OF A DELEGATION — an
51
- * application acting as it on some person's authority. `POST /internal/accounts/
52
- * :id/service-switch` and the OAuth delegated subject both gate on this.
53
- *
54
- * It is NOT the question an account switcher asks. See
55
- * {@link isOperatorSwitchTargetKind}, and the note below on why the difference
56
- * is `bot`.
57
- *
58
- * Two kinds are refused, for opposite reasons:
59
- *
60
- * - `personal` is a human login, so assuming it would be impersonation.
61
- * - `channel` is a CONTENT identity, not an operating one. A channel exists so
62
- * that posts can be authored BY it; it is never a seat anybody occupies. Its
63
- * operators act on it through their own membership, and an application
64
- * publishes to it with its own credential. Refusing act-as is what makes
65
- * "no login, ever" structural rather than incidental: no session can be
66
- * minted whose subject is a channel, so no bearer exists that could add an
67
- * auth method to one (every auth-method write resolves its target from the
68
- * authenticated subject, never from a parameter).
69
- *
70
- * Consumers must gate on this predicate rather than testing `kind === 'personal'`,
71
- * which silently admits every kind added after it was written.
72
- *
73
- * Being act-as-eligible is NOT being without an identity of one's own. A bot is
74
- * act-as-eligible AND its own actor (`accountKindActorNature` → `'agent'` in
75
- * `accountSubject.ts`): an application acting as it on a person's authority
76
- * records that person as the actor, and the bot acting with nobody operating it
77
- * records the bot. Neither ever records the bot's owner in the bot's place.
78
- */
79
- function isDelegatedActAsEligibleKind(kind) {
80
- return kind === 'organization' || kind === 'project' || kind === 'bot';
81
- }
82
- /**
83
- * Whether a PERSON may switch into an account of this kind — become it, in an
84
- * account switcher, for the rest of their session.
85
- *
86
- * ## Why this is not the same question as {@link isDelegatedActAsEligibleKind}
87
- *
88
- * The two differ on exactly one kind, `bot`, and that difference is the whole
89
- * reason both exist.
90
- *
91
- * **A bot is not something you become. It is somebody of its own.** It is an
92
- * agent's identity, with its own resources, roles, plan and balance, and it is
93
- * the actor of what it does unoperated. A person may still act AS it on their
94
- * own authority — that is delegation, recorded with the person as actor, and it
95
- * is what {@link isDelegatedActAsEligibleKind} admits it for. What a person
96
- * never does is occupy its seat. This is a refusal of a SEAT, not a capability
97
- * restriction: nothing a bot may do depends on this predicate.
98
- *
99
- * Handing a person the bot's seat instead inverts that. It puts a human inside
100
- * the identity that exists to act without one, and it does so on the human's own
101
- * device, next to their personal login — which is precisely what happened: a
102
- * `bot` account held a live session on a person's device, offered to them by a
103
- * switcher that had asked the delegation question by mistake.
104
- *
105
- * `channel` is refused here as well, for the reason set out above, and
106
- * `personal` because assuming somebody else's login is impersonation.
107
- *
108
- * ## This is the narrower predicate, deliberately
109
- *
110
- * Everything a person may become, a service may also act as; the reverse does
111
- * not hold. A caller that is unsure which question it is asking wants THIS one:
112
- * being wrong here withholds an affordance, while being wrong the other way
113
- * hands out a seat.
114
- */
115
- function isOperatorSwitchTargetKind(kind) {
116
- return kind === 'organization' || kind === 'project';
117
- }
118
- /**
119
- * Narrow an unknown value to an {@link AccountKind}.
120
- *
121
- * The user-DTO serializers read from structurally-permissive `unknown` sources
122
- * (a Drizzle row, a raw document, an already-formatted object), so each one
123
- * would otherwise hand-roll this check and they would drift on what counts.
124
- */
125
- function isAccountKind(value) {
126
- return typeof value === 'string' && exports.ACCOUNT_KINDS.includes(value);
127
- }
128
- // ===========================================================================
129
- // Account categories
130
- //
131
- // Four rules govern this taxonomy. Each one is here because the obvious
132
- // alternative fails silently rather than loudly.
133
- //
134
- // 1. AN ID IS AN OPAQUE, IMMUTABLE SLUG — the LABEL is not stored anywhere.
135
- // Every value below is an identifier that no rename may ever change. The
136
- // human-readable label lives in each client's own translation catalogue,
137
- // keyed by the id. Re-labelling `agency` from "Real estate agency" to
138
- // "Agency" is therefore a one-line locale edit that touches no row; moving
139
- // the label into the column or the DTO would make the same edit a data
140
- // migration, and would pin every reader to the language of whoever chose it.
141
- //
142
- // 2. THE PRIMARY CATEGORY IS THE FIRST ELEMENT of an ordered list. Not a
143
- // separate flag, and not a second field: a flag admits two primaries and
144
- // admits none, and ordering makes both unrepresentable. The cost is that
145
- // ORDER IS DATA — anything that re-serializes, sorts or de-duplicates this
146
- // list can change which category is primary without erroring, so no layer
147
- // between the column and the client may reorder it.
148
- //
149
- // 3. THE VOCABULARY IS APPEND-ONLY. `ACCOUNT_CATEGORY_IDS` may gain ids and may
150
- // never lose one, because rows already carry the ids it holds. Withdrawing a
151
- // category means listing it in {@link RETIRED_ACCOUNT_CATEGORY_IDS}, which
152
- // removes it from the picker while leaving it readable and re-writable. The
153
- // database enforces the same asymmetry: its CHECK is re-evaluated on EVERY
154
- // update to a row, so narrowing the allowed set makes an unrelated write —
155
- // saving a bio — fail on any account that had picked the withdrawn value.
156
- // Measured on a real Postgres, `NOT VALID` included; it does not help.
157
- //
158
- // 4. THE SET IS CLOSED. An id nobody can validate is an id no client can render:
159
- // the label comes from a translation key derived from the id, so an invented
160
- // value paints as a blank or a raw slug in every app at once. Closing it also
161
- // costs nothing that rule 3 does not already charge — adding a category needs
162
- // a migration to widen the CHECK whether or not the enum exists, so an open
163
- // list would buy back no work, only the validation.
164
- // ===========================================================================
165
- /**
166
- * Every account category, by stable id.
167
- *
168
- * Grouped by comment for readability only; the storage, the wire and the picker
169
- * all treat this as one flat list. `other` is the escape hatch for an account
170
- * that fits nothing here.
171
- *
172
- * TO ADD ONE: append an id (lowercase ASCII, `snake_case`) here, publish
173
- * `@oxy.so/contracts`, then ship a migration that widens
174
- * `users_account_categories_check` — never edit an existing migration — and add
175
- * an `accounts.accountCategory.<id>` label to each client's locales.
176
- *
177
- * TO WITHDRAW ONE: leave the id here and add it to
178
- * {@link RETIRED_ACCOUNT_CATEGORY_IDS}. See rule 3 above.
179
- */
180
- exports.ACCOUNT_CATEGORY_IDS = [
181
- // ---- media & public information -----------------------------------------
182
- 'news',
183
- 'politics',
184
- // ---- business & economy --------------------------------------------------
185
- 'business',
186
- 'startup',
187
- 'finance',
188
- 'crypto',
189
- 'marketplace',
190
- 'retail',
191
- // The four ids the single-valued `organizationCategory` field used to hold,
192
- // carried forward VERBATIM. Their labels may be rewritten freely; their ids
193
- // may not, because live rows hold them.
194
- 'real_estate',
195
- 'agency',
196
- 'landlord',
197
- 'cooperative',
198
- 'architecture',
199
- // ---- technology ----------------------------------------------------------
200
- 'technology',
201
- 'software',
202
- 'ai',
203
- 'security',
204
- 'automation',
205
- // ---- knowledge -----------------------------------------------------------
206
- 'science',
207
- 'education',
208
- 'books',
209
- // ---- health --------------------------------------------------------------
210
- 'health',
211
- 'fitness',
212
- // ---- sport & play --------------------------------------------------------
213
- 'sports',
214
- 'gaming',
215
- // ---- culture & entertainment ---------------------------------------------
216
- //
217
- // There is deliberately NO generic `entertainment` here, and re-adding one is
218
- // a regression rather than a gap. It is the only id this list ever carried
219
- // that was dominated by its own specifics — `film`, `music`, `gaming` and
220
- // `comedy` all exist — and a generic drawer sitting beside its four
221
- // concretions collects the lazy pick, which degrades the data for all four at
222
- // once: the accounts that would have said `film` say `entertainment` instead,
223
- // and `film` stops meaning what it meant.
224
- //
225
- // The other overlaps in this vocabulary are NOT the same case and must not be
226
- // merged on this reasoning: `sports`/`fitness`, `art`/`photography`,
227
- // `business`/`startup`, `finance`/`crypto`, `home_garden`/`diy` and
228
- // `technology`/`software`/`security` are genuinely different audiences, and
229
- // with a cap of four the granularity is cheap.
230
- 'music',
231
- 'film',
232
- 'podcast',
233
- 'art',
234
- 'photography',
235
- 'comedy',
236
- // ---- everyday life -------------------------------------------------------
237
- 'food',
238
- 'travel',
239
- 'fashion',
240
- 'home_garden',
241
- 'diy',
242
- 'automotive',
243
- 'animals',
244
- 'family',
245
- // ---- society -------------------------------------------------------------
246
- 'nonprofit',
247
- 'government',
248
- 'community',
249
- 'activism',
250
- 'environment',
251
- 'religion',
252
- // ---- fallback ------------------------------------------------------------
253
- 'other',
254
- ];
16
+ // The Zod-free half of this contract lives in `./client/` (`@oxy.so/contracts/client`);
17
+ // it is re-exported here so the root entry is unchanged.
18
+ const accountGraph_1 = require("./client/accountGraph");
19
+ var accountGraph_2 = require("./client/accountGraph");
20
+ Object.defineProperty(exports, "ACCOUNT_KINDS", { enumerable: true, get: function () { return accountGraph_2.ACCOUNT_KINDS; } });
21
+ Object.defineProperty(exports, "CHILD_ACCOUNT_KINDS", { enumerable: true, get: function () { return accountGraph_2.CHILD_ACCOUNT_KINDS; } });
22
+ Object.defineProperty(exports, "isDelegatedActAsEligibleKind", { enumerable: true, get: function () { return accountGraph_2.isDelegatedActAsEligibleKind; } });
23
+ Object.defineProperty(exports, "isOperatorSwitchTargetKind", { enumerable: true, get: function () { return accountGraph_2.isOperatorSwitchTargetKind; } });
24
+ Object.defineProperty(exports, "isAccountKind", { enumerable: true, get: function () { return accountGraph_2.isAccountKind; } });
25
+ Object.defineProperty(exports, "ACCOUNT_CATEGORY_IDS", { enumerable: true, get: function () { return accountGraph_2.ACCOUNT_CATEGORY_IDS; } });
26
+ Object.defineProperty(exports, "RETIRED_ACCOUNT_CATEGORY_IDS", { enumerable: true, get: function () { return accountGraph_2.RETIRED_ACCOUNT_CATEGORY_IDS; } });
27
+ Object.defineProperty(exports, "isSelectableAccountCategoryId", { enumerable: true, get: function () { return accountGraph_2.isSelectableAccountCategoryId; } });
28
+ Object.defineProperty(exports, "SELECTABLE_ACCOUNT_CATEGORY_IDS", { enumerable: true, get: function () { return accountGraph_2.SELECTABLE_ACCOUNT_CATEGORY_IDS; } });
29
+ Object.defineProperty(exports, "newlyAddedRetiredCategories", { enumerable: true, get: function () { return accountGraph_2.newlyAddedRetiredCategories; } });
30
+ Object.defineProperty(exports, "MAX_ACCOUNT_CATEGORIES", { enumerable: true, get: function () { return accountGraph_2.MAX_ACCOUNT_CATEGORIES; } });
31
+ Object.defineProperty(exports, "ACCOUNT_CATEGORY_KINDS", { enumerable: true, get: function () { return accountGraph_2.ACCOUNT_CATEGORY_KINDS; } });
32
+ Object.defineProperty(exports, "kindAcceptsAccountCategories", { enumerable: true, get: function () { return accountGraph_2.kindAcceptsAccountCategories; } });
33
+ exports.accountKindSchema = zod_1.z.enum(accountGraph_1.ACCOUNT_KINDS);
34
+ exports.childAccountKindSchema = zod_1.z.enum(accountGraph_1.CHILD_ACCOUNT_KINDS);
255
35
  /**
256
36
  * Accepts EVERY id, withdrawn ones included — see rule 3.
257
37
  *
@@ -260,60 +40,7 @@ exports.ACCOUNT_CATEGORY_IDS = [
260
40
  * picked one could no longer save its bio either. That is the same failure the
261
41
  * nullable `bio` / `avatar` fix addressed, wearing a different hat.
262
42
  */
263
- exports.accountCategoryIdSchema = zod_1.z.enum(exports.ACCOUNT_CATEGORY_IDS);
264
- /**
265
- * Ids withdrawn from the picker. Empty today.
266
- *
267
- * A withdrawn id keeps working everywhere it is already stored: it validates,
268
- * it survives a round-trip save, it still renders from its label key, and it
269
- * stays PRIMARY if it was primary. Nothing rewrites a stored list — a read-time
270
- * or migration-time demotion would silently replace a choice its owner made,
271
- * which is precisely what stable ids exist to prevent. The owner drops it on
272
- * their next edit; until then it is honoured.
273
- *
274
- * What withdrawal changes is only this: the id leaves
275
- * {@link SELECTABLE_ACCOUNT_CATEGORY_IDS}, so no picker offers it, and
276
- * {@link newlyAddedRetiredCategories} refuses to let a write ADD it to an
277
- * account that did not already have it.
278
- */
279
- exports.RETIRED_ACCOUNT_CATEGORY_IDS = [];
280
- /** Whether a category may still be OFFERED. A stored one is readable either way. */
281
- function isSelectableAccountCategoryId(id) {
282
- return !exports.RETIRED_ACCOUNT_CATEGORY_IDS.includes(id);
283
- }
284
- /** The ids a picker may offer, in declaration order. */
285
- exports.SELECTABLE_ACCOUNT_CATEGORY_IDS = exports.ACCOUNT_CATEGORY_IDS.filter(isSelectableAccountCategoryId);
286
- /**
287
- * Which of `next` are withdrawn ids the account did not already carry — i.e.
288
- * the ones a write must be refused for.
289
- *
290
- * `retired` is a parameter rather than a module read so the rule can be
291
- * exercised against a non-empty set while the production one is empty; a test
292
- * over `RETIRED_ACCOUNT_CATEGORY_IDS` alone would pass vacuously today and stay
293
- * passing if the rule were deleted.
294
- */
295
- function newlyAddedRetiredCategories(next, previous, retired) {
296
- return next.filter((id) => retired.includes(id) && !previous.includes(id));
297
- }
298
- /**
299
- * How many categories one account may carry.
300
- *
301
- * Four, not "as many as you like". Three reasons, in the order they bind:
302
- *
303
- * - The primary has to MEAN something. At ten categories the first element
304
- * reads as a sort artifact rather than a choice, and rule 2 above is the
305
- * entire mechanism by which a primary exists.
306
- * - The profile RENDERS them as a row of chips; four labels of this length is
307
- * what fits a phone-width profile header before the row wraps or truncates.
308
- * - Four is enough to place a genuinely compound account without a tag cloud:
309
- * a housing cooperative that is also a non-profit serving a local community
310
- * spends `cooperative`, `nonprofit`, `community`, `real_estate` — and is the
311
- * most compound real example in the ecosystem.
312
- *
313
- * One constant, read by the wire schema, the database CHECK and the picker, so
314
- * changing it is one edit plus a migration.
315
- */
316
- exports.MAX_ACCOUNT_CATEGORIES = 4;
43
+ exports.accountCategoryIdSchema = zod_1.z.enum(accountGraph_1.ACCOUNT_CATEGORY_IDS);
317
44
  /**
318
45
  * An account's categories on the wire. ORDER IS MEANINGFUL — index 0 is the
319
46
  * primary (rule 2).
@@ -326,7 +53,7 @@ exports.MAX_ACCOUNT_CATEGORIES = 4;
326
53
  */
327
54
  exports.accountCategoriesSchema = zod_1.z
328
55
  .array(exports.accountCategoryIdSchema)
329
- .max(exports.MAX_ACCOUNT_CATEGORIES)
56
+ .max(accountGraph_1.MAX_ACCOUNT_CATEGORIES)
330
57
  .superRefine((ids, ctx) => {
331
58
  const seen = new Set();
332
59
  ids.forEach((id, index) => {
@@ -340,31 +67,6 @@ exports.accountCategoriesSchema = zod_1.z
340
67
  seen.add(id);
341
68
  });
342
69
  });
343
- /**
344
- * Kinds that may carry categories: every kind EXCEPT `personal`.
345
- *
346
- * A person has interests, not a sector — and their interests are not a
347
- * classification anybody else gets to read off their profile. Spelled out
348
- * positively, like {@link isDelegatedActAsEligibleKind} and for the same reason: a `kind
349
- * !== 'personal'` test silently admits every kind invented after it was
350
- * written, whereas this list forces whoever adds one to decide.
351
- */
352
- exports.ACCOUNT_CATEGORY_KINDS = [
353
- 'organization',
354
- 'project',
355
- 'bot',
356
- 'channel',
357
- ];
358
- /**
359
- * Whether an account of this kind may carry categories.
360
- *
361
- * The API refuses the write and the `users_account_categories_kind_check`
362
- * constraint makes it unrepresentable; both derive from
363
- * {@link ACCOUNT_CATEGORY_KINDS}, so they cannot disagree.
364
- */
365
- function kindAcceptsAccountCategories(kind) {
366
- return exports.ACCOUNT_CATEGORY_KINDS.includes(kind ?? '');
367
- }
368
70
  /**
369
71
  * An account's name on the create/update wire.
370
72
  *