@oxyhq/core 17.1.0 → 18.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.
@@ -39,7 +39,7 @@ 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, OrganizationCategory, AccountRelationship, AccountRole, AccountMemberStatus, AccountMemberSource, AccountMember, AccountNode, AccountCredentialType, AccountCredentialEnvironment, AccountCredentialStatus, AccountCredential, AccountCredentialWithSecret, RotateAccountCredentialResult, ListAccountsOptions, UserCreatableAccountKind, 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';
42
+ export type { AccountKind, OrganizationCategory, 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
43
  export { ORGANIZATION_CATEGORIES } 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';
@@ -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, OrganizationCategory } from '@oxyhq/contracts';
36
+ import type { AccountKind, OrganizationCategory, ChildAccountKind } from '@oxyhq/contracts';
37
37
  import type { SessionLoginResponse } from '../models/session';
38
38
  import type { OxyServicesBase } from '../OxyServices.base';
39
39
  /**
@@ -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
- /** Classification of the new account. `personal` and `channel` are not creatable here. */
139
- kind: UserCreatableAccountKind;
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.
@@ -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` (rotation-in-use)
48
- * and the projected device-session `state`.
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-
@@ -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 the rotated secret, planted the token.
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
- * diverged (another tab/device rotated it past the grace window).
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 (the SERVER rotated the secret) but
49
- * the rotated `nextDeviceSecret` could NOT be durably persisted. The token is
50
- * deliberately NOT planted: advertising a healthy session on a secret that
51
- * will not survive a reload is exactly the divergence that logs users out.
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 rotating device-secret mint, run under the owning client's
73
- * PROCESS-WIDE single-flight (`httpService.runSingleFlightDeviceSecretMint`).
72
+ * Arm 1 — the device-secret mint, run under the owning client's PROCESS-WIDE
73
+ * single-flight (`httpService.runSingleFlightDeviceSecretMint`).
74
74
  *
75
- * The server ROTATES the presented `deviceSecret` on every successful mint and
76
- * the just-presented secret is valid only for a short grace window. If two lanes
77
- * (cold boot, the proactive scheduler, a request-time preflight, a 401 retry,
78
- * the socket token transport, or a tab-focus reconcile) minted concurrently they
79
- * would double-rotate the server and the durable store could converge on the
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "17.1.0",
3
+ "version": "18.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",
@@ -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 the rotated `nextDeviceSecret` BEFORE planting the
231
- // token, and returns a classified outcome — so this step can never
232
- // double-rotate the server against the scheduler/transport/401 lanes, and
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 the rotated secret and planted the token.
251
+ // The arm persisted nextDeviceSecret and planted the token.
253
252
  return {
254
253
  kind: 'session',
255
254
  session: {
package/src/index.ts CHANGED
@@ -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,
@@ -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, OrganizationCategory } from '@oxyhq/contracts';
36
+ import type { AccountKind, OrganizationCategory, ChildAccountKind } from '@oxyhq/contracts';
37
37
  import type { SessionLoginResponse } from '../models/session';
38
38
  import type { OxyServicesBase } from '../OxyServices.base';
39
39
  import { normalizeUserIdentity } from '../utils/userIdentity';
@@ -146,13 +146,23 @@ export interface ListAccountsOptions {
146
146
  tree?: boolean;
147
147
  }
148
148
 
149
- /** Kinds a signed-in user may create via `POST /accounts`. Channels are service-provisioned only. */
150
- export type UserCreatableAccountKind = 'organization' | 'project' | 'bot';
151
-
152
149
  /** Input accepted by `createAccount`. */
153
150
  export interface CreateAccountInput {
154
- /** Classification of the new account. `personal` and `channel` are not creatable here. */
155
- kind: UserCreatableAccountKind;
151
+ /**
152
+ * Classification of the new account. Every CHILD kind is creatable here with
153
+ * the caller's own bearer, `channel` included: a signed-in person has already
154
+ * proven who they are, and minting a child under their own tree is the same
155
+ * operation whichever kind it is.
156
+ *
157
+ * `channel` used to be excluded, on the reasoning that channels are
158
+ * service-provisioned only. What actually makes a channel safe does not depend
159
+ * on who creates it: `createChildAccount` writes no auth method, so it is born
160
+ * with no login, and `POST /accounts/:id/switch` refuses it via
161
+ * `isActAsEligibleKind`, so no session can ever have a channel as its subject
162
+ * and therefore no bearer exists that could add one. `personal` is excluded
163
+ * because it is a human login, minted at signup.
164
+ */
165
+ kind: ChildAccountKind;
156
166
  /**
157
167
  * Parent account `_id` to nest the new account under. Omitted → the API roots
158
168
  * it under the caller's personal account.
@@ -70,8 +70,9 @@ export function OxyServicesDeviceBootMixin<T extends typeof OxyServicesBase>(Bas
70
70
  * Zero-cookie mint. Present the first-party `deviceId` + `deviceSecret` to
71
71
  * `POST /session/device/token` — NO bearer, NO cookies: possession of the
72
72
  * secret IS the device-ownership proof. Returns a fresh short access token
73
- * for the device's active account plus `nextDeviceSecret` (rotation-in-use)
74
- * and the projected device-session `state`.
73
+ * for the device's active account plus `nextDeviceSecret` (on mint, the same
74
+ * proven secret echoed back — rotation happens on sign-in, not mint) and
75
+ * the projected device-session `state`.
75
76
  *
76
77
  * `skipAuth`: this call carries no bearer, so a 401 must surface DIRECTLY —
77
78
  * never trigger `HttpService`'s 401→refresh→retry dance. The cold boot / re-
@@ -88,10 +88,10 @@ export interface RefreshDeps {
88
88
  * The outcome of ONE device-secret mint attempt (arm 1). Discriminated so both
89
89
  * the re-mint handler and the cold boot can react per the transport contract
90
90
  * without re-classifying the raw error:
91
- * - `ok` — minted, persisted the rotated secret, planted the token.
91
+ * - `ok` — minted, persisted `nextDeviceSecret`, planted the token.
92
92
  * - `no-secret` — the store holds no `deviceId` + `deviceSecret` to mint from.
93
93
  * - `invalid-secret` — 401 `invalid_device_secret`: the presented secret
94
- * diverged (another tab/device rotated it past the grace window).
94
+ * no longer matches the server's stored hash.
95
95
  * - `no-session` — 401 `no_active_session`: the device is known but has no live
96
96
  * session (authoritative signed-out).
97
97
  * - `account-not-on-device` — 401 `account_not_on_device` for a PINNED mint: the
@@ -99,10 +99,10 @@ export interface RefreshDeps {
99
99
  * The device secret is FINE — it is the identity binding that went stale, so
100
100
  * the caller must re-establish from the local key, never drop the credential.
101
101
  * - `transient` — network / 5xx; keep the secret, a later attempt can succeed.
102
- * - `persist-failed` — the mint succeeded (the SERVER rotated the secret) but
103
- * the rotated `nextDeviceSecret` could NOT be durably persisted. The token is
104
- * deliberately NOT planted: advertising a healthy session on a secret that
105
- * will not survive a reload is exactly the divergence that logs users out.
102
+ * - `persist-failed` — the mint succeeded but `nextDeviceSecret` could NOT be
103
+ * durably persisted. The token is deliberately NOT planted: advertising a
104
+ * healthy session on a secret that will not survive a reload is exactly the
105
+ * divergence that logs users out.
106
106
  */
107
107
  export type DeviceSecretMintOutcome =
108
108
  | { status: 'ok'; token: string; sessionId: string; userId: string }
@@ -114,19 +114,14 @@ export type DeviceSecretMintOutcome =
114
114
  | { status: 'persist-failed' };
115
115
 
116
116
  /**
117
- * Arm 1 — the rotating device-secret mint, run under the owning client's
118
- * PROCESS-WIDE single-flight (`httpService.runSingleFlightDeviceSecretMint`).
117
+ * Arm 1 — the device-secret mint, run under the owning client's PROCESS-WIDE
118
+ * single-flight (`httpService.runSingleFlightDeviceSecretMint`).
119
119
  *
120
- * The server ROTATES the presented `deviceSecret` on every successful mint and
121
- * the just-presented secret is valid only for a short grace window. If two lanes
122
- * (cold boot, the proactive scheduler, a request-time preflight, a 401 retry,
123
- * the socket token transport, or a tab-focus reconcile) minted concurrently they
124
- * would double-rotate the server and the durable store could converge on the
125
- * SUPERSEDED secret — after the grace window the next cold boot mint 401s and the
126
- * user is signed out. Routing EVERY lane through this one single-flight makes
127
- * concurrent callers await the SAME in-flight mint and all receive its result, so
128
- * there is exactly one rotation and the store always converges on the true
129
- * `current` secret.
120
+ * Concurrent lanes (cold boot, the proactive scheduler, a request-time preflight,
121
+ * a 401 retry, the socket token transport, or a tab-focus reconcile) must not
122
+ * each persist a different view of the mint response. Routing EVERY lane through
123
+ * this one single-flight makes concurrent callers await the SAME in-flight mint
124
+ * and all receive its result, so the durable store converges on one credential.
130
125
  *
131
126
  * On success it persists `nextDeviceSecret` (read-back-verified) BEFORE planting
132
127
  * the access token; a failed durable persist yields `persist-failed` WITHOUT
@@ -200,8 +195,8 @@ export async function refreshDeviceSecretArm(deps: {
200
195
  expiresAt: mint.expiresAt,
201
196
  ...(bound ? { sessionId: bound.sessionId, userId: bound.accountId } : {}),
202
197
  };
203
- // Rotation-in-use anti-loss: persist the NEXT secret and read-back-VERIFY it
204
- // landed BEFORE planting the token. A failed durable persist must NOT plant.
198
+ // Persist nextDeviceSecret (read-back-verified) BEFORE planting the token.
199
+ // A failed durable persist must NOT plant.
205
200
  const persistedOk = await store.save(next);
206
201
  if (!persistedOk) {
207
202
  return { status: 'persist-failed' };