@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.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/boot/sessionColdBoot.js +4 -5
- package/dist/cjs/mixins/OxyServices.deviceBoot.js +3 -2
- package/dist/cjs/session/refresh.js +9 -14
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/boot/sessionColdBoot.js +4 -5
- package/dist/esm/mixins/OxyServices.deviceBoot.js +3 -2
- package/dist/esm/session/refresh.js +9 -14
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/mixins/OxyServices.accounts.d.ts +16 -5
- package/dist/types/mixins/OxyServices.deviceBoot.d.ts +3 -2
- package/dist/types/session/refresh.d.ts +13 -18
- package/package.json +1 -1
- package/src/boot/sessionColdBoot.ts +4 -5
- package/src/index.ts +0 -1
- package/src/mixins/OxyServices.accounts.ts +16 -6
- package/src/mixins/OxyServices.deviceBoot.ts +3 -2
- package/src/session/refresh.ts +15 -20
package/dist/types/index.d.ts
CHANGED
|
@@ -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,
|
|
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
|
-
/**
|
|
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.
|
|
@@ -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-
|
|
@@ -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
|
package/package.json
CHANGED
|
@@ -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: {
|
package/src/index.ts
CHANGED
|
@@ -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
|
-
/**
|
|
155
|
-
|
|
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` (
|
|
74
|
-
*
|
|
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-
|
package/src/session/refresh.ts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
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
|
|
118
|
-
*
|
|
117
|
+
* Arm 1 — the device-secret mint, run under the owning client's PROCESS-WIDE
|
|
118
|
+
* single-flight (`httpService.runSingleFlightDeviceSecretMint`).
|
|
119
119
|
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
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
|
-
//
|
|
204
|
-
//
|
|
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' };
|