@oxyhq/core 17.1.0 → 19.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.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/boot/sessionColdBoot.js +4 -5
- package/dist/cjs/i18n/locales/en-US.json +48 -5
- package/dist/cjs/i18n/locales/es-ES.json +48 -5
- package/dist/cjs/i18n/locales/locales/en-US.json +48 -5
- package/dist/cjs/i18n/locales/locales/es-ES.json +48 -5
- package/dist/cjs/index.js +10 -6
- package/dist/cjs/mixins/OxyServices.accounts.js +27 -2
- package/dist/cjs/mixins/OxyServices.deviceBoot.js +3 -2
- package/dist/cjs/mixins/OxyServices.user.js +14 -20
- package/dist/cjs/server/index.js +8 -2
- package/dist/cjs/server/userInvalidation.js +6 -28
- package/dist/cjs/session/refresh.js +9 -14
- package/dist/cjs/utils/identityCacheSweep.js +97 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/boot/sessionColdBoot.js +4 -5
- package/dist/esm/i18n/locales/en-US.json +48 -5
- package/dist/esm/i18n/locales/es-ES.json +48 -5
- package/dist/esm/i18n/locales/locales/en-US.json +48 -5
- package/dist/esm/i18n/locales/locales/es-ES.json +48 -5
- package/dist/esm/index.js +1 -1
- package/dist/esm/mixins/OxyServices.accounts.js +22 -1
- package/dist/esm/mixins/OxyServices.deviceBoot.js +3 -2
- package/dist/esm/mixins/OxyServices.user.js +14 -20
- package/dist/esm/server/index.js +5 -1
- package/dist/esm/server/userInvalidation.js +5 -26
- package/dist/esm/session/refresh.js +9 -14
- package/dist/esm/utils/identityCacheSweep.js +92 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/mixins/OxyServices.accounts.d.ts +54 -11
- package/dist/types/mixins/OxyServices.deviceBoot.d.ts +3 -2
- package/dist/types/mixins/OxyServices.user.d.ts +9 -7
- package/dist/types/models/interfaces.d.ts +11 -3
- package/dist/types/server/index.d.ts +4 -2
- package/dist/types/server/userInvalidation.d.ts +5 -24
- package/dist/types/session/refresh.d.ts +13 -18
- package/dist/types/utils/identityCacheSweep.d.ts +80 -0
- package/package.json +2 -2
- package/src/boot/sessionColdBoot.ts +4 -5
- package/src/i18n/locales/en-US.json +48 -5
- package/src/i18n/locales/es-ES.json +48 -5
- package/src/index.ts +8 -3
- package/src/mixins/OxyServices.accounts.ts +73 -12
- package/src/mixins/OxyServices.deviceBoot.ts +3 -2
- package/src/mixins/OxyServices.user.ts +14 -20
- package/src/mixins/__tests__/identityWriteCacheInvalidation.test.ts +370 -0
- package/src/models/interfaces.ts +11 -3
- package/src/server/__tests__/userInvalidation.test.ts +3 -20
- package/src/server/index.ts +5 -2
- package/src/server/userInvalidation.ts +8 -36
- package/src/session/refresh.ts +15 -20
- package/src/utils/__tests__/identityCacheSweep.test.ts +151 -0
- package/src/utils/identityCacheSweep.ts +104 -0
package/dist/types/index.d.ts
CHANGED
|
@@ -39,8 +39,8 @@ export type { CanonicalUserHandleInput, UserHandleInput } from './utils/userHand
|
|
|
39
39
|
export { normalizeProfileLinks } from './utils/profileLinks';
|
|
40
40
|
export type { ProfileLink, ProfileLinkMetadata } from './utils/profileLinks';
|
|
41
41
|
export type { PublicApplication, ConnectedApp, } from './mixins/OxyServices.connectedApps';
|
|
42
|
-
export type { AccountKind,
|
|
43
|
-
export {
|
|
42
|
+
export type { AccountKind, AccountCategoryId, AccountRelationship, AccountRole, AccountMemberStatus, AccountMemberSource, AccountMember, AccountNode, AccountCredentialType, AccountCredentialEnvironment, AccountCredentialStatus, AccountCredential, AccountCredentialWithSecret, RotateAccountCredentialResult, ListAccountsOptions, CreateAccountInput, UpdateAccountInput, ProvisionChannelInput, ProvisionChannelMemberInput, ProvisionChannelResult, InviteAccountMemberInput, UpdateAccountMemberInput, TransferAccountOwnershipInput, CreateAccountCredentialInput, AccountSuccessResult, SwitchAccountResult, Application, ApplicationType, ApplicationStatus, ApplicationCredential, ApplicationCredentialType, ApplicationCredentialStatus, ApplicationEnvironment, CreateApplicationInput, UpdateApplicationInput, CreateApplicationCredentialInput, ApplicationCredentialWithSecret, RotateApplicationCredentialResult, ApplicationUsagePeriod, ApplicationUsageSummary, ApplicationUsageByDay, ApplicationUsageByEndpoint, ApplicationUsageStats, } from './mixins/OxyServices.accounts';
|
|
43
|
+
export { ACCOUNT_CATEGORY_IDS, MAX_ACCOUNT_CATEGORIES, SELECTABLE_ACCOUNT_CATEGORY_IDS, isSelectableAccountCategoryId, kindAcceptsAccountCategories, } from './mixins/OxyServices.accounts';
|
|
44
44
|
export { buildUserDid } from './mixins/OxyServices.identity';
|
|
45
45
|
export type { IdentityRecordType, UnlinkableAuthMethodType, LinkAuthMethodResult, PublishRecordResult, VerifyRecordResult, VerifyDomainResult, RemoveDomainResult, RotateKeyProof, RotateKeyOptions, RotateKeyResult, } from './mixins/OxyServices.identity';
|
|
46
46
|
export { parseIdPayload, parseAttestPayload, verifyPublicCardAttestation, } from './mixins/OxyServices.civic';
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* registers the switched session into the operator's device-set directly).
|
|
34
34
|
*/
|
|
35
35
|
import type { User } from '../models/interfaces';
|
|
36
|
-
import type { AccountKind,
|
|
36
|
+
import type { AccountCategoryId, AccountKind, ChildAccountKind } from '@oxyhq/contracts';
|
|
37
37
|
import type { SessionLoginResponse } from '../models/session';
|
|
38
38
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
39
39
|
/**
|
|
@@ -45,8 +45,8 @@ import type { OxyServicesBase } from '../OxyServices.base';
|
|
|
45
45
|
*
|
|
46
46
|
* Single source of truth is `@oxyhq/contracts`.
|
|
47
47
|
*/
|
|
48
|
-
export type {
|
|
49
|
-
export { ACCOUNT_KINDS,
|
|
48
|
+
export type { AccountCategoryId, AccountKind } from '@oxyhq/contracts';
|
|
49
|
+
export { ACCOUNT_CATEGORY_IDS, ACCOUNT_KINDS, MAX_ACCOUNT_CATEGORIES, SELECTABLE_ACCOUNT_CATEGORY_IDS, isActAsEligibleKind, isSelectableAccountCategoryId, kindAcceptsAccountCategories, } from '@oxyhq/contracts';
|
|
50
50
|
/**
|
|
51
51
|
* The calling user's relationship to an account node, as resolved by the API:
|
|
52
52
|
* - `self` — the caller's own personal (root) account.
|
|
@@ -131,12 +131,23 @@ export interface ListAccountsOptions {
|
|
|
131
131
|
*/
|
|
132
132
|
tree?: boolean;
|
|
133
133
|
}
|
|
134
|
-
/** Kinds a signed-in user may create via `POST /accounts`. Channels are service-provisioned only. */
|
|
135
|
-
export type UserCreatableAccountKind = 'organization' | 'project' | 'bot';
|
|
136
134
|
/** Input accepted by `createAccount`. */
|
|
137
135
|
export interface CreateAccountInput {
|
|
138
|
-
/**
|
|
139
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Classification of the new account. Every CHILD kind is creatable here with
|
|
138
|
+
* the caller's own bearer, `channel` included: a signed-in person has already
|
|
139
|
+
* proven who they are, and minting a child under their own tree is the same
|
|
140
|
+
* operation whichever kind it is.
|
|
141
|
+
*
|
|
142
|
+
* `channel` used to be excluded, on the reasoning that channels are
|
|
143
|
+
* service-provisioned only. What actually makes a channel safe does not depend
|
|
144
|
+
* on who creates it: `createChildAccount` writes no auth method, so it is born
|
|
145
|
+
* with no login, and `POST /accounts/:id/switch` refuses it via
|
|
146
|
+
* `isActAsEligibleKind`, so no session can ever have a channel as its subject
|
|
147
|
+
* and therefore no bearer exists that could add one. `personal` is excluded
|
|
148
|
+
* because it is a human login, minted at signup.
|
|
149
|
+
*/
|
|
150
|
+
kind: ChildAccountKind;
|
|
140
151
|
/**
|
|
141
152
|
* Parent account `_id` to nest the new account under. Omitted → the API roots
|
|
142
153
|
* it under the caller's personal account.
|
|
@@ -157,8 +168,17 @@ export interface CreateAccountInput {
|
|
|
157
168
|
};
|
|
158
169
|
bio?: string;
|
|
159
170
|
avatar?: string;
|
|
160
|
-
/**
|
|
161
|
-
|
|
171
|
+
/**
|
|
172
|
+
* What the account is about. ORDERED — the FIRST element is the primary
|
|
173
|
+
* category, so a picker must submit them in the order the user arranged them
|
|
174
|
+
* and must not sort. Stable ids, never labels: render each one through the
|
|
175
|
+
* `accounts.accountCategory.<id>` translation key.
|
|
176
|
+
*
|
|
177
|
+
* Offer `SELECTABLE_ACCOUNT_CATEGORY_IDS`, not `ACCOUNT_CATEGORY_IDS` — the
|
|
178
|
+
* latter still contains withdrawn ids so that accounts already carrying one
|
|
179
|
+
* keep working. At most `MAX_ACCOUNT_CATEGORIES`, no duplicates.
|
|
180
|
+
*/
|
|
181
|
+
accountCategories?: AccountCategoryId[];
|
|
162
182
|
}
|
|
163
183
|
/** Input accepted by `updateAccount`. Tree placement changes go through `/move`. */
|
|
164
184
|
export interface UpdateAccountInput {
|
|
@@ -176,8 +196,19 @@ export interface UpdateAccountInput {
|
|
|
176
196
|
};
|
|
177
197
|
bio?: string | null;
|
|
178
198
|
avatar?: string | null;
|
|
179
|
-
/**
|
|
180
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Replaces the WHOLE list, in the order given — there is no add/remove verb,
|
|
201
|
+
* because a partial edit cannot express a re-ordering and the order is what
|
|
202
|
+
* names the primary category. `[]` clears it.
|
|
203
|
+
*
|
|
204
|
+
* Not nullable, unlike `bio` and `avatar`: the empty case already has a
|
|
205
|
+
* spelling of its own, so a second one could only ever disagree with it.
|
|
206
|
+
*
|
|
207
|
+
* Rejected for a `personal` account, and rejected when it ADDS a withdrawn
|
|
208
|
+
* id the account did not already carry — keeping or re-ordering one it has is
|
|
209
|
+
* always allowed.
|
|
210
|
+
*/
|
|
211
|
+
accountCategories?: AccountCategoryId[];
|
|
181
212
|
}
|
|
182
213
|
/** Input accepted by `provisionChannelAccount` (service token + `accounts:provision`). */
|
|
183
214
|
export interface ProvisionChannelInput {
|
|
@@ -549,6 +580,18 @@ export declare function OxyServicesAccountsMixin<T extends typeof OxyServicesBas
|
|
|
549
580
|
/**
|
|
550
581
|
* Update an account's mutable profile fields. Tree placement changes
|
|
551
582
|
* (reparenting) go through the dedicated move endpoint, not here.
|
|
583
|
+
*
|
|
584
|
+
* An account IS a user, so this write changes identity — and a profile
|
|
585
|
+
* screen never reads `/accounts/<id>`. It reads `GET /users/<id>` and
|
|
586
|
+
* `GET /profiles/username/<handle>`, both cached for 5 minutes in the
|
|
587
|
+
* CALLER'S OWN process, so busting only the account-graph keys left every
|
|
588
|
+
* profile surface serving the pre-edit avatar and name for the full TTL
|
|
589
|
+
* with a perfectly healthy server (the cross-service `oxy:user:invalidate`
|
|
590
|
+
* signal does not help: it evicts BACKEND caches, and cannot reach a cache
|
|
591
|
+
* living in a browser tab). {@link evictOxyIdentityCache} owns that key
|
|
592
|
+
* list — see its docs for why the handle-keyed entries are prefix-swept
|
|
593
|
+
* (a RENAME leaves the old handle's entry unreachable by any targeted key).
|
|
594
|
+
*
|
|
552
595
|
* @param accountId - The account's Mongo `_id`.
|
|
553
596
|
* @param data - Subset of updatable profile fields.
|
|
554
597
|
*/
|
|
@@ -44,8 +44,9 @@ export declare function OxyServicesDeviceBootMixin<T extends typeof OxyServicesB
|
|
|
44
44
|
* Zero-cookie mint. Present the first-party `deviceId` + `deviceSecret` to
|
|
45
45
|
* `POST /session/device/token` — NO bearer, NO cookies: possession of the
|
|
46
46
|
* secret IS the device-ownership proof. Returns a fresh short access token
|
|
47
|
-
* for the device's active account plus `nextDeviceSecret` (
|
|
48
|
-
*
|
|
47
|
+
* for the device's active account plus `nextDeviceSecret` (on mint, the same
|
|
48
|
+
* proven secret echoed back — rotation happens on sign-in, not mint) and
|
|
49
|
+
* the projected device-session `state`.
|
|
49
50
|
*
|
|
50
51
|
* `skipAuth`: this call carries no bearer, so a 401 must surface DIRECTLY —
|
|
51
52
|
* never trigger `HttpService`'s 401→refresh→retry dance. The cold boot / re-
|
|
@@ -254,13 +254,15 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
|
|
|
254
254
|
/**
|
|
255
255
|
* Update user profile.
|
|
256
256
|
*
|
|
257
|
-
* Invalidates the SDK-side response cache for every endpoint that
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
257
|
+
* Invalidates the SDK-side response cache for every endpoint that can
|
|
258
|
+
* return this user — the list is owned by {@link evictOxyIdentityCache}, so
|
|
259
|
+
* a new identity read is added in one place instead of to each writer
|
|
260
|
+
* separately (this method's own hand-written copy had already drifted from
|
|
261
|
+
* the server-side one, missing `GET /auth/lookup/*` and
|
|
262
|
+
* `GET /profiles/resolve`). Without the sweep a follow-up
|
|
263
|
+
* `getUserBySession` inside the cache window returns the pre-update user —
|
|
264
|
+
* most visibly during onboarding, where the username step flickers back as
|
|
265
|
+
* if nothing was saved.
|
|
264
266
|
*
|
|
265
267
|
* TanStack Query handles offline queuing automatically.
|
|
266
268
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AccountKind,
|
|
1
|
+
import type { AccountKind, AccountCategoryId, UserNameResponse, UserRelationship, ThemePreference } from '@oxyhq/contracts';
|
|
2
2
|
export interface OxyConfig {
|
|
3
3
|
baseURL: string;
|
|
4
4
|
cloudURL?: string;
|
|
@@ -151,8 +151,16 @@ export interface User {
|
|
|
151
151
|
};
|
|
152
152
|
isManagedAccount?: boolean;
|
|
153
153
|
managedBy?: string;
|
|
154
|
-
/**
|
|
155
|
-
|
|
154
|
+
/**
|
|
155
|
+
* What this account is about, for any NON-personal account. ORDERED — the
|
|
156
|
+
* first element is the primary category, and nothing may reorder it.
|
|
157
|
+
*
|
|
158
|
+
* Stable ids, not labels: render each through the
|
|
159
|
+
* `accounts.accountCategory.<id>` translation key so the reader sees their own
|
|
160
|
+
* language rather than the language of whoever chose it. Absent when the
|
|
161
|
+
* account has none.
|
|
162
|
+
*/
|
|
163
|
+
accountCategories?: AccountCategoryId[];
|
|
156
164
|
/**
|
|
157
165
|
* The account's languages as full BCP-47 locales (`language-REGION`, e.g.
|
|
158
166
|
* `en-US`, `es-MX`, `pt-BR`), ordered with the PRIMARY (UI) locale first.
|
|
@@ -25,7 +25,9 @@ export type { OxyCorsOptions } from './cors';
|
|
|
25
25
|
export { buildOxyCspDirectives, buildOxyPagesHeaders, createOxySecurityHeaders, formatOxyCspPolicy, OXY_CSP_BASELINE, } from './securityHeaders';
|
|
26
26
|
export type { OxyCspDirective, OxyCspExtensions, OxyPagesHeadersOptions, OxySecurityHeadersOptions, } from './securityHeaders';
|
|
27
27
|
export { verifySecret } from './verifySecret';
|
|
28
|
-
export { createOxyUserInvalidationHandler,
|
|
29
|
-
export type {
|
|
28
|
+
export { createOxyUserInvalidationHandler, publishOxyUserInvalidation, } from './userInvalidation';
|
|
29
|
+
export type { OxyInvalidationPublisher, OxyUserInvalidationHandlerOptions, } from './userInvalidation';
|
|
30
|
+
export { evictOxyIdentityCache, oxyUserByIdCacheKey, OXY_IDENTITY_CACHE_PREFIXES } from '../utils/identityCacheSweep';
|
|
31
|
+
export type { OxyIdentityCacheEvictor } from '../utils/identityCacheSweep';
|
|
30
32
|
export { registrableApex } from '../utils/registrableApex';
|
|
31
33
|
export { isOfficialWebOrigin } from '../utils/officialOrigins';
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* Every Oxy backend caches Oxy identity, and none of them find out when it
|
|
7
7
|
* changes. The `OxyServices` GET response cache holds `GET /users/:id` and
|
|
8
8
|
* `GET /profiles/username/:name` for five minutes; it is swept when THIS process
|
|
9
|
-
* writes the profile (
|
|
10
|
-
* never when somebody else does — which is the normal case, since
|
|
11
|
-
* edited in Oxy's own apps. So an avatar or display-name change is
|
|
12
|
-
* every consuming backend for up to five minutes, per process.
|
|
9
|
+
* writes the profile (the `evictOxyIdentityCache` calls in the user and accounts
|
|
10
|
+
* mixins) and never when somebody else does — which is the normal case, since
|
|
11
|
+
* profiles are edited in Oxy's own apps. So an avatar or display-name change is
|
|
12
|
+
* invisible to every consuming backend for up to five minutes, per process.
|
|
13
13
|
*
|
|
14
14
|
* oxy-api broadcasts {@link OXY_USER_INVALIDATION_CHANNEL} on the shared Valkey
|
|
15
15
|
* when a user's identity changes. This module is the consumer half: it parses
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
* Node-only; exported solely from `@oxyhq/core/server`.
|
|
52
52
|
*/
|
|
53
53
|
import { type OxyUserChangeReason, type OxyUserInvalidationEvent } from '@oxyhq/contracts';
|
|
54
|
+
import { type OxyIdentityCacheEvictor } from '../utils/identityCacheSweep';
|
|
54
55
|
/**
|
|
55
56
|
* The publish surface of a Redis client. Both `ioredis` and `node-redis`
|
|
56
57
|
* satisfy this structurally, so neither library is a dependency here.
|
|
@@ -58,14 +59,6 @@ import { type OxyUserChangeReason, type OxyUserInvalidationEvent } from '@oxyhq/
|
|
|
58
59
|
export interface OxyInvalidationPublisher {
|
|
59
60
|
publish(channel: string, message: string): unknown;
|
|
60
61
|
}
|
|
61
|
-
/**
|
|
62
|
-
* The cache-eviction surface of an {@link OxyServices} instance. Declared
|
|
63
|
-
* structurally so this Node-only module does not pull in the client.
|
|
64
|
-
*/
|
|
65
|
-
export interface OxyIdentityCacheEvictor {
|
|
66
|
-
clearCacheEntry(key: string): void;
|
|
67
|
-
clearCacheByPrefix(prefix: string): number;
|
|
68
|
-
}
|
|
69
62
|
/**
|
|
70
63
|
* Broadcast that an Oxy user's record changed.
|
|
71
64
|
*
|
|
@@ -119,15 +112,3 @@ export interface OxyUserInvalidationHandlerOptions {
|
|
|
119
112
|
* fixes.
|
|
120
113
|
*/
|
|
121
114
|
export declare function createOxyUserInvalidationHandler(options?: OxyUserInvalidationHandlerOptions): (raw: string) => void;
|
|
122
|
-
/**
|
|
123
|
-
* Sweep an `OxyServices` GET response cache of everything that could carry the
|
|
124
|
-
* given user's identity.
|
|
125
|
-
*
|
|
126
|
-
* The by-id entry is exact. The by-username and resolve entries are keyed by
|
|
127
|
-
* HANDLE, which cannot be derived from an id without the very lookup we are
|
|
128
|
-
* invalidating, so those are swept by prefix — the same imprecision the SDK
|
|
129
|
-
* already accepts when it sweeps its own cache after a local profile write, and
|
|
130
|
-
* bounded by the fact that over-eviction costs a refetch and can never serve
|
|
131
|
-
* wrong data.
|
|
132
|
-
*/
|
|
133
|
-
export declare function evictOxyIdentityCache(oxy: OxyIdentityCacheEvictor, userId: string): void;
|
|
@@ -34,10 +34,10 @@ export interface RefreshDeps {
|
|
|
34
34
|
* The outcome of ONE device-secret mint attempt (arm 1). Discriminated so both
|
|
35
35
|
* the re-mint handler and the cold boot can react per the transport contract
|
|
36
36
|
* without re-classifying the raw error:
|
|
37
|
-
* - `ok` — minted, persisted
|
|
37
|
+
* - `ok` — minted, persisted `nextDeviceSecret`, planted the token.
|
|
38
38
|
* - `no-secret` — the store holds no `deviceId` + `deviceSecret` to mint from.
|
|
39
39
|
* - `invalid-secret` — 401 `invalid_device_secret`: the presented secret
|
|
40
|
-
*
|
|
40
|
+
* no longer matches the server's stored hash.
|
|
41
41
|
* - `no-session` — 401 `no_active_session`: the device is known but has no live
|
|
42
42
|
* session (authoritative signed-out).
|
|
43
43
|
* - `account-not-on-device` — 401 `account_not_on_device` for a PINNED mint: the
|
|
@@ -45,10 +45,10 @@ export interface RefreshDeps {
|
|
|
45
45
|
* The device secret is FINE — it is the identity binding that went stale, so
|
|
46
46
|
* the caller must re-establish from the local key, never drop the credential.
|
|
47
47
|
* - `transient` — network / 5xx; keep the secret, a later attempt can succeed.
|
|
48
|
-
* - `persist-failed` — the mint succeeded
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
48
|
+
* - `persist-failed` — the mint succeeded but `nextDeviceSecret` could NOT be
|
|
49
|
+
* durably persisted. The token is deliberately NOT planted: advertising a
|
|
50
|
+
* healthy session on a secret that will not survive a reload is exactly the
|
|
51
|
+
* divergence that logs users out.
|
|
52
52
|
*/
|
|
53
53
|
export type DeviceSecretMintOutcome = {
|
|
54
54
|
status: 'ok';
|
|
@@ -69,19 +69,14 @@ export type DeviceSecretMintOutcome = {
|
|
|
69
69
|
status: 'persist-failed';
|
|
70
70
|
};
|
|
71
71
|
/**
|
|
72
|
-
* Arm 1 — the
|
|
73
|
-
*
|
|
72
|
+
* Arm 1 — the device-secret mint, run under the owning client's PROCESS-WIDE
|
|
73
|
+
* single-flight (`httpService.runSingleFlightDeviceSecretMint`).
|
|
74
74
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* SUPERSEDED secret — after the grace window the next cold boot mint 401s and the
|
|
81
|
-
* user is signed out. Routing EVERY lane through this one single-flight makes
|
|
82
|
-
* concurrent callers await the SAME in-flight mint and all receive its result, so
|
|
83
|
-
* there is exactly one rotation and the store always converges on the true
|
|
84
|
-
* `current` secret.
|
|
75
|
+
* Concurrent lanes (cold boot, the proactive scheduler, a request-time preflight,
|
|
76
|
+
* a 401 retry, the socket token transport, or a tab-focus reconcile) must not
|
|
77
|
+
* each persist a different view of the mint response. Routing EVERY lane through
|
|
78
|
+
* this one single-flight makes concurrent callers await the SAME in-flight mint
|
|
79
|
+
* and all receive its result, so the durable store converges on one credential.
|
|
85
80
|
*
|
|
86
81
|
* On success it persists `nextDeviceSecret` (read-back-verified) BEFORE planting
|
|
87
82
|
* the access token; a failed durable persist yields `persist-failed` WITHOUT
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE enumeration of `OxyServices` GET-cache keys that can carry a single
|
|
3
|
+
* account's identity, and the one sweep that clears them.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS IS ONE LIST
|
|
6
|
+
* --------------------
|
|
7
|
+
* An Oxy account is readable under SEVERAL cache keys, and a write that only
|
|
8
|
+
* busts the key it happens to know about leaves every other one serving the
|
|
9
|
+
* pre-write snapshot for up to its TTL — from the caller's OWN in-memory cache,
|
|
10
|
+
* with a perfectly healthy server. That failure has already shipped twice with
|
|
11
|
+
* two different sets of keys:
|
|
12
|
+
*
|
|
13
|
+
* - `updateAccount` busted `GET:/accounts/<id>` and the account lists, but a
|
|
14
|
+
* profile screen reads `GET:/profiles/username/<name>` and
|
|
15
|
+
* `GET:/users/<id>`, so a channel's new picture stayed invisible for the
|
|
16
|
+
* full 5-minute profile TTL.
|
|
17
|
+
* - `updateProfile` busted four of the six keys below, missing
|
|
18
|
+
* `GET:/auth/lookup/` (the login-flow avatar/display-name lookup) and
|
|
19
|
+
* `GET:/profiles/resolve` (handle resolution) — two independently-drifted
|
|
20
|
+
* copies of a list that has to agree.
|
|
21
|
+
*
|
|
22
|
+
* So the list lives here, once, and every writer calls
|
|
23
|
+
* {@link evictOxyIdentityCache}. Adding a new identity read means adding its key
|
|
24
|
+
* HERE and every writer inherits it.
|
|
25
|
+
*
|
|
26
|
+
* WHERE THE LINE IS DRAWN
|
|
27
|
+
* -----------------------
|
|
28
|
+
* These are the SINGLE-PROFILE reads — the account is the subject of the
|
|
29
|
+
* response and is addressable by id, handle, or session. Reads that merely
|
|
30
|
+
* CONTAIN an account among many (`GET:/profiles/search`,
|
|
31
|
+
* `GET:/users/<other>/followers`, `GET:/profiles/<other>/similar`) are
|
|
32
|
+
* deliberately NOT swept: an account cannot be located in them without the very
|
|
33
|
+
* lookup being invalidated, so sweeping them means sweeping the whole namespace
|
|
34
|
+
* on every identity change — a real cost on a backend consuming the
|
|
35
|
+
* cross-service invalidation signal, for a surface where a stale thumbnail
|
|
36
|
+
* expires on its own in ~2 minutes.
|
|
37
|
+
*
|
|
38
|
+
* WHY PREFIXES RATHER THAN EXACT KEYS
|
|
39
|
+
* -----------------------------------
|
|
40
|
+
* Only the by-id key can be built from a user id. The handle-keyed and
|
|
41
|
+
* session-keyed entries cannot — deriving a handle from an id needs the lookup
|
|
42
|
+
* we are invalidating, and the SDK never tracks active session ids centrally.
|
|
43
|
+
* Prefix sweeping is also what makes a USERNAME CHANGE correct: the entry under
|
|
44
|
+
* the OLD handle is unreachable by construction (nothing in the write response
|
|
45
|
+
* carries it), and a sweep targeted at the new handle alone would leave the old
|
|
46
|
+
* one serving the pre-rename profile until its TTL. Over-eviction costs a
|
|
47
|
+
* refetch; under-eviction serves wrong data.
|
|
48
|
+
*
|
|
49
|
+
* Platform-neutral by construction (no imports, no `OxyServices` reference) so
|
|
50
|
+
* the client mixins and the Node-only `@oxyhq/core/server` invalidation
|
|
51
|
+
* subscriber can share it without either pulling in the other.
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* The cache-eviction surface of an `OxyServices` instance. Declared
|
|
55
|
+
* structurally so this module stays free of any client import.
|
|
56
|
+
*/
|
|
57
|
+
export interface OxyIdentityCacheEvictor {
|
|
58
|
+
clearCacheEntry(key: string): void;
|
|
59
|
+
clearCacheByPrefix(prefix: string): number;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Cache-key PREFIXES under which an account's identity can be served, for the
|
|
63
|
+
* reads whose key cannot be derived from a user id. Swept wholesale.
|
|
64
|
+
*/
|
|
65
|
+
export declare const OXY_IDENTITY_CACHE_PREFIXES: readonly string[];
|
|
66
|
+
/**
|
|
67
|
+
* Build the exact cache key `getUserById` reads under. The only identity key
|
|
68
|
+
* derivable from a user id, so the only one that does not need a prefix sweep.
|
|
69
|
+
*/
|
|
70
|
+
export declare function oxyUserByIdCacheKey(userId: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* Sweep an `OxyServices` GET response cache of everything that could carry the
|
|
73
|
+
* given account's identity.
|
|
74
|
+
*
|
|
75
|
+
* @param oxy - Anything exposing the SDK's two eviction methods.
|
|
76
|
+
* @param userId - The account whose by-id entry to drop. Optional: a caller
|
|
77
|
+
* that does not know the id still clears every handle-, session-
|
|
78
|
+
* and self-keyed entry, which is the majority of the surface.
|
|
79
|
+
*/
|
|
80
|
+
export declare function evictOxyIdentityCache(oxy: OxyIdentityCacheEvictor, userId?: string): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "19.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",
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"dependencies": {
|
|
116
116
|
"@noble/ciphers": "^1.3.0",
|
|
117
117
|
"@noble/hashes": "^1.8.0",
|
|
118
|
-
"@oxyhq/contracts": "^0.
|
|
118
|
+
"@oxyhq/contracts": "^0.23.0",
|
|
119
119
|
"@oxyhq/protocol": "^0.1.6",
|
|
120
120
|
"@scure/bip39": "^1.6.0",
|
|
121
121
|
"@types/elliptic": "^6.4.18",
|
|
@@ -227,10 +227,9 @@ export async function runSessionColdBoot(
|
|
|
227
227
|
// origin persisted a deviceId + deviceSecret, mint a short access token with
|
|
228
228
|
// a single bearer-less POST (no cookie, no navigation). The mint itself runs
|
|
229
229
|
// through `refreshDeviceSecretArm`, which acquires the client's PROCESS-WIDE
|
|
230
|
-
// single-flight, persists
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
// the durable store always converges on the true `current` secret.
|
|
230
|
+
// single-flight, persists `nextDeviceSecret` BEFORE planting the token, and
|
|
231
|
+
// returns a classified outcome — so concurrent mint lanes share one in-flight
|
|
232
|
+
// request and the durable store always converges on the server's credential.
|
|
234
233
|
steps.push({
|
|
235
234
|
id: 'device-secret-mint',
|
|
236
235
|
// Network step — skip entirely when the caller reports the device offline so
|
|
@@ -249,7 +248,7 @@ export async function runSessionColdBoot(
|
|
|
249
248
|
const result = await refreshDeviceSecretArm({ oxy, store, pin });
|
|
250
249
|
switch (result.status) {
|
|
251
250
|
case 'ok':
|
|
252
|
-
// The arm persisted
|
|
251
|
+
// The arm persisted nextDeviceSecret and planted the token.
|
|
253
252
|
return {
|
|
254
253
|
kind: 'session',
|
|
255
254
|
session: {
|
|
@@ -1748,11 +1748,53 @@
|
|
|
1748
1748
|
"description": "A programmatic account with service credentials"
|
|
1749
1749
|
}
|
|
1750
1750
|
},
|
|
1751
|
-
"
|
|
1751
|
+
"accountCategory": {
|
|
1752
|
+
"news": "News",
|
|
1753
|
+
"politics": "Politics & policy",
|
|
1754
|
+
"business": "Business",
|
|
1755
|
+
"startup": "Startups & entrepreneurship",
|
|
1756
|
+
"finance": "Finance & investing",
|
|
1757
|
+
"crypto": "Crypto & web3",
|
|
1758
|
+
"marketplace": "Marketplace & classifieds",
|
|
1759
|
+
"retail": "Retail & ecommerce",
|
|
1760
|
+
"real_estate": "Real estate",
|
|
1752
1761
|
"agency": "Real estate agency",
|
|
1762
|
+
"landlord": "Landlord & property management",
|
|
1753
1763
|
"cooperative": "Housing cooperative",
|
|
1754
|
-
"
|
|
1755
|
-
"
|
|
1764
|
+
"architecture": "Architecture & construction",
|
|
1765
|
+
"technology": "Technology",
|
|
1766
|
+
"software": "Software & development",
|
|
1767
|
+
"ai": "AI & machine learning",
|
|
1768
|
+
"security": "Security & privacy",
|
|
1769
|
+
"automation": "Automation & tools",
|
|
1770
|
+
"science": "Science & research",
|
|
1771
|
+
"education": "Education & training",
|
|
1772
|
+
"books": "Books & writing",
|
|
1773
|
+
"health": "Health & medicine",
|
|
1774
|
+
"fitness": "Fitness & wellbeing",
|
|
1775
|
+
"sports": "Sports",
|
|
1776
|
+
"gaming": "Gaming & esports",
|
|
1777
|
+
"music": "Music",
|
|
1778
|
+
"film": "Film & TV",
|
|
1779
|
+
"podcast": "Podcasts & audio",
|
|
1780
|
+
"art": "Art & design",
|
|
1781
|
+
"photography": "Photography",
|
|
1782
|
+
"comedy": "Comedy",
|
|
1783
|
+
"food": "Food & drink",
|
|
1784
|
+
"travel": "Travel & places",
|
|
1785
|
+
"fashion": "Fashion & beauty",
|
|
1786
|
+
"home_garden": "Home & garden",
|
|
1787
|
+
"diy": "DIY & making",
|
|
1788
|
+
"automotive": "Cars & motoring",
|
|
1789
|
+
"animals": "Animals & pets",
|
|
1790
|
+
"family": "Family & parenting",
|
|
1791
|
+
"nonprofit": "Non-profit & charity",
|
|
1792
|
+
"government": "Government & public sector",
|
|
1793
|
+
"community": "Local community",
|
|
1794
|
+
"activism": "Activism & advocacy",
|
|
1795
|
+
"environment": "Environment & sustainability",
|
|
1796
|
+
"religion": "Religion & spirituality",
|
|
1797
|
+
"other": "Other"
|
|
1756
1798
|
},
|
|
1757
1799
|
"create": {
|
|
1758
1800
|
"title": "Create account",
|
|
@@ -1772,8 +1814,9 @@
|
|
|
1772
1814
|
"invalidChars": "Only letters, numbers, hyphens, and underscores",
|
|
1773
1815
|
"checkFailed": "Could not check availability"
|
|
1774
1816
|
},
|
|
1775
|
-
"
|
|
1776
|
-
"label": "
|
|
1817
|
+
"accountCategory": {
|
|
1818
|
+
"label": "Categories",
|
|
1819
|
+
"hint": "Pick up to {{max}}. The first one is your main category."
|
|
1777
1820
|
},
|
|
1778
1821
|
"toasts": {
|
|
1779
1822
|
"success": "Account created",
|
|
@@ -1748,11 +1748,53 @@
|
|
|
1748
1748
|
"description": "Una cuenta programática con credenciales de servicio"
|
|
1749
1749
|
}
|
|
1750
1750
|
},
|
|
1751
|
-
"
|
|
1751
|
+
"accountCategory": {
|
|
1752
|
+
"news": "Noticias",
|
|
1753
|
+
"politics": "Política",
|
|
1754
|
+
"business": "Negocios",
|
|
1755
|
+
"startup": "Startups y emprendimiento",
|
|
1756
|
+
"finance": "Finanzas e inversión",
|
|
1757
|
+
"crypto": "Cripto y web3",
|
|
1758
|
+
"marketplace": "Marketplace y anuncios",
|
|
1759
|
+
"retail": "Comercio y ecommerce",
|
|
1760
|
+
"real_estate": "Inmobiliaria",
|
|
1752
1761
|
"agency": "Agencia inmobiliaria",
|
|
1762
|
+
"landlord": "Casero y gestión de fincas",
|
|
1753
1763
|
"cooperative": "Cooperativa de vivienda",
|
|
1754
|
-
"
|
|
1755
|
-
"
|
|
1764
|
+
"architecture": "Arquitectura y construcción",
|
|
1765
|
+
"technology": "Tecnología",
|
|
1766
|
+
"software": "Software y desarrollo",
|
|
1767
|
+
"ai": "IA y aprendizaje automático",
|
|
1768
|
+
"security": "Seguridad y privacidad",
|
|
1769
|
+
"automation": "Automatización y herramientas",
|
|
1770
|
+
"science": "Ciencia e investigación",
|
|
1771
|
+
"education": "Educación y formación",
|
|
1772
|
+
"books": "Libros y escritura",
|
|
1773
|
+
"health": "Salud y medicina",
|
|
1774
|
+
"fitness": "Deporte y bienestar",
|
|
1775
|
+
"sports": "Deportes",
|
|
1776
|
+
"gaming": "Videojuegos y esports",
|
|
1777
|
+
"music": "Música",
|
|
1778
|
+
"film": "Cine y TV",
|
|
1779
|
+
"podcast": "Pódcast y audio",
|
|
1780
|
+
"art": "Arte y diseño",
|
|
1781
|
+
"photography": "Fotografía",
|
|
1782
|
+
"comedy": "Comedia",
|
|
1783
|
+
"food": "Comida y bebida",
|
|
1784
|
+
"travel": "Viajes y lugares",
|
|
1785
|
+
"fashion": "Moda y belleza",
|
|
1786
|
+
"home_garden": "Hogar y jardín",
|
|
1787
|
+
"diy": "Bricolaje y manualidades",
|
|
1788
|
+
"automotive": "Coches y motor",
|
|
1789
|
+
"animals": "Animales y mascotas",
|
|
1790
|
+
"family": "Familia y crianza",
|
|
1791
|
+
"nonprofit": "ONG y benéficas",
|
|
1792
|
+
"government": "Gobierno y sector público",
|
|
1793
|
+
"community": "Comunidad local",
|
|
1794
|
+
"activism": "Activismo y defensa",
|
|
1795
|
+
"environment": "Medio ambiente y sostenibilidad",
|
|
1796
|
+
"religion": "Religión y espiritualidad",
|
|
1797
|
+
"other": "Otra"
|
|
1756
1798
|
},
|
|
1757
1799
|
"create": {
|
|
1758
1800
|
"title": "Crear cuenta",
|
|
@@ -1772,8 +1814,9 @@
|
|
|
1772
1814
|
"invalidChars": "Solo letras, números, guiones y guiones bajos",
|
|
1773
1815
|
"checkFailed": "No se pudo comprobar la disponibilidad"
|
|
1774
1816
|
},
|
|
1775
|
-
"
|
|
1776
|
-
"label": "
|
|
1817
|
+
"accountCategory": {
|
|
1818
|
+
"label": "Categorías",
|
|
1819
|
+
"hint": "Elige hasta {{max}}. La primera es tu categoría principal."
|
|
1777
1820
|
},
|
|
1778
1821
|
"toasts": {
|
|
1779
1822
|
"success": "Cuenta creada",
|
package/src/index.ts
CHANGED
|
@@ -122,7 +122,7 @@ export type {
|
|
|
122
122
|
// ---------------------------------------------------------------------------
|
|
123
123
|
export type {
|
|
124
124
|
AccountKind,
|
|
125
|
-
|
|
125
|
+
AccountCategoryId,
|
|
126
126
|
AccountRelationship,
|
|
127
127
|
AccountRole,
|
|
128
128
|
AccountMemberStatus,
|
|
@@ -136,7 +136,6 @@ export type {
|
|
|
136
136
|
AccountCredentialWithSecret,
|
|
137
137
|
RotateAccountCredentialResult,
|
|
138
138
|
ListAccountsOptions,
|
|
139
|
-
UserCreatableAccountKind,
|
|
140
139
|
CreateAccountInput,
|
|
141
140
|
UpdateAccountInput,
|
|
142
141
|
ProvisionChannelInput,
|
|
@@ -168,7 +167,13 @@ export type {
|
|
|
168
167
|
ApplicationUsageStats,
|
|
169
168
|
} from './mixins/OxyServices.accounts';
|
|
170
169
|
|
|
171
|
-
export {
|
|
170
|
+
export {
|
|
171
|
+
ACCOUNT_CATEGORY_IDS,
|
|
172
|
+
MAX_ACCOUNT_CATEGORIES,
|
|
173
|
+
SELECTABLE_ACCOUNT_CATEGORY_IDS,
|
|
174
|
+
isSelectableAccountCategoryId,
|
|
175
|
+
kindAcceptsAccountCategories,
|
|
176
|
+
} from './mixins/OxyServices.accounts';
|
|
172
177
|
|
|
173
178
|
// ---------------------------------------------------------------------------
|
|
174
179
|
// Reputation (Oxy Trust: ledger, balances, disputes, rules, influence).
|