@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,20 @@
1
+ /**
2
+ * The public-application check, without Zod. Schemas: `../sessionStatus.ts`.
3
+ *
4
+ * Part of `@oxy.so/contracts/client`: never import `zod` here (see `./check.ts`).
5
+ */
6
+ import { array, boolean, exact, object, oneOf, optional, string } from './check.js';
7
+ export const publicApplicationCheck = exact()(object({
8
+ id: string(),
9
+ name: string(),
10
+ description: optional(string()),
11
+ icon: optional(string()),
12
+ websiteUrl: optional(string()),
13
+ privacyPolicyUrl: optional(string()),
14
+ termsUrl: optional(string()),
15
+ type: oneOf(['first_party', 'third_party', 'internal', 'system']),
16
+ isOfficial: boolean,
17
+ isInternal: boolean,
18
+ scopes: array(string()),
19
+ developerName: optional(string()),
20
+ }));
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Sign-in constants, guards and response checks, without Zod. Schemas: `../signIn.ts`.
3
+ *
4
+ * Part of `@oxy.so/contracts/client`: never import `zod` here (see `./check.ts`).
5
+ */
6
+ import { EMAIL_CODE_LENGTH, emailTicketCheck } from './accountEmail.js';
7
+ import { array, boolean, exact, exactItems, int, literal, maxItems, maxLength, minItems, minLength, nonnegative, number, object, optional, pattern, positive, refine, startsWith, strictObject, string, trim, } from './check.js';
8
+ /** How long the link in a sign-in email can be opened. */
9
+ export const EMAIL_SIGNIN_LINK_TTL_MS = 15 * 60 * 1000;
10
+ /** How long a second-factor challenge can be answered. */
11
+ export const SIGNIN_SECOND_FACTOR_TTL_MS = 5 * 60 * 1000;
12
+ /** Wrong second-factor codes one challenge accepts. */
13
+ export const SIGNIN_SECOND_FACTOR_MAX_ATTEMPTS = 5;
14
+ /** Shortest password Oxy accepts when one is set. */
15
+ export const PASSWORD_MIN_LENGTH = 10;
16
+ /** Longest password Oxy accepts (characters). */
17
+ export const PASSWORD_MAX_LENGTH = 256;
18
+ /** Digits in an authenticator code. */
19
+ export const TOTP_DIGITS = 6;
20
+ /** Seconds each authenticator code lasts. */
21
+ export const TOTP_PERIOD_SECONDS = 30;
22
+ /** Backup codes issued when an authenticator is enabled or they are regenerated. */
23
+ export const TOTP_BACKUP_CODE_COUNT = 10;
24
+ /**
25
+ * The long sign-in code: ten characters of Crockford base32 without the
26
+ * look-alikes (no 0, O, 1, I, L, U), shown as `XXXXX-XXXXX`. An account whose
27
+ * sign-in codes were guessed at too often today (the per-account ceiling) is
28
+ * sent this instead of 6 digits for the rest of the day, so its owner can still
29
+ * type a code while guessing one is infeasible.
30
+ */
31
+ export const EMAIL_SIGNIN_LONG_CODE_ALPHABET = '23456789ABCDEFGHJKMNPQRSTVWXYZ';
32
+ export const EMAIL_SIGNIN_LONG_CODE_LENGTH = 10;
33
+ /**
34
+ * Normalise a typed sign-in code: 6 digits stay as they are; a long code is
35
+ * upper-cased with its dash and spaces removed. Anything else is returned
36
+ * trimmed (and will simply be wrong).
37
+ */
38
+ export function normalizeEmailSignInCode(code) {
39
+ const compact = code.trim().replace(/[\s-]/g, '').toUpperCase();
40
+ return compact;
41
+ }
42
+ /**
43
+ * How long, once the email is proven, the person has to choose which of its
44
+ * accounts signs in.
45
+ */
46
+ export const EMAIL_SIGNIN_CHOICE_TTL_MS = 5 * 60 * 1000;
47
+ /** Most accounts one email can hold; sign-up refuses another past it. */
48
+ export const EMAIL_ACCOUNTS_MAX = 20;
49
+ /** Narrow a {@link SignInStepResult}. */
50
+ export function isSecondFactorRequired(result) {
51
+ return result.secondFactorRequired === true;
52
+ }
53
+ /** Narrow an {@link EmailSignInStepResult}. */
54
+ export function isEmailSignInChooseAccount(result) {
55
+ return result.chooseAccount === true;
56
+ }
57
+ /**
58
+ * What a re-verification email code confirms. The code is bound to it: a code
59
+ * asked for one change never authorises another, and the email names it.
60
+ */
61
+ export const REAUTH_ACTIONS = [
62
+ 'change_password',
63
+ 'totp',
64
+ 'link_commons',
65
+ 'delete_account',
66
+ 'credentials_manage',
67
+ 'supervision',
68
+ 'supervision_purchase',
69
+ ];
70
+ /**
71
+ * Stable error codes (`error.code` in the API error body). Clients map these
72
+ * through their localization, never the English message.
73
+ */
74
+ export const SIGN_IN_ERROR_CODES = {
75
+ /** Wrong identifier or password — never says which. */
76
+ invalidCredentials: 'SIGNIN_INVALID_CREDENTIALS',
77
+ /** The request is unknown, expired, spent, or its secret is wrong. */
78
+ requestInvalid: 'SIGNIN_REQUEST_INVALID',
79
+ /** The link is unknown, expired or already used. */
80
+ linkInvalid: 'SIGNIN_LINK_INVALID',
81
+ /** The link was opened in another browser: type the code in the app instead. */
82
+ linkOtherDevice: 'SIGNIN_LINK_OTHER_DEVICE',
83
+ /** The second-factor challenge or its code is wrong, expired or spent. */
84
+ secondFactorInvalid: 'SECOND_FACTOR_INVALID',
85
+ /** Too many failures for this account: wait and try again. */
86
+ locked: 'SIGNIN_LOCKED',
87
+ /** The step needs a fresh proof (`reauth`). */
88
+ reauthRequired: 'REAUTH_REQUIRED',
89
+ /** The fresh proof was wrong. */
90
+ reauthInvalid: 'REAUTH_INVALID',
91
+ /** The account has an authenticator: the proof must carry its code. */
92
+ totpRequired: 'TOTP_REQUIRED',
93
+ /** Enrolling or confirming an authenticator that is already on. */
94
+ totpAlreadyEnabled: 'TOTP_ALREADY_ENABLED',
95
+ /** Disabling, confirming or regenerating codes with no authenticator (or no pending one). */
96
+ totpNotEnabled: 'TOTP_NOT_ENABLED',
97
+ /** The code the authenticator showed at enrolment is wrong. */
98
+ totpCodeInvalid: 'TOTP_CODE_INVALID',
99
+ /** Only official Oxy apps and auth.oxy.so may sign people in here. */
100
+ originNotAllowed: 'SIGNIN_ORIGIN_NOT_ALLOWED',
101
+ /** The username is taken. */
102
+ usernameTaken: 'USERNAME_TAKEN',
103
+ /** The email already holds `EMAIL_ACCOUNTS_MAX` accounts: no sign-up adds another. */
104
+ emailAccountsFull: 'EMAIL_ACCOUNTS_FULL',
105
+ };
106
+ /* -------------------------------------------------------------------------- */
107
+ /* Checks */
108
+ /* -------------------------------------------------------------------------- */
109
+ const expiresAt = number(int, positive);
110
+ const sixDigits = string(trim, pattern(new RegExp(`^\\d{${EMAIL_CODE_LENGTH}}$`), `code must be ${EMAIL_CODE_LENGTH} digits`));
111
+ const passwordInput = string(minLength(1), maxLength(PASSWORD_MAX_LENGTH));
112
+ const secondFactorCode = string(trim, minLength(TOTP_DIGITS), maxLength(16));
113
+ export const emailSignInStartResponseCheck = exact()(object({
114
+ requestId: string(minLength(1), maxLength(64)),
115
+ requestSecret: emailTicketCheck,
116
+ expiresAt,
117
+ retryLater: optional(literal(true)),
118
+ }));
119
+ export const emailSignInLinkResponseCheck = exact()(object({ approved: literal(true) }));
120
+ export const secondFactorRequiredCheck = exact()(object({ secondFactorRequired: literal(true), challengeId: emailTicketCheck, expiresAt }));
121
+ export const emailSignInPendingCheck = exact()(object({ status: literal('pending'), expiresAt }));
122
+ const emailSignInAccountOption = exact()(object({
123
+ id: string(minLength(1), maxLength(64)),
124
+ username: optional(string(minLength(1))),
125
+ displayName: optional(string(minLength(1))),
126
+ avatar: optional(string(minLength(1))),
127
+ }));
128
+ export const emailSignInChooseAccountCheck = exact()(object({
129
+ chooseAccount: literal(true),
130
+ accounts: array(emailSignInAccountOption, minItems(2), maxItems(EMAIL_ACCOUNTS_MAX)),
131
+ expiresAt,
132
+ }));
133
+ export const signInMethodsCheck = exact()(object({
134
+ hasEmail: boolean,
135
+ hasPassword: boolean,
136
+ totpEnabled: boolean,
137
+ backupCodesRemaining: number(int, nonnegative),
138
+ }));
139
+ export const totpEnrollResponseCheck = exact()(object({
140
+ secret: string(pattern(/^[A-Z2-7]+$/)),
141
+ otpauthUri: string(startsWith('otpauth://totp/')),
142
+ }));
143
+ export const totpBackupCodesResponseCheck = exact()(object({
144
+ backupCodes: array(string(pattern(/^[a-z2-9]{5}-[a-z2-9]{5}$/)), exactItems(TOTP_BACKUP_CODE_COUNT)),
145
+ }));
146
+ export const reauthProofCheck = exact()(refine(strictObject({
147
+ password: optional(passwordInput),
148
+ emailCode: optional(strictObject({
149
+ verificationId: string(trim, minLength(1), maxLength(64)),
150
+ code: sixDigits,
151
+ })),
152
+ totpCode: optional(secondFactorCode),
153
+ }), (proof) => proof.password !== undefined || proof.emailCode !== undefined, 'password or emailCode is required'));
@@ -0,0 +1,12 @@
1
+ /**
2
+ * User-response checks, without Zod. Schemas: `../userResponse.ts`.
3
+ *
4
+ * Part of `@oxy.so/contracts/client`: never import `zod` here (see `./check.ts`).
5
+ */
6
+ import { exact, looseObject, optional, string } from './check.js';
7
+ export const userNameCheck = exact()(looseObject({
8
+ first: optional(string()),
9
+ last: optional(string()),
10
+ full: optional(string()),
11
+ displayName: optional(string()),
12
+ }));
@@ -0,0 +1,167 @@
1
+ /**
2
+ * The username policy, without Zod. Schemas: `../username.ts`.
3
+ *
4
+ * Part of `@oxy.so/contracts/client`: never import `zod` here (see `./check.ts`).
5
+ */
6
+ import { maxLength, minLength, pattern, refine, string, trim } from './check.js';
7
+ /** Shortest storable handle. `oxy` sets the floor. */
8
+ export const USERNAME_MIN_LENGTH = 3;
9
+ /** Longest storable handle, and the `maxLength` an input field should carry. */
10
+ export const USERNAME_MAX_LENGTH = 30;
11
+ /** The 400 / inline-validation copy for every path that rejects a handle. */
12
+ export const USERNAME_INVALID_MESSAGE = 'Username must be 3-30 characters, use only letters, numbers, hyphens and underscores, ' +
13
+ 'start and end with a letter or number, and never repeat a separator';
14
+ /**
15
+ * Exact-match handles nobody may newly claim, regardless of account kind.
16
+ *
17
+ * This is NOT a namespace tightening like the bot suffix — it is a list of
18
+ * names withheld from `users_lower_username_key` before anybody asks for
19
+ * them. Compared against the same normalization the unique index applies
20
+ * (`lower(btrim(username))`), so `Admin`, ` ADMIN ` and `admin` are the one
21
+ * name this list means.
22
+ *
23
+ * `oxy`, `mention`, `homiio`, `clarity`, `faircoin`, `astro` and `mercaria`
24
+ * are deliberately NOT here: each already has an `organization`/`project`
25
+ * row, so the unique index already refuses a second one, and `oxy` is the
26
+ * name `USERNAME_MIN_LENGTH` is pinned against in
27
+ * `__tests__/username.test.ts` — listing it would make
28
+ * `usernameSchema.safeParse('oxy')` fail and falsify that comment. A brand
29
+ * with no account of its own yet (measured 2026-09-18: Alia, Allo, TNP,
30
+ * Kaana, Bloom) has no row to protect it, so it is listed until one exists.
31
+ */
32
+ export const RESERVED_USERNAMES = new Set([
33
+ // Oxy product lines with no account row of their own yet.
34
+ 'alia',
35
+ 'allo',
36
+ 'tnp',
37
+ 'kaana',
38
+ 'bloom',
39
+ // System / role words a signup impersonating staff or Oxy itself would reach for.
40
+ 'admin',
41
+ 'administrator',
42
+ 'root',
43
+ 'superadmin',
44
+ 'super',
45
+ 'superuser',
46
+ 'support',
47
+ 'staff',
48
+ 'moderator',
49
+ 'mod',
50
+ 'security',
51
+ 'system',
52
+ 'official',
53
+ 'help',
54
+ 'noreply',
55
+ 'anonymous',
56
+ 'everyone',
57
+ 'owner',
58
+ ]);
59
+ /** The 400 / inline-validation copy for a handle on {@link RESERVED_USERNAMES}. */
60
+ export const RESERVED_USERNAME_MESSAGE = 'This username is reserved and cannot be registered';
61
+ /**
62
+ * {@link RESERVED_USERNAMES}, minus `alia`, checked against each `-`/`_`
63
+ * separated SEGMENT of a candidate rather than the whole string — so
64
+ * `official-oxy`, `super-admin` and `team-kaana` are refused the same as the
65
+ * bare words, without banning every word that merely CONTAINS one as a
66
+ * substring (`superman`, `modern`, `grassroot`, `homeowner` all stay legal:
67
+ * none of them separates the reserved word from the rest with `-` or `_`).
68
+ *
69
+ * `alia` is excluded because `alia-` is the live internal-cost-centre
70
+ * namespace: `alia-production-chat` is a minted account and
71
+ * `alia-research` / `alia-voice` / `alia-evaluations` are pinned as legal
72
+ * slugs in `__tests__/username.test.ts` and `internalCostCenterSpecs.test.ts`.
73
+ * Segment-matching `alia` would refuse all four. The bare word `alia` is
74
+ * still refused — {@link RESERVED_USERNAMES} above catches it exactly.
75
+ */
76
+ export const RESERVED_USERNAME_SEGMENTS = new Set([...RESERVED_USERNAMES].filter((word) => word !== 'alia'));
77
+ /** The 400 / inline-validation copy for a handle that is only digits. */
78
+ export const NUMERIC_USERNAME_MESSAGE = 'Username cannot be only numbers';
79
+ /**
80
+ * Alphanumeric runs joined by single separators, as a SOURCE string.
81
+ *
82
+ * A string rather than a literal because the OpenAPI docblocks that publish this
83
+ * rule (`POST /auth/register`, `PUT /users/:userId`) must quote it verbatim, and
84
+ * `usernamePolicySingleSource.test.ts` compares them against THIS constant. A
85
+ * published `pattern:` that drifts from the enforced rule is a lie told to every
86
+ * client that generates from the spec, and it is exactly the kind of copy nobody
87
+ * notices going stale.
88
+ *
89
+ * Deliberately NOT re-exported from the package barrel: it exists for the
90
+ * schema below and for that one gate. Anything validating a username uses
91
+ * {@link usernameSchema}, so there is no second way to ask the question.
92
+ *
93
+ * Written as an unambiguous alternation rather than a lookahead: every character
94
+ * belongs to exactly one branch, so matching is linear and there is no
95
+ * backtracking to bound. It also carries no `\p{…}` property escape, which
96
+ * mobile Hermes throws on at runtime — this module is reachable from every React
97
+ * Native consumer.
98
+ */
99
+ export const USERNAME_PATTERN_SOURCE = '^[A-Za-z0-9]+(?:[-_][A-Za-z0-9]+)*$';
100
+ export const USERNAME_PATTERN = new RegExp(USERNAME_PATTERN_SOURCE);
101
+ /**
102
+ * Drop the characters the policy forbids, for an input field that filters
103
+ * keystrokes.
104
+ *
105
+ * This is a TYPING aid and nothing else — the result still has to pass
106
+ * {@link usernameSchema}, which is what decides. It does not lower-case (case is
107
+ * preserved, see the header) and it cannot repair a name: a value that is too
108
+ * short, edge-separated or doubly-separated comes back unchanged and fails
109
+ * validation with a message, which is the outcome the coercing rules this
110
+ * replaces used to hide.
111
+ */
112
+ export function stripDisallowedUsernameCharacters(input) {
113
+ return input.replace(/[^A-Za-z0-9_-]/g, '');
114
+ }
115
+ /**
116
+ * The label a bot's handle ends in — the ONE exception the policy carries.
117
+ *
118
+ * Lower-case here because it is what a generator appends; the comparison that
119
+ * enforces it folds case, so `MyBot` satisfies it just as `mybot` does.
120
+ *
121
+ * Deliberately NOT re-exported from the package barrel, for the same reason
122
+ * {@link USERNAME_PATTERN_SOURCE} is not: it exists for the schema and the
123
+ * generator aid below. A consumer that wants to SAY the rule uses
124
+ * {@link BOT_USERNAME_INVALID_MESSAGE}, which already quotes it, and one that
125
+ * wants to APPLY it uses {@link applyBotUsernameSuffix} — so there is no second
126
+ * way to spell the check.
127
+ */
128
+ export const BOT_USERNAME_SUFFIX = 'bot';
129
+ /** The 400 / inline-validation copy for a bot handle that carries no label. */
130
+ export const BOT_USERNAME_INVALID_MESSAGE = `A bot account's username must end in "${BOT_USERNAME_SUFFIX}"`;
131
+ /**
132
+ * Label a proposed bot handle, so a generator PROPOSES something the server will
133
+ * accept.
134
+ *
135
+ * Alia builds an agent's handle from its display name; without this it would
136
+ * propose `garden-helper` for a bot and be 400ed on submit, which is the same
137
+ * "suggest, then refuse" defect the minimum length already caused once.
138
+ *
139
+ * It appends and never inserts: a separator is the caller's choice, so
140
+ * `garden-helper` becomes `garden-helperbot` while a caller who typed
141
+ * `garden-helper-` gets `garden-helper-bot`. It truncates to leave room rather
142
+ * than overflowing {@link USERNAME_MAX_LENGTH}.
143
+ *
144
+ * Like {@link stripDisallowedUsernameCharacters} it is an aid, not a decision: it
145
+ * cannot repair a handle the base policy refuses, and the result still has to
146
+ * pass {@link botUsernameSchema}.
147
+ */
148
+ export function applyBotUsernameSuffix(candidate) {
149
+ const trimmed = candidate.trim();
150
+ if (trimmed.toLowerCase().endsWith(BOT_USERNAME_SUFFIX)) {
151
+ return trimmed;
152
+ }
153
+ return trimmed.slice(0, USERNAME_MAX_LENGTH - BOT_USERNAME_SUFFIX.length) + BOT_USERNAME_SUFFIX;
154
+ }
155
+ /* -------------------------------------------------------------------------- */
156
+ /* Checks */
157
+ /* -------------------------------------------------------------------------- */
158
+ /** The username policy as a check — the twin of `usernameSchema`, same messages. */
159
+ export const usernameCheck = refine(refine(refine(string(trim, minLength(USERNAME_MIN_LENGTH, USERNAME_INVALID_MESSAGE), maxLength(USERNAME_MAX_LENGTH, USERNAME_INVALID_MESSAGE), pattern(USERNAME_PATTERN, USERNAME_INVALID_MESSAGE)), (username) => !/^[0-9]+$/.test(username), NUMERIC_USERNAME_MESSAGE), (username) => !RESERVED_USERNAMES.has(username.toLowerCase()), RESERVED_USERNAME_MESSAGE), (username) => !username
160
+ .split(/[-_]/)
161
+ .some((segment) => RESERVED_USERNAME_SEGMENTS.has(segment.toLowerCase())), RESERVED_USERNAME_MESSAGE);
162
+ /** A bot's username: the policy plus the `bot` suffix — the twin of `botUsernameSchema`. */
163
+ export const botUsernameCheck = refine(usernameCheck, (username) => username.toLowerCase().endsWith(BOT_USERNAME_SUFFIX), BOT_USERNAME_INVALID_MESSAGE);
164
+ /** The twin of `usernameSchemaForAccountKind`. */
165
+ export function usernameCheckForAccountKind(kind) {
166
+ return kind === 'bot' ? botUsernameCheck : usernameCheck;
167
+ }
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ export { SESSION_ACCOUNTS_CHANGED_EVENT } from './client/deviceSession.js';
2
3
  export const sessionAccountSchema = z.object({
3
4
  accountId: z.string(),
4
5
  sessionId: z.string(),
@@ -58,21 +59,6 @@ export const deviceTokenMintResponseSchema = z.object({
58
59
  nextDeviceSecret: z.string(),
59
60
  state: deviceSessionStateSchema,
60
61
  });
61
- /* -------------------------------------------------------------------------- */
62
- /* Instant cross-app session sync (token-free socket signal) */
63
- /* -------------------------------------------------------------------------- */
64
- /**
65
- * Name of the token-free Socket.IO event emitted to room `user:<userId>` on
66
- * every DeviceSession mutation that changes what is signed in for that user.
67
- *
68
- * This is a pure SIGNAL — it carries NO access token, NO deviceSecret and NO
69
- * account bodies. A client that receives it re-fetches its authenticated
70
- * session/account state (`GET /session/device/state`, `GET /accounts`). Unlike
71
- * `session_state` (scoped to `device:<deviceId>`, i.e. a single origin), this
72
- * reaches ALL of a user's connected sockets across their devices/origins so
73
- * every Oxy app reflects an add / switch / signout instantly.
74
- */
75
- export const SESSION_ACCOUNTS_CHANGED_EVENT = 'session_accounts_changed';
76
62
  /**
77
63
  * Why the signed-in set changed:
78
64
  * - `login` — a brand-new session was minted for the user (QR / cross-app authorize)
@@ -26,37 +26,14 @@
26
26
  import { z } from 'zod';
27
27
  import { identityProofSchema } from './identityProof.js';
28
28
  import { emailReauthProofSchema } from './signIn.js';
29
- export const IDENTITY_LINK_STATUSES = ['pending', 'signed', 'completed', 'cancelled'];
30
- /** The scheme and host Commons routes a link QR to. */
31
- export const IDENTITY_LINK_QR_PREFIX = 'oxycommons://link';
32
- const LINK_ID = /^[0-9a-f]{32}$/;
33
- const CHALLENGE = /^[0-9a-f]{64}$/;
29
+ // The Zod-free half of this contract lives in `./client/` (`@oxy.so/contracts/client`);
30
+ // it is re-exported here so the root entry is unchanged.
31
+ import { IDENTITY_LINK_STATUSES, IDENTITY_LINK_QR_PREFIX, LINK_ID, CHALLENGE, } from './client/identityLink.js';
32
+ export { IDENTITY_LINK_STATUSES, IDENTITY_LINK_QR_PREFIX, buildIdentityLinkQrPayload, parseIdentityLinkQrPayload, } from './client/identityLink.js';
34
33
  export const identityLinkIdSchema = z
35
34
  .string()
36
35
  .trim()
37
36
  .regex(LINK_ID, 'linkId must be 32 lowercase hex characters');
38
- /** The QR auth.oxy.so shows: the request's id and the challenge Commons signs. */
39
- export function buildIdentityLinkQrPayload(linkId, challenge) {
40
- return `${IDENTITY_LINK_QR_PREFIX}?id=${linkId}&c=${challenge}`;
41
- }
42
- /** The request a scanned code names, or `null` for anything that is not a link QR. */
43
- export function parseIdentityLinkQrPayload(raw) {
44
- const value = raw.trim();
45
- if (!value.startsWith(`${IDENTITY_LINK_QR_PREFIX}?`))
46
- return null;
47
- // Parsed by hand: React Native's `URLSearchParams` does not implement `get`.
48
- const params = new Map();
49
- for (const pair of value.slice(IDENTITY_LINK_QR_PREFIX.length + 1).split('&')) {
50
- const separator = pair.indexOf('=');
51
- if (separator > 0)
52
- params.set(pair.slice(0, separator), pair.slice(separator + 1));
53
- }
54
- const linkId = params.get('id') ?? '';
55
- const challenge = params.get('c') ?? '';
56
- if (!LINK_ID.test(linkId) || !CHALLENGE.test(challenge))
57
- return null;
58
- return { linkId, challenge };
59
- }
60
37
  export const identityLinkCreateResponseSchema = z.object({
61
38
  linkId: identityLinkIdSchema,
62
39
  challenge: z.string().regex(CHALLENGE),
@@ -16,94 +16,10 @@
16
16
  * primitive and passes the hex digest in).
17
17
  */
18
18
  import { z } from 'zod';
19
- export const IDENTITY_PROOF_VERSION = 2;
20
- export const IDENTITY_PROOF_DOMAIN = 'oxy-identity-proof';
21
- /** The audience every API-verified identity proof names. */
22
- export const IDENTITY_PROOF_AUDIENCE = 'oxy-api/identity';
23
- /** How long a proof challenge lives: one interactive ceremony. */
24
- export const IDENTITY_PROOF_CHALLENGE_TTL_MS = 5 * 60 * 1000;
25
- /**
26
- * Everything a root proof may authorize. A challenge is minted for exactly one
27
- * action and spent only by a proof for that action.
28
- */
29
- export const IDENTITY_PROOF_ACTIONS = {
30
- /**
31
- * Link Commons' root to an account that has none (`POST /auth/link`).
32
- * The account becomes self-custodied and its email is deleted
33
- * (ADR 0029 D3).
34
- */
35
- link: 'link_identity',
36
- };
37
- export const IDENTITY_PROOF_ACTION_VALUES = Object.values(IDENTITY_PROOF_ACTIONS);
38
- const HEX_DIGEST = /^[0-9a-f]{64}$/;
39
- const ROOT_KEY = /^04[0-9a-f]{128}$/;
40
- const CHALLENGE = /^[0-9a-f]{64}$/;
41
- /**
42
- * Canonical JSON: object keys sorted by UTF-16 code unit, no whitespace,
43
- * `undefined` members omitted, arrays in order. Numbers must be finite. This is
44
- * the ONLY serializer for anything digested into a proof.
45
- */
46
- export function canonicalJson(value) {
47
- if (value === null)
48
- return 'null';
49
- switch (typeof value) {
50
- case 'string':
51
- case 'boolean':
52
- return JSON.stringify(value);
53
- case 'number':
54
- if (!Number.isFinite(value))
55
- throw new Error('canonicalJson: non-finite number');
56
- return JSON.stringify(value);
57
- case 'object': {
58
- if (Array.isArray(value)) {
59
- return `[${value.map((entry) => (entry === undefined ? 'null' : canonicalJson(entry))).join(',')}]`;
60
- }
61
- const record = value;
62
- const keys = Object.keys(record)
63
- .filter((key) => record[key] !== undefined)
64
- .sort();
65
- return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
66
- }
67
- default:
68
- throw new Error(`canonicalJson: unsupported ${typeof value}`);
69
- }
70
- }
71
- /**
72
- * The exact bytes a root signs. Throws on a malformed claim rather than signing
73
- * (or verifying) something ambiguous.
74
- */
75
- export function buildIdentityProofMessage(claims) {
76
- if (!IDENTITY_PROOF_ACTION_VALUES.includes(claims.action))
77
- throw new Error('identity proof: unknown action');
78
- if (!claims.subject || !claims.actor)
79
- throw new Error('identity proof: subject and actor are required');
80
- if (!ROOT_KEY.test(claims.rootPublicKey))
81
- throw new Error('identity proof: rootPublicKey must be canonical');
82
- if (claims.payloadDigest !== null && !HEX_DIGEST.test(claims.payloadDigest)) {
83
- throw new Error('identity proof: payloadDigest must be a lowercase SHA-256 hex digest');
84
- }
85
- if (claims.expectedRevision !== null &&
86
- (!Number.isSafeInteger(claims.expectedRevision) || claims.expectedRevision < 0)) {
87
- throw new Error('identity proof: expectedRevision must be a non-negative integer');
88
- }
89
- if (!CHALLENGE.test(claims.challenge))
90
- throw new Error('identity proof: challenge must be 64 lowercase hex characters');
91
- if (!Number.isSafeInteger(claims.expiresAt) || claims.expiresAt <= 0)
92
- throw new Error('identity proof: expiresAt must be unix milliseconds');
93
- return canonicalJson({
94
- v: IDENTITY_PROOF_VERSION,
95
- domain: IDENTITY_PROOF_DOMAIN,
96
- action: claims.action,
97
- subject: claims.subject,
98
- actor: claims.actor,
99
- rootPublicKey: claims.rootPublicKey,
100
- payloadDigest: claims.payloadDigest,
101
- expectedRevision: claims.expectedRevision,
102
- audience: claims.audience,
103
- challenge: claims.challenge,
104
- expiresAt: claims.expiresAt,
105
- });
106
- }
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
+ import { IDENTITY_PROOF_VERSION, IDENTITY_PROOF_ACTION_VALUES, CHALLENGE, } from './client/identityProof.js';
22
+ export { IDENTITY_PROOF_VERSION, IDENTITY_PROOF_DOMAIN, IDENTITY_PROOF_AUDIENCE, IDENTITY_PROOF_CHALLENGE_TTL_MS, IDENTITY_PROOF_ACTIONS, IDENTITY_PROOF_ACTION_VALUES, canonicalJson, buildIdentityProofMessage, IDENTITY_ERROR_CODES, } from './client/identityProof.js';
107
23
  /** The proof as it travels: the signature plus the two claims the verifier cannot derive. */
108
24
  export const identityProofSchema = z.object({
109
25
  v: z.literal(IDENTITY_PROOF_VERSION),
@@ -120,17 +36,6 @@ export const identityProofChallengeResponseSchema = z.object({
120
36
  expiresAt: z.number().int().positive(),
121
37
  audience: z.string().min(1),
122
38
  });
123
- /**
124
- * Stable error codes the root routes answer with (`error.code` in the API error
125
- * body). Clients map these through their localization, never the English message.
126
- */
127
- export const IDENTITY_ERROR_CODES = {
128
- proofInvalid: 'IDENTITY_PROOF_INVALID',
129
- rootAlreadyLinked: 'IDENTITY_ROOT_ALREADY_LINKED',
130
- rootLinkedElsewhere: 'IDENTITY_ROOT_LINKED_ELSEWHERE',
131
- freshFactorRequired: 'IDENTITY_FRESH_FACTOR_REQUIRED',
132
- notPersonal: 'IDENTITY_NOT_PERSONAL_ACCOUNT',
133
- };
134
39
  export const identityRootStatusSchema = z.object({
135
40
  rootLinked: z.boolean(),
136
41
  recoveryEmail: z.string().nullable(),
package/dist/esm/index.js CHANGED
@@ -180,13 +180,14 @@ PRODUCT_PLAN_STATUSES, productPlanStatusSchema, LIVE_PRODUCT_PLAN_STATUSES, plan
180
180
  export {
181
181
  // Economic treatment (I09): derived by the server, never sent by a caller.
182
182
  INFERENCE_ECONOMIC_TREATMENTS, inferenceEconomicTreatmentSchema, PROVIDER_COST_SOURCES, providerCostSourceSchema, costCenterUsageSchema, } from './inference/economics.js';
183
- export { AUTONOMY_LEVELS, CAPABILITY_PACKAGES, autonomyLevelSchema, capabilityPackageSchema, actorRefSchema, executionActorRefSchema, foregroundExecutionAuthorizationInputSchema, resourceRefSchema, toolGrantOverrideSchema, grantLimitSchema, capabilityCatalogBindingSchema, executionAuthorizationRefSchema, capabilityCoordinatorSchema, capabilityTicketRequestSchema, delegationGrantSchema, automationTriggerSchema, automationActorSelectionSchema, automationDataFlowSchema, automationDefinitionSchema, capabilityTicketClaimsSchema, invocationPrincipalSchema, policyDecisionSchema, auditResultSchema, auditEventSchema, catalogToolSchema, catalogEventSchema, appCapabilityCatalogSchema, catalogRegistrationSchema, normalizedAppEventSchema, } from './agency.js';
183
+ export { AUTONOMY_LEVELS, CAPABILITY_PACKAGES, autonomyLevelSchema, capabilityPackageSchema, actorRefSchema, executionActorRefSchema, foregroundExecutionAuthorizationInputSchema, resourceRefSchema, toolGrantOverrideSchema, grantLimitSchema, capabilityCatalogBindingSchema, executionAuthorizationRefSchema, capabilityCoordinatorSchema, capabilityTicketRequestSchema, delegationGrantSchema, AGENT_REQUESTER_CEILING_AUTONOMY, agentRequesterCeilingWriteSchema, agentRequesterCeilingSchema, automationTriggerSchema, automationActorSelectionSchema, automationDataFlowSchema, automationDefinitionSchema, capabilityTicketClaimsSchema, invocationPrincipalSchema, policyDecisionSchema, auditResultSchema, auditEventSchema, catalogToolSchema, catalogEventSchema, catalogFeatureSchema, CATALOG_INTERNAL_TRANSPORTS, appCapabilityCatalogSchema, catalogRegistrationSchema, normalizedAppEventSchema, } from './agency.js';
184
184
  export { catalogInvocationPathParameters, catalogInvocationsOverlap, catalogInvocationTemplatesEquivalent, findOverlappingCatalogInvocations, matchCatalogInvocation, resolveCatalogInvocation, } from './capabilityInvocation.js';
185
185
  export { emailContextAddressSchema, emailContextMailboxSchema, emailContextMessageSchema, emailAgentContextSchema, } from './emailAgentContext.js';
186
186
  export { inboxComposeRequestSchema, inboxDailyBriefRequestSchema, inboxDailyBriefResponseSchema, INBOX_DAILY_BRIEF_SECTIONS, inboxNaturalSearchRequestSchema, inboxMessageInferenceParamsSchema, inboxInferenceTextResponseSchema, inboxNaturalSearchResponseSchema, inboxSmartRepliesResponseSchema, inboxThreadSummaryResponseSchema, inboxInferenceStreamEventSchema, } from './inference/inbox.js';
187
187
  export * from './externalIdentity.js';
188
188
  export * from './linkedAccounts.js';
189
189
  export * from './federationInstanceFetch.js';
190
+ export * from './accountAccess.js';
190
191
  export * from './notifications.js';
191
192
  export * from './email/messages.js';
192
193
  export * from './stickers.js';
@@ -204,3 +205,5 @@ export { personalPlanDisplayPriceSchema, personalPlanCatalogueSchema, } from './
204
205
  export * from './products/checkout.js';
205
206
  export * from './organizations.js';
206
207
  export * from './safety.js';
208
+ export { serviceTokenScopesSchema, serviceTokenOptionsSchema, } from './serviceToken.js';
209
+ export * from './applicationRoles.js';
@@ -1,22 +1,8 @@
1
1
  import { z } from 'zod';
2
- /** Real-world identity classification, independent of account topics and permissions. */
3
- export const ORGANIZATION_TYPES = [
4
- 'business',
5
- 'nonprofit',
6
- 'public_body',
7
- 'community',
8
- 'other',
9
- ];
10
- export const ORGANIZATION_STATUSES = ['active', 'inactive', 'merged', 'suppressed'];
11
- export const ORGANIZATION_CLAIM_STATUSES = ['unclaimed', 'claimed'];
12
- export const ORGANIZATION_ALIAS_KINDS = [
13
- 'name_variant',
14
- 'former_name',
15
- 'localized_name',
16
- 'marketing_name',
17
- 'misspelling',
18
- ];
19
- export const ORGANIZATION_CLAIM_DECISIONS = ['pending', 'approved', 'rejected'];
2
+ // The Zod-free half of this contract lives in `./client/` (`@oxy.so/contracts/client`);
3
+ // it is re-exported here so the root entry is unchanged.
4
+ import { ORGANIZATION_TYPES, ORGANIZATION_STATUSES, ORGANIZATION_CLAIM_STATUSES, ORGANIZATION_ALIAS_KINDS, ORGANIZATION_CLAIM_DECISIONS, ORGANIZATION_NAME_MAX_LENGTH, ORGANIZATION_ERROR_CODES, } from './client/organizations.js';
5
+ export { ORGANIZATION_TYPES, ORGANIZATION_STATUSES, ORGANIZATION_CLAIM_STATUSES, ORGANIZATION_ALIAS_KINDS, ORGANIZATION_CLAIM_DECISIONS, ORGANIZATION_NAME_MAX_LENGTH, ORGANIZATION_ERROR_CODES, } from './client/organizations.js';
20
6
  const id = z.string().min(1).max(128);
21
7
  const optionalText = (max) => z.string().trim().min(1).max(max).nullable().optional();
22
8
  export const organizationAliasSchema = z
@@ -26,7 +12,6 @@ export const organizationAliasSchema = z
26
12
  language: z.string().min(2).max(35).optional(),
27
13
  })
28
14
  .strict();
29
- export const ORGANIZATION_NAME_MAX_LENGTH = 300;
30
15
  export const organizationNameSchema = z
31
16
  .string()
32
17
  .trim()
@@ -168,18 +153,6 @@ export const organizationMergeSchema = z
168
153
  evidenceReference: z.string().trim().min(1).max(1000),
169
154
  })
170
155
  .strict();
171
- export const ORGANIZATION_ERROR_CODES = [
172
- 'bad_request',
173
- 'unauthorized',
174
- 'forbidden',
175
- 'not_found',
176
- 'unknown_route',
177
- 'conflict',
178
- 'validation_failed',
179
- 'rate_limited',
180
- 'internal_error',
181
- 'service_unavailable',
182
- ];
183
156
  export const organizationErrorSchema = z
184
157
  .object({
185
158
  error: z