@oxyhq/core 21.1.0 → 22.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -95,7 +95,7 @@ export type { PaginationParams, FollowGraphParams, FollowGraphSort, ApiResponse,
95
95
  export { ErrorCodes, createApiError, handleHttpError, isHttpRequestError, parseHttpErrorBody, validateRequiredFields, } from './utils/errorUtils';
96
96
  export type { HttpRequestError, ParsedHttpErrorBody } from './utils/errorUtils';
97
97
  export { retryAsync } from './utils/asyncUtils';
98
- export { EMAIL_REGEX, USERNAME_REGEX, PASSWORD_REGEX, MAX_DISPLAY_NAME_LENGTH, DISPLAY_NAME_INVALID_MESSAGE, isValidEmail, isValidUsername, isValidPassword, isValidDisplayName, DISPLAY_NAME_ALLOWED_SCRIPTS, DISPLAY_NAME_DISALLOWED_SOURCE, DISPLAY_NAME_ORPHANED_MARK_SOURCE, DISPLAY_NAME_UNFLANKED_SEPARATOR_SOURCE, isRequiredString, isRequiredNumber, isRequiredBoolean, isValidArray, isValidObject, isValidUUID, isValidURL, isValidDate, isValidFileSize, isValidFileType, sanitizeString, sanitizeHTML, isValidObjectId, validateAndSanitizeUserInput, } from './utils/validationUtils';
98
+ export { EMAIL_REGEX, PASSWORD_REGEX, MAX_DISPLAY_NAME_LENGTH, DISPLAY_NAME_INVALID_MESSAGE, isValidEmail, isValidPassword, isValidDisplayName, DISPLAY_NAME_ALLOWED_SCRIPTS, DISPLAY_NAME_DISALLOWED_SOURCE, DISPLAY_NAME_ORPHANED_MARK_SOURCE, DISPLAY_NAME_UNFLANKED_SEPARATOR_SOURCE, isRequiredString, isRequiredNumber, isRequiredBoolean, isValidArray, isValidObject, isValidUUID, isValidURL, isValidDate, isValidFileSize, isValidFileType, sanitizeString, sanitizeHTML, isValidObjectId, validateAndSanitizeUserInput, } from './utils/validationUtils';
99
99
  export { normalizeInlineText, normalizeMultilineText, } from './utils/textNormalization';
100
100
  export { logger, createLogger, configureLogger, getLoggerConfig, resetLoggerConfig, consoleSink, isDev, } from './logger';
101
101
  export type { Logger, LogLevel, EmittableLogLevel, LogContext, LogEntry, LogSink, LoggerConfig, } from './logger';
@@ -180,6 +180,21 @@ export interface CreateAccountInput {
180
180
  };
181
181
  bio?: string;
182
182
  avatar?: string;
183
+ /**
184
+ * Named color preset KEY — `'blue'`, `'mint'`, … — never a hex value. The
185
+ * account graph's half of `User.color`, which every account DTO already
186
+ * carries; this is how one gets WRITTEN for an account you administer.
187
+ *
188
+ * Set it HERE rather than after the fact. For a managed account the colour is
189
+ * a visual identity, and an account that is discoverable without one and
190
+ * acquires it on a second request is a face that changes by itself.
191
+ *
192
+ * Omitted is not "no colour": the platform assigns a random preset, exactly as
193
+ * it did before this field existed. A reserved preset is refused unless the
194
+ * account has a claim to it — the administrator's own entitlements are not the
195
+ * ones weighed.
196
+ */
197
+ color?: string;
183
198
  /**
184
199
  * What the account is about. ORDERED — the FIRST element is the primary
185
200
  * category, so a picker must submit them in the order the user arranged them
@@ -224,6 +239,15 @@ export interface UpdateAccountInput {
224
239
  };
225
240
  bio?: string | null;
226
241
  avatar?: string | null;
242
+ /**
243
+ * Named color preset KEY, same vocabulary as `CreateAccountInput['color']`.
244
+ *
245
+ * NOT nullable, unlike `bio` and `avatar`: the column is `NOT NULL` with a
246
+ * default, so an account always HAS a colour and there is no "clear" to
247
+ * express. Sending the value the account already carries is always accepted,
248
+ * so a client may PATCH back the object it was served.
249
+ */
250
+ color?: string;
227
251
  /**
228
252
  * Replaces the WHOLE list, in the order given — there is no add/remove verb,
229
253
  * because a partial edit cannot express a re-ordering and the order is what
@@ -12,10 +12,6 @@ export declare const DISPLAY_NAME_INVALID_MESSAGE = "Name may only contain lette
12
12
  * Email validation regex
13
13
  */
14
14
  export declare const EMAIL_REGEX: RegExp;
15
- /**
16
- * Username validation regex (alphanumeric, underscores, and hyphens, 3-30 chars)
17
- */
18
- export declare const USERNAME_REGEX: RegExp;
19
15
  /**
20
16
  * Password validation regex (at least 8 chars, 1 uppercase, 1 lowercase, 1 number)
21
17
  */
@@ -24,10 +20,6 @@ export declare const PASSWORD_REGEX: RegExp;
24
20
  * Validate email format
25
21
  */
26
22
  export declare function isValidEmail(email: string): boolean;
27
- /**
28
- * Validate username format
29
- */
30
- export declare function isValidUsername(username: string): boolean;
31
23
  /**
32
24
  * Validate password strength
33
25
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "21.1.0",
3
+ "version": "22.0.0",
4
4
  "description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -116,7 +116,7 @@
116
116
  "dependencies": {
117
117
  "@noble/ciphers": "^1.3.0",
118
118
  "@noble/hashes": "^1.8.0",
119
- "@oxyhq/contracts": "^0.31.0",
119
+ "@oxyhq/contracts": "^0.33.0",
120
120
  "@oxyhq/protocol": "^0.2.0",
121
121
  "@scure/bip39": "^1.6.0",
122
122
  "@types/elliptic": "^6.4.18",
package/src/index.ts CHANGED
@@ -521,12 +521,10 @@ export { retryAsync } from './utils/asyncUtils';
521
521
  // ---------------------------------------------------------------------------
522
522
  export {
523
523
  EMAIL_REGEX,
524
- USERNAME_REGEX,
525
524
  PASSWORD_REGEX,
526
525
  MAX_DISPLAY_NAME_LENGTH,
527
526
  DISPLAY_NAME_INVALID_MESSAGE,
528
527
  isValidEmail,
529
- isValidUsername,
530
528
  isValidPassword,
531
529
  isValidDisplayName,
532
530
  DISPLAY_NAME_ALLOWED_SCRIPTS,
@@ -201,6 +201,21 @@ export interface CreateAccountInput {
201
201
  name?: { first?: string; last?: string; displayName?: string };
202
202
  bio?: string;
203
203
  avatar?: string;
204
+ /**
205
+ * Named color preset KEY — `'blue'`, `'mint'`, … — never a hex value. The
206
+ * account graph's half of `User.color`, which every account DTO already
207
+ * carries; this is how one gets WRITTEN for an account you administer.
208
+ *
209
+ * Set it HERE rather than after the fact. For a managed account the colour is
210
+ * a visual identity, and an account that is discoverable without one and
211
+ * acquires it on a second request is a face that changes by itself.
212
+ *
213
+ * Omitted is not "no colour": the platform assigns a random preset, exactly as
214
+ * it did before this field existed. A reserved preset is refused unless the
215
+ * account has a claim to it — the administrator's own entitlements are not the
216
+ * ones weighed.
217
+ */
218
+ color?: string;
204
219
  /**
205
220
  * What the account is about. ORDERED — the FIRST element is the primary
206
221
  * category, so a picker must submit them in the order the user arranged them
@@ -242,6 +257,15 @@ export interface UpdateAccountInput {
242
257
  name?: { first?: string; last?: string; displayName?: string };
243
258
  bio?: string | null;
244
259
  avatar?: string | null;
260
+ /**
261
+ * Named color preset KEY, same vocabulary as `CreateAccountInput['color']`.
262
+ *
263
+ * NOT nullable, unlike `bio` and `avatar`: the column is `NOT NULL` with a
264
+ * default, so an account always HAS a colour and there is no "clear" to
265
+ * express. Sending the value the account already carries is always accepted,
266
+ * so a client may PATCH back the object it was served.
267
+ */
268
+ color?: string;
245
269
  /**
246
270
  * Replaces the WHOLE list, in the order given — there is no add/remove verb,
247
271
  * because a partial edit cannot express a re-ordering and the order is what
@@ -255,13 +255,20 @@ describe('OxyServices.accounts', () => {
255
255
  it('posts the payload, unwraps `account`, and busts every list', async () => {
256
256
  makeRequestSpy.mockResolvedValue({ account: accountNodeFixture });
257
257
 
258
- const result = await oxy.createAccount({ kind: 'organization', username: 'oxy-org' });
258
+ const result = await oxy.createAccount({
259
+ kind: 'organization',
260
+ username: 'oxy-org',
261
+ color: 'purple',
262
+ });
259
263
 
260
264
  expect(result).toEqual(accountNodeFixture);
265
+ // `color` rides the CREATE body, not a follow-up patch: an account that is
266
+ // discoverable without its colour and acquires one on a second request is
267
+ // a face that changes by itself.
261
268
  expect(makeRequestSpy).toHaveBeenCalledWith(
262
269
  'POST',
263
270
  '/accounts',
264
- { kind: 'organization', username: 'oxy-org' },
271
+ { kind: 'organization', username: 'oxy-org', color: 'purple' },
265
272
  expect.objectContaining({ cache: false }),
266
273
  );
267
274
  expect(clearEntrySpy).toHaveBeenCalledWith('GET:/accounts');
@@ -273,13 +280,13 @@ describe('OxyServices.accounts', () => {
273
280
  it('patches, unwraps `account`, and busts the detail + lists', async () => {
274
281
  makeRequestSpy.mockResolvedValue({ account: accountNodeFixture });
275
282
 
276
- const result = await oxy.updateAccount('acc1', { bio: 'hello' });
283
+ const result = await oxy.updateAccount('acc1', { bio: 'hello', color: 'mint' });
277
284
 
278
285
  expect(result).toEqual(accountNodeFixture);
279
286
  expect(makeRequestSpy).toHaveBeenCalledWith(
280
287
  'PATCH',
281
288
  '/accounts/acc1',
282
- { bio: 'hello' },
289
+ { bio: 'hello', color: 'mint' },
283
290
  expect.objectContaining({ cache: false }),
284
291
  );
285
292
  expect(clearEntrySpy).toHaveBeenCalledWith('GET:/accounts/acc1');
@@ -7,7 +7,6 @@ import {
7
7
  isValidArray,
8
8
  isValidObject,
9
9
  isValidEmail,
10
- isValidUsername,
11
10
  isValidPassword,
12
11
  isValidDisplayName,
13
12
  DISPLAY_NAME_ALLOWED_SCRIPTS,
@@ -126,21 +125,14 @@ describe('Validation Utils', () => {
126
125
  });
127
126
  });
128
127
 
129
- describe('isValidUsername', () => {
130
- it('should return true for valid usernames', () => {
131
- expect(isValidUsername('user123')).toBe(true);
132
- expect(isValidUsername('test_user')).toBe(true);
133
- expect(isValidUsername('john-doe')).toBe(true);
134
- });
135
-
136
- it('should return false for invalid usernames', () => {
137
- expect(isValidUsername('')).toBe(false);
138
- expect(isValidUsername('a')).toBe(false); // too short
139
- expect(isValidUsername('ab')).toBe(false); // too short
140
- expect(isValidUsername('user@domain')).toBe(false); // invalid characters
141
- expect(isValidUsername('user with spaces')).toBe(false); // spaces
142
- });
143
- });
128
+ /**
129
+ * `isValidUsername` and `USERNAME_REGEX` were REMOVED from this module: they
130
+ * were a second username policy, looser than the one the server enforced, so
131
+ * the SDK could call a name valid and the API 400 it. The rule now lives once,
132
+ * in `@oxyhq/contracts`, and its own suite covers it. What survives here is the
133
+ * one thing this module still does with a username — sanitise-then-validate,
134
+ * asserted below to answer from that single policy.
135
+ */
144
136
 
145
137
  describe('isValidPassword', () => {
146
138
  it('should return true for valid passwords', () => {
@@ -676,6 +668,12 @@ describe('Validation Utils', () => {
676
668
  expect(validateAndSanitizeUserInput(' testuser ', 'username')).toBe('testuser');
677
669
  expect(validateAndSanitizeUserInput('ab', 'username')).toBeNull(); // too short
678
670
  expect(validateAndSanitizeUserInput(123, 'username')).toBeNull();
671
+ // Answers from the one policy in `@oxyhq/contracts`, not from a rule of its
672
+ // own: a dot and an edge separator are rejected here because they are
673
+ // rejected there.
674
+ expect(validateAndSanitizeUserInput('my-bot', 'username')).toBe('my-bot');
675
+ expect(validateAndSanitizeUserInput('my.bot', 'username')).toBeNull();
676
+ expect(validateAndSanitizeUserInput('-mybot', 'username')).toBeNull();
679
677
  });
680
678
 
681
679
  it('should validate and sanitize string input', () => {
@@ -9,6 +9,7 @@ import {
9
9
  DISPLAY_NAME_LETTERS_RANGES,
10
10
  DISPLAY_NAME_NAME_SEPARATORS_RANGES,
11
11
  } from './displayNamePolicyRanges.generated';
12
+ import { usernameSchema } from '@oxyhq/contracts';
12
13
 
13
14
  /**
14
15
  * Maximum stored length of a display name, in code units after cleaning.
@@ -25,11 +26,6 @@ export const DISPLAY_NAME_INVALID_MESSAGE =
25
26
  */
26
27
  export const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
27
28
 
28
- /**
29
- * Username validation regex (alphanumeric, underscores, and hyphens, 3-30 chars)
30
- */
31
- export const USERNAME_REGEX = /^[a-zA-Z0-9_-]{3,30}$/;
32
-
33
29
  /**
34
30
  * Password validation regex (at least 8 chars, 1 uppercase, 1 lowercase, 1 number)
35
31
  */
@@ -43,13 +39,6 @@ export function isValidEmail(email: string): boolean {
43
39
  return EMAIL_REGEX.test(email);
44
40
  }
45
41
 
46
- /**
47
- * Validate username format
48
- */
49
- export function isValidUsername(username: string): boolean {
50
- return USERNAME_REGEX.test(username);
51
- }
52
-
53
42
  /**
54
43
  * Validate password strength
55
44
  */
@@ -378,7 +367,10 @@ export function validateAndSanitizeUserInput(input: unknown, type: 'string' | 'e
378
367
  case 'email':
379
368
  return isValidEmail(sanitized) ? sanitized : null;
380
369
  case 'username':
381
- return isValidUsername(sanitized) ? sanitized : null;
370
+ // The ONE policy, from `@oxyhq/contracts`. This module used to declare a
371
+ // second one (`^[a-zA-Z0-9_-]{3,30}$`) that the server did not enforce, so
372
+ // the SDK could call a name valid and the API 400 it.
373
+ return usernameSchema.safeParse(sanitized).success ? sanitized : null;
382
374
  case 'string':
383
375
  return isRequiredString(sanitized) ? sanitized : null;
384
376
  default: