@oxyhq/contracts 0.31.0 → 0.33.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.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/accountGraph.js +26 -1
- package/dist/cjs/index.js +16 -9
- package/dist/cjs/username.js +168 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/accountGraph.js +26 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/username.js +163 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/accountGraph.d.ts +26 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/userResponse.d.ts +2 -2
- package/dist/types/username.d.ts +152 -0
- package/package.json +1 -1
|
@@ -190,6 +190,13 @@ export declare function kindAcceptsAccountCategories(kind: AccountKind | null |
|
|
|
190
190
|
export declare const createAccountRequestSchema: z.ZodObject<{
|
|
191
191
|
parentAccountId: z.ZodOptional<z.ZodString>;
|
|
192
192
|
kind: z.ZodEnum<["organization", "project", "bot", "channel"]>;
|
|
193
|
+
/**
|
|
194
|
+
* The SAME policy a person's handle is held to. `users.username` is one unique
|
|
195
|
+
* index, so a managed account may not reserve a name a person could not ask
|
|
196
|
+
* for — and this route's predecessor (`.min(1).max(100)` here, `^[\w.-]+$`
|
|
197
|
+
* with no ceiling in the service) is how a one-character or dotted or
|
|
198
|
+
* 100-character handle became reachable for bots alone.
|
|
199
|
+
*/
|
|
193
200
|
username: z.ZodString;
|
|
194
201
|
name: z.ZodOptional<z.ZodObject<{
|
|
195
202
|
first: z.ZodOptional<z.ZodString>;
|
|
@@ -207,6 +214,23 @@ export declare const createAccountRequestSchema: z.ZodObject<{
|
|
|
207
214
|
bio: z.ZodOptional<z.ZodString>;
|
|
208
215
|
avatar: z.ZodOptional<z.ZodString>;
|
|
209
216
|
description: z.ZodOptional<z.ZodString>;
|
|
217
|
+
/**
|
|
218
|
+
* A named color preset KEY (`"blue"`, `"mint"`, …), never a hex value.
|
|
219
|
+
*
|
|
220
|
+
* Here at CREATION for the reason `isPrivateAccount` is, in miniature: for a
|
|
221
|
+
* managed account the color is a visual identity, and an account that is
|
|
222
|
+
* discoverable without one and acquires it on a second request is a face that
|
|
223
|
+
* changes by itself. One statement, one row, born looking like what its owner
|
|
224
|
+
* chose.
|
|
225
|
+
*
|
|
226
|
+
* The VALUE is checked in the API rather than here. The vocabulary is
|
|
227
|
+
* `USER_COLOR_PRESETS`, which is declared next to the `users_color_check` CHECK
|
|
228
|
+
* that is rendered from it — pinning the list a second time in this package
|
|
229
|
+
* would be a second source of truth for what the database accepts, and the two
|
|
230
|
+
* would drift apart silently. What this shape does is keep an over-long or
|
|
231
|
+
* non-string value from reaching the service at all.
|
|
232
|
+
*/
|
|
233
|
+
color: z.ZodOptional<z.ZodString>;
|
|
210
234
|
/** Ordered, PRIMARY FIRST — see rule 2 above {@link ACCOUNT_CATEGORY_IDS}. */
|
|
211
235
|
accountCategories: z.ZodOptional<z.ZodEffects<z.ZodArray<z.ZodEnum<["news", "politics", "business", "startup", "finance", "crypto", "marketplace", "retail", "real_estate", "agency", "landlord", "cooperative", "architecture", "technology", "software", "ai", "security", "automation", "science", "education", "books", "health", "fitness", "sports", "gaming", "music", "film", "podcast", "art", "photography", "comedy", "food", "travel", "fashion", "home_garden", "diy", "automotive", "animals", "family", "nonprofit", "government", "community", "activism", "environment", "religion", "other"]>, "many">, ("news" | "politics" | "business" | "startup" | "finance" | "crypto" | "marketplace" | "retail" | "real_estate" | "agency" | "landlord" | "cooperative" | "architecture" | "technology" | "software" | "ai" | "security" | "automation" | "science" | "education" | "books" | "health" | "fitness" | "sports" | "gaming" | "music" | "film" | "podcast" | "art" | "photography" | "comedy" | "food" | "travel" | "fashion" | "home_garden" | "diy" | "automotive" | "animals" | "family" | "nonprofit" | "government" | "community" | "activism" | "environment" | "religion" | "other")[], ("news" | "politics" | "business" | "startup" | "finance" | "crypto" | "marketplace" | "retail" | "real_estate" | "agency" | "landlord" | "cooperative" | "architecture" | "technology" | "software" | "ai" | "security" | "automation" | "science" | "education" | "books" | "health" | "fitness" | "sports" | "gaming" | "music" | "film" | "podcast" | "art" | "photography" | "comedy" | "food" | "travel" | "fashion" | "home_garden" | "diy" | "automotive" | "animals" | "family" | "nonprofit" | "government" | "community" | "activism" | "environment" | "religion" | "other")[]>>;
|
|
212
236
|
/**
|
|
@@ -262,6 +286,7 @@ export declare const createAccountRequestSchema: z.ZodObject<{
|
|
|
262
286
|
bio?: string | undefined;
|
|
263
287
|
avatar?: string | undefined;
|
|
264
288
|
description?: string | undefined;
|
|
289
|
+
color?: string | undefined;
|
|
265
290
|
accountCategories?: ("news" | "politics" | "business" | "startup" | "finance" | "crypto" | "marketplace" | "retail" | "real_estate" | "agency" | "landlord" | "cooperative" | "architecture" | "technology" | "software" | "ai" | "security" | "automation" | "science" | "education" | "books" | "health" | "fitness" | "sports" | "gaming" | "music" | "film" | "podcast" | "art" | "photography" | "comedy" | "food" | "travel" | "fashion" | "home_garden" | "diy" | "automotive" | "animals" | "family" | "nonprofit" | "government" | "community" | "activism" | "environment" | "religion" | "other")[] | undefined;
|
|
266
291
|
isPrivateAccount?: boolean | undefined;
|
|
267
292
|
}, {
|
|
@@ -276,6 +301,7 @@ export declare const createAccountRequestSchema: z.ZodObject<{
|
|
|
276
301
|
bio?: string | undefined;
|
|
277
302
|
avatar?: string | undefined;
|
|
278
303
|
description?: string | undefined;
|
|
304
|
+
color?: string | undefined;
|
|
279
305
|
accountCategories?: ("news" | "politics" | "business" | "startup" | "finance" | "crypto" | "marketplace" | "retail" | "real_estate" | "agency" | "landlord" | "cooperative" | "architecture" | "technology" | "software" | "ai" | "security" | "automation" | "science" | "education" | "books" | "health" | "fitness" | "sports" | "gaming" | "music" | "film" | "podcast" | "art" | "photography" | "comedy" | "food" | "travel" | "fashion" | "home_garden" | "diy" | "automotive" | "animals" | "family" | "nonprofit" | "government" | "community" | "activism" | "environment" | "religion" | "other")[] | undefined;
|
|
280
306
|
isPrivateAccount?: boolean | undefined;
|
|
281
307
|
}>;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export { ACCOUNT_KINDS, accountKindSchema, CHILD_ACCOUNT_KINDS, childAccountKindSchema, isAccountKind, isActAsEligibleKind, ACCOUNT_CATEGORY_IDS, ACCOUNT_CATEGORY_KINDS, accountCategoriesSchema, accountCategoryIdSchema, isSelectableAccountCategoryId, kindAcceptsAccountCategories, MAX_ACCOUNT_CATEGORIES, newlyAddedRetiredCategories, RETIRED_ACCOUNT_CATEGORY_IDS, SELECTABLE_ACCOUNT_CATEGORY_IDS, createAccountRequestSchema, } from './accountGraph';
|
|
13
13
|
export type { AccountKind, AccountCategoryId, AccountCategoryKind, ChildAccountKind, CreateAccountRequest, } from './accountGraph';
|
|
14
|
+
export { usernameSchema, isValidUsername, stripDisallowedUsernameCharacters, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH, USERNAME_INVALID_MESSAGE, } from './username';
|
|
14
15
|
export { userNameSchema, userRelationshipSchema, themePreferenceSchema, userResponseSchema, userProfileUpdateSchema, currentUserResponseSchema, deviceLinkedSessionSchema, deviceLinkedSessionsResponseSchema, resolveUserId, safeParseContract, } from './userResponse';
|
|
15
16
|
export type { UserNameResponse, UserRelationship, ThemePreference, UserResponse, UserProfileUpdate, CurrentUserResponseContract, DeviceLinkedSessionResponse, DeviceLinkedSessionsResponseContract, } from './userResponse';
|
|
16
17
|
export { applicationTypeSchema, publicApplicationSchema, sessionStatusSchema, } from './sessionStatus';
|
|
@@ -808,6 +808,7 @@ export declare const currentUserResponseSchema: z.ZodObject<{
|
|
|
808
808
|
kind?: "personal" | "organization" | "project" | "bot" | "channel" | undefined;
|
|
809
809
|
username?: string | undefined;
|
|
810
810
|
avatar?: string | null | undefined;
|
|
811
|
+
color?: string | null | undefined;
|
|
811
812
|
accountCategories?: ("news" | "politics" | "business" | "startup" | "finance" | "crypto" | "marketplace" | "retail" | "real_estate" | "agency" | "landlord" | "cooperative" | "architecture" | "technology" | "software" | "ai" | "security" | "automation" | "science" | "education" | "books" | "health" | "fitness" | "sports" | "gaming" | "music" | "film" | "podcast" | "art" | "photography" | "comedy" | "food" | "travel" | "fashion" | "home_garden" | "diy" | "automotive" | "animals" | "family" | "nonprofit" | "government" | "community" | "activism" | "environment" | "religion" | "other")[] | undefined;
|
|
812
813
|
id?: string | undefined;
|
|
813
814
|
publicKey?: string | undefined;
|
|
@@ -818,7 +819,6 @@ export declare const currentUserResponseSchema: z.ZodObject<{
|
|
|
818
819
|
phone?: string | undefined;
|
|
819
820
|
address?: string | undefined;
|
|
820
821
|
birthday?: string | undefined;
|
|
821
|
-
color?: string | null | undefined;
|
|
822
822
|
verified?: boolean | undefined;
|
|
823
823
|
languages?: string[] | undefined;
|
|
824
824
|
relationship?: UserRelationship | undefined;
|
|
@@ -832,6 +832,7 @@ export declare const currentUserResponseSchema: z.ZodObject<{
|
|
|
832
832
|
kind?: "personal" | "organization" | "project" | "bot" | "channel" | undefined;
|
|
833
833
|
username?: string | undefined;
|
|
834
834
|
avatar?: string | null | undefined;
|
|
835
|
+
color?: string | null | undefined;
|
|
835
836
|
accountCategories?: ("news" | "politics" | "business" | "startup" | "finance" | "crypto" | "marketplace" | "retail" | "real_estate" | "agency" | "landlord" | "cooperative" | "architecture" | "technology" | "software" | "ai" | "security" | "automation" | "science" | "education" | "books" | "health" | "fitness" | "sports" | "gaming" | "music" | "film" | "podcast" | "art" | "photography" | "comedy" | "food" | "travel" | "fashion" | "home_garden" | "diy" | "automotive" | "animals" | "family" | "nonprofit" | "government" | "community" | "activism" | "environment" | "religion" | "other")[] | undefined;
|
|
836
837
|
id?: string | undefined;
|
|
837
838
|
publicKey?: string | undefined;
|
|
@@ -842,7 +843,6 @@ export declare const currentUserResponseSchema: z.ZodObject<{
|
|
|
842
843
|
phone?: string | undefined;
|
|
843
844
|
address?: string | undefined;
|
|
844
845
|
birthday?: string | undefined;
|
|
845
|
-
color?: string | null | undefined;
|
|
846
846
|
verified?: boolean | undefined;
|
|
847
847
|
languages?: string[] | undefined;
|
|
848
848
|
relationship?: UserRelationship | undefined;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Username policy — the ONE rule, for every kind of account.
|
|
3
|
+
*
|
|
4
|
+
* A username is a HANDLE: the routing key of a profile URL (`/@alice`), the
|
|
5
|
+
* local part of a webfinger `acct:`, and a login identifier. `users.username`
|
|
6
|
+
* carries a single unique index, `lower(btrim(username))`, and people, bots,
|
|
7
|
+
* organizations, projects and channels all draw from it. There is no per-kind
|
|
8
|
+
* namespace, so there is no per-kind rule — a bot that may reserve a name a
|
|
9
|
+
* person cannot ask for is a disagreement inside one index, not a variant.
|
|
10
|
+
*
|
|
11
|
+
* ## Why this file exists
|
|
12
|
+
*
|
|
13
|
+
* Seven rules governed this one namespace: four validators (this package's
|
|
14
|
+
* predecessor in `@oxyhq/api`, `@oxyhq/core`, `@oxyhq/commons`, and one written
|
|
15
|
+
* inline in `AccountService.resolveUniqueUsername`) and three that COERCED —
|
|
16
|
+
* silently deleting the characters they disliked, which hands somebody an
|
|
17
|
+
* account under a name they never chose. They lived in five packages and no test
|
|
18
|
+
* asserted they agreed. `contracts` is where the single declaration can actually
|
|
19
|
+
* live: `api`, `core`, `commons`, `services` and `auth` all already depend on it,
|
|
20
|
+
* so every write path can IMPORT the rule instead of restating it.
|
|
21
|
+
* `__tests__/usernamePolicySingleSource.test.ts` fails if a second one appears.
|
|
22
|
+
*
|
|
23
|
+
* ## The rule, and why each part of it
|
|
24
|
+
*
|
|
25
|
+
* ```
|
|
26
|
+
* 3–30 characters
|
|
27
|
+
* first and last character: [A-Za-z0-9]
|
|
28
|
+
* interior: [A-Za-z0-9_-]
|
|
29
|
+
* never two separators in a row
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* - **Hyphens are admitted because the DATABASE already admits them.**
|
|
33
|
+
* `internal_cost_centers_slug_check` is a CHECK constraint —
|
|
34
|
+
* `^[a-z0-9][a-z0-9-]{0,62}$` — and `seed-internal-cost-centers` mints a
|
|
35
|
+
* `project` account whose username IS the slug; four of the five declared
|
|
36
|
+
* centres contain a hyphen. An alphanumeric-only handle rule would contradict
|
|
37
|
+
* a constraint written to permit them, and would make those centres
|
|
38
|
+
* unmintable. This is the argument, not the four hyphenated accounts that
|
|
39
|
+
* happen to exist — they are a symptom.
|
|
40
|
+
* - **Dots are NOT admitted.** A dot is the delimiter that separates handle from
|
|
41
|
+
* domain in the federated form this same column stores for remote actors
|
|
42
|
+
* (`alice@mastodon.social`), it collides with extension-style routing
|
|
43
|
+
* (`/@alice.json`), and `n.ate` beside `nate` is the strongest confusable pair
|
|
44
|
+
* an ASCII handle can produce. Only the inline account rule ever accepted one,
|
|
45
|
+
* and it accepted it by accident: its `[\w.-]` was written for a SLUG.
|
|
46
|
+
* - **A length bound, always.** The account path had none — the only ceiling was
|
|
47
|
+
* a `.max(100)` on the wire schema. 3 is the floor because `oxy`, the platform
|
|
48
|
+
* owner's own organization, is three characters. 30 is the ceiling four of the
|
|
49
|
+
* seven rules and the availability endpoint already published.
|
|
50
|
+
* - **First and last character alphanumeric, and no `--` / `__` / `-_` run.**
|
|
51
|
+
* Both are free — no account uses such a name — and they remove the
|
|
52
|
+
* confusable shapes that admitting two separators would otherwise introduce.
|
|
53
|
+
*
|
|
54
|
+
* ## Case, and what this schema deliberately does not do
|
|
55
|
+
*
|
|
56
|
+
* **Case is PRESERVED.** Uniqueness is decided by the database's
|
|
57
|
+
* `lower(btrim(username))` index, so `Alice` and `alice` cannot coexist, but a
|
|
58
|
+
* name that was typed with a capital keeps it. This schema therefore never
|
|
59
|
+
* lower-cases: rewriting a caller's input is how `resolveUniqueUsername` used to
|
|
60
|
+
* return `mybot` to somebody who asked for `MyBot`.
|
|
61
|
+
*
|
|
62
|
+
* **This is a WRITE-path rule.** It states what may be newly stored, not what may
|
|
63
|
+
* be read. Rows that predate it — including 11 with no username at all — must go
|
|
64
|
+
* on loading, resolving and rendering; validating on a read turns an existing
|
|
65
|
+
* account into a 500.
|
|
66
|
+
*
|
|
67
|
+
* **It does not govern remote actors.** The same column holds ~73k federated
|
|
68
|
+
* rows in `handle@domain` form, written by `POST /users/resolve` through its own
|
|
69
|
+
* normalizer. Those are another server's namespace; this rule would reject every
|
|
70
|
+
* one of them and must never be pointed at that path.
|
|
71
|
+
*
|
|
72
|
+
* ## Usable by a handle GENERATOR, deliberately
|
|
73
|
+
*
|
|
74
|
+
* Slug generators are how the eighth copy of this rule appears. Alia's
|
|
75
|
+
* `suggestAgentUsername` builds one from an agent's name and re-derives a subset
|
|
76
|
+
* of these rules by hand — its own docblock admits it ("A leading digit or an
|
|
77
|
+
* empty slug both fail Oxy's username rules") — and, having no minimum, proposes
|
|
78
|
+
* `al` for an agent called "Al", which the server then refuses.
|
|
79
|
+
*
|
|
80
|
+
* So this module answers a generator's three questions without dragging a server
|
|
81
|
+
* dependency along. It is zod and nothing else, so it imports cleanly into a
|
|
82
|
+
* React Native bundle or another repo's backend:
|
|
83
|
+
*
|
|
84
|
+
* - *Does this candidate pass?* {@link isValidUsername}, or `safeParse` when the
|
|
85
|
+
* reason matters.
|
|
86
|
+
* - *How short is too short, how long is too long?* {@link USERNAME_MIN_LENGTH}
|
|
87
|
+
* and {@link USERNAME_MAX_LENGTH}, so a generator can pad or truncate instead
|
|
88
|
+
* of guessing and being 400ed.
|
|
89
|
+
* - *Which characters survive?* {@link stripDisallowedUsernameCharacters}.
|
|
90
|
+
*
|
|
91
|
+
* A generator PROPOSES; only `POST /accounts` decides, and a taken handle comes
|
|
92
|
+
* back as a 409 for the client to retry with a fresh suggestion. Nothing here
|
|
93
|
+
* knows what is taken, and it must not pretend to.
|
|
94
|
+
*/
|
|
95
|
+
import { z } from 'zod';
|
|
96
|
+
/** Shortest storable handle. `oxy` sets the floor. */
|
|
97
|
+
export declare const USERNAME_MIN_LENGTH = 3;
|
|
98
|
+
/** Longest storable handle, and the `maxLength` an input field should carry. */
|
|
99
|
+
export declare const USERNAME_MAX_LENGTH = 30;
|
|
100
|
+
/** The 400 / inline-validation copy for every path that rejects a handle. */
|
|
101
|
+
export declare const USERNAME_INVALID_MESSAGE: string;
|
|
102
|
+
/**
|
|
103
|
+
* Alphanumeric runs joined by single separators, as a SOURCE string.
|
|
104
|
+
*
|
|
105
|
+
* A string rather than a literal because the OpenAPI docblocks that publish this
|
|
106
|
+
* rule (`POST /auth/register`, `PUT /users/:userId`) must quote it verbatim, and
|
|
107
|
+
* `usernamePolicySingleSource.test.ts` compares them against THIS constant. A
|
|
108
|
+
* published `pattern:` that drifts from the enforced rule is a lie told to every
|
|
109
|
+
* client that generates from the spec, and it is exactly the kind of copy nobody
|
|
110
|
+
* notices going stale.
|
|
111
|
+
*
|
|
112
|
+
* Deliberately NOT re-exported from the package barrel: it exists for the
|
|
113
|
+
* schema below and for that one gate. Anything validating a username uses
|
|
114
|
+
* {@link usernameSchema}, so there is no second way to ask the question.
|
|
115
|
+
*
|
|
116
|
+
* Written as an unambiguous alternation rather than a lookahead: every character
|
|
117
|
+
* belongs to exactly one branch, so matching is linear and there is no
|
|
118
|
+
* backtracking to bound. It also carries no `\p{…}` property escape, which
|
|
119
|
+
* mobile Hermes throws on at runtime — this module is reachable from every React
|
|
120
|
+
* Native consumer.
|
|
121
|
+
*/
|
|
122
|
+
export declare const USERNAME_PATTERN_SOURCE = "^[A-Za-z0-9]+(?:[-_][A-Za-z0-9]+)*$";
|
|
123
|
+
/**
|
|
124
|
+
* The one username policy, as a schema.
|
|
125
|
+
*
|
|
126
|
+
* `.trim()` first, so surrounding whitespace is a typo rather than a rejection —
|
|
127
|
+
* but interior whitespace is NOT removed. It falls to the pattern, because
|
|
128
|
+
* squashing `"al ice"` into `"alice"` would hand the user an account under a name
|
|
129
|
+
* they never chose. Every write path validates through THIS object; nothing
|
|
130
|
+
* re-implements it.
|
|
131
|
+
*/
|
|
132
|
+
export declare const usernameSchema: z.ZodString;
|
|
133
|
+
/**
|
|
134
|
+
* Whether a candidate handle is storable — the boolean form, for input surfaces
|
|
135
|
+
* that show a message as somebody types rather than throwing.
|
|
136
|
+
*
|
|
137
|
+
* Answers from {@link usernameSchema}, so a client's inline check and the
|
|
138
|
+
* server's 400 cannot disagree.
|
|
139
|
+
*/
|
|
140
|
+
export declare function isValidUsername(candidate: string): boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Drop the characters the policy forbids, for an input field that filters
|
|
143
|
+
* keystrokes.
|
|
144
|
+
*
|
|
145
|
+
* This is a TYPING aid and nothing else — the result still has to pass
|
|
146
|
+
* {@link usernameSchema}, which is what decides. It does not lower-case (case is
|
|
147
|
+
* preserved, see the header) and it cannot repair a name: a value that is too
|
|
148
|
+
* short, edge-separated or doubly-separated comes back unchanged and fails
|
|
149
|
+
* validation with a message, which is the outcome the coercing rules this
|
|
150
|
+
* replaces used to hide.
|
|
151
|
+
*/
|
|
152
|
+
export declare function stripDisallowedUsernameCharacters(input: string): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/contracts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "OxyHQ API contracts — single source of truth for request/response Zod schemas and inferred types, shared by the backend and the client SDKs",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|