@oxyhq/core 3.14.0 → 3.15.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/index.js +3 -1
- package/dist/cjs/mixins/OxyServices.applications.js +43 -0
- package/dist/cjs/mixins/OxyServices.nodes.js +175 -0
- package/dist/cjs/mixins/index.js +4 -0
- package/dist/cjs/utils/ssoBounce.js +48 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/mixins/OxyServices.applications.js +43 -0
- package/dist/esm/mixins/OxyServices.nodes.js +172 -0
- package/dist/esm/mixins/index.js +4 -0
- package/dist/esm/utils/ssoBounce.js +46 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +4 -2
- package/dist/types/mixins/OxyServices.applications.d.ts +51 -0
- package/dist/types/mixins/OxyServices.nodes.d.ts +242 -0
- package/dist/types/mixins/index.d.ts +2 -1
- package/dist/types/utils/ssoBounce.d.ts +61 -0
- package/package.json +1 -1
- package/src/index.ts +5 -0
- package/src/mixins/OxyServices.applications.ts +79 -0
- package/src/mixins/OxyServices.nodes.ts +348 -0
- package/src/mixins/__tests__/OxyServices.nodes.test.ts +341 -0
- package/src/mixins/__tests__/connectedApps.test.ts +123 -0
- package/src/mixins/index.ts +5 -0
- package/src/utils/__tests__/ssoBounce.test.ts +28 -0
- package/src/utils/ssoBounce.ts +69 -0
package/dist/types/index.d.ts
CHANGED
|
@@ -41,13 +41,14 @@ export { getCanonicalUserHandle, getNormalizedUserHandle, } from './utils/userHa
|
|
|
41
41
|
export type { CanonicalUserHandleInput, UserHandleInput } from './utils/userHandle';
|
|
42
42
|
export { normalizeProfileLinks } from './utils/profileLinks';
|
|
43
43
|
export type { ProfileLink, ProfileLinkMetadata } from './utils/profileLinks';
|
|
44
|
-
export type { Application, PublicApplication, ApplicationMember, ApplicationCredential, ApplicationRole, ApplicationType, ApplicationStatus, ApplicationMemberStatus, ApplicationCredentialType, ApplicationCredentialStatus, ApplicationEnvironment, CreateApplicationInput, UpdateApplicationInput, InviteApplicationMemberInput, UpdateApplicationMemberInput, TransferApplicationOwnershipInput, CreateApplicationCredentialInput, ApplicationCredentialWithSecret, RotateApplicationCredentialResult, ApplicationUsagePeriod, ApplicationUsageSummary, ApplicationUsageByDay, ApplicationUsageByEndpoint, ApplicationUsageStats, ApplicationSuccessResult, } from './mixins/OxyServices.applications';
|
|
44
|
+
export type { Application, PublicApplication, ConnectedApp, ApplicationMember, ApplicationCredential, ApplicationRole, ApplicationType, ApplicationStatus, ApplicationMemberStatus, ApplicationCredentialType, ApplicationCredentialStatus, ApplicationEnvironment, CreateApplicationInput, UpdateApplicationInput, InviteApplicationMemberInput, UpdateApplicationMemberInput, TransferApplicationOwnershipInput, CreateApplicationCredentialInput, ApplicationCredentialWithSecret, RotateApplicationCredentialResult, ApplicationUsagePeriod, ApplicationUsageSummary, ApplicationUsageByDay, ApplicationUsageByEndpoint, ApplicationUsageStats, ApplicationSuccessResult, } from './mixins/OxyServices.applications';
|
|
45
45
|
export type { Workspace, WorkspaceMember, WorkspaceRole, WorkspaceType, WorkspaceStatus, WorkspaceMemberStatus, CreateWorkspaceInput, UpdateWorkspaceInput, InviteWorkspaceMemberInput, UpdateWorkspaceMemberInput, TransferWorkspaceOwnershipInput, WorkspaceSuccessResult, } from './mixins/OxyServices.workspaces';
|
|
46
46
|
export type { ReputationCategory, TrustTier, ReputationTransactionStatus, ReputationTargetEntityType, ReputationDisputeStatus, ReputationInfluenceContext, ReputationTransaction, ReputationBalanceBreakdown, ReputationInfluence, ReputationReliability, ReputationBalance, ReputationDispute, ReputationRule, ReputationLeaderboardEntry, ReputationInfluenceResult, ReverseReputationTransactionResult, AwardReputationInput, CreateReputationDisputeInput, ResolveReputationDisputeInput, UpsertReputationRuleInput, ReverseReputationTransactionInput, } from './mixins/OxyServices.reputation';
|
|
47
47
|
export { buildUserDid } from './mixins/OxyServices.identity';
|
|
48
48
|
export type { IdentityRecordType, UnlinkableAuthMethodType, LinkAuthMethodResult, PublishRecordResult, VerifyRecordResult, VerifyDomainResult, RemoveDomainResult, } from './mixins/OxyServices.identity';
|
|
49
49
|
export { parseIdPayload, parseAttestPayload, verifyPublicCardAttestation, } from './mixins/OxyServices.civic';
|
|
50
50
|
export type { CivicCardResult, IdCardRef, AttestQrPayload, ParsedAttestPayload, SubmitRealLifeAttestationInput, DenyValidationResult, VouchForPersonInput, WithdrawVouchResult, IssueCredentialInput, RevokeCredentialResult, } from './mixins/OxyServices.civic';
|
|
51
|
+
export type { UserNodeStatus, UserNodeMode, UserNodeController, UserNodeLivenessStatus, RegisterNodeInput, RemoveNodeResult } from './mixins/OxyServices.nodes';
|
|
51
52
|
export { SessionSyncRequiredError, AuthenticationFailedError, ensureValidToken, isAuthenticationError, withAuthErrorHandling, authenticatedApiCall, } from './utils/authHelpers';
|
|
52
53
|
export type { HandleApiErrorOptions } from './utils/authHelpers';
|
|
53
54
|
export { mergeSessions, normalizeAndSortSessions, sessionsArraysEqual, } from './utils/sessionUtils';
|
|
@@ -93,7 +94,8 @@ export { CENTRAL_AUTH_URL, CENTRAL_IDP_APEX, resolveCentralAuthUrl } from './uti
|
|
|
93
94
|
export { parseSsoReturnFragment, consumeSsoReturn } from './utils/ssoReturn';
|
|
94
95
|
export type { SsoReturnKind, SsoReturnResult, ConsumeSsoReturnDeps } from './utils/ssoReturn';
|
|
95
96
|
export { generateSsoState } from './mixins/OxyServices.sso';
|
|
96
|
-
export { SSO_CALLBACK_PATH, SSO_GUARD_TTL_MS, ssoStateKey, ssoGuardKey, ssoDestKey, ssoNoSessionKey, ssoAttemptedKey, ssoCallbackBootstrapKey, ssoNavigate, getSsoCallbackBootstrapScript, buildSsoBounceUrl, isCentralIdPOrigin, guardActive, } from './utils/ssoBounce';
|
|
97
|
+
export { SSO_CALLBACK_PATH, SSO_GUARD_TTL_MS, ssoStateKey, ssoGuardKey, ssoDestKey, ssoNoSessionKey, ssoAttemptedKey, ssoPriorSessionKey, ssoCallbackBootstrapKey, ssoNavigate, getSsoCallbackBootstrapScript, buildSsoBounceUrl, isCentralIdPOrigin, guardActive, allowSsoBounce, } from './utils/ssoBounce';
|
|
98
|
+
export type { SsoBounceGate } from './utils/ssoBounce';
|
|
97
99
|
export { runColdBoot } from './utils/coldBoot';
|
|
98
100
|
export type { ColdBootStep, ColdBootStepResult, ColdBootSession, ColdBootSkip, ColdBootOutcome, RunColdBootOptions, } from './utils/coldBoot';
|
|
99
101
|
export { packageInfo } from './constants/version';
|
|
@@ -151,6 +151,31 @@ export interface PublicApplication {
|
|
|
151
151
|
/** Optional display name of the developer/owner organisation. */
|
|
152
152
|
developerName?: string;
|
|
153
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* A connected (OAuth-authorized) application from the current user's point of
|
|
156
|
+
* view: an application the user has granted access to via the consent flow.
|
|
157
|
+
*
|
|
158
|
+
* Returned by `GET /auth/grants` and rendered in the user-facing "Connected
|
|
159
|
+
* apps" management surface. Keyed by `applicationId` (the application's Mongo
|
|
160
|
+
* `_id`) rather than a credential/client id, so the grant — and a subsequent
|
|
161
|
+
* {@link OxyServicesApplicationsMixin.revokeAppGrant} — survive credential
|
|
162
|
+
* rotation. This is a display shape: it carries the application's name/logo and
|
|
163
|
+
* the granted scopes, never any membership or credential material.
|
|
164
|
+
*/
|
|
165
|
+
export interface ConnectedApp {
|
|
166
|
+
/** The connected application's Mongo `_id`. Use this to revoke the grant. */
|
|
167
|
+
applicationId: string;
|
|
168
|
+
/** Human-readable application name shown to the user. */
|
|
169
|
+
name: string;
|
|
170
|
+
/** Optional logo URL for the application. */
|
|
171
|
+
logoUrl?: string;
|
|
172
|
+
/** OAuth scopes the user has granted to the application. */
|
|
173
|
+
scopes: string[];
|
|
174
|
+
/** ISO timestamp of when the user first authorized the application. */
|
|
175
|
+
firstGrantedAt: string;
|
|
176
|
+
/** ISO timestamp of when the grant was last exercised. */
|
|
177
|
+
lastUsedAt: string;
|
|
178
|
+
}
|
|
154
179
|
/** Input accepted by `createApplication`. Staff-only fields are not settable here. */
|
|
155
180
|
export interface CreateApplicationInput {
|
|
156
181
|
name: string;
|
|
@@ -265,6 +290,32 @@ export declare function OxyServicesApplicationsMixin<T extends typeof OxyService
|
|
|
265
290
|
* key). URL-encoded before being placed in the path.
|
|
266
291
|
*/
|
|
267
292
|
getPublicApplication(clientId: string): Promise<PublicApplication>;
|
|
293
|
+
/**
|
|
294
|
+
* List the OAuth-authorized applications the current user has connected —
|
|
295
|
+
* the third-party apps the user granted access to via the consent flow.
|
|
296
|
+
* Each entry is a {@link ConnectedApp} carrying the application's display
|
|
297
|
+
* identity, the granted scopes, and when the grant was first made and last
|
|
298
|
+
* exercised. Requires an authenticated session.
|
|
299
|
+
*
|
|
300
|
+
* Backed by `GET /auth/grants`. The response is briefly cached
|
|
301
|
+
* (identity-scoped); {@link revokeAppGrant} busts that cache so a revoke is
|
|
302
|
+
* reflected on the next read.
|
|
303
|
+
*/
|
|
304
|
+
listConnectedApps(): Promise<ConnectedApp[]>;
|
|
305
|
+
/**
|
|
306
|
+
* Revoke the current user's grant for a connected application, identified by
|
|
307
|
+
* its application `_id` (a {@link ConnectedApp.applicationId}, NOT a
|
|
308
|
+
* credential/client id — keyed by application so the revocation survives
|
|
309
|
+
* credential rotation). After this the application can no longer act on the
|
|
310
|
+
* user's behalf until it is re-authorized.
|
|
311
|
+
*
|
|
312
|
+
* Backed by `DELETE /auth/grants/:applicationId`. On success the cached
|
|
313
|
+
* connected-apps list (`GET:/auth/grants`) is invalidated so the next
|
|
314
|
+
* {@link listConnectedApps} read reflects the removal.
|
|
315
|
+
*
|
|
316
|
+
* @param applicationId - The connected application's Mongo `_id`.
|
|
317
|
+
*/
|
|
318
|
+
revokeAppGrant(applicationId: string): Promise<void>;
|
|
268
319
|
/**
|
|
269
320
|
* List applications the current user is an active member of.
|
|
270
321
|
*
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-Node Methods Mixin (self-sovereign identity layer — Fase 5 user nodes)
|
|
3
|
+
*
|
|
4
|
+
* The client surface for a user's personal data NODE — the decentralised store
|
|
5
|
+
* that holds an authentic copy of their signed-record chain. Commons drives all
|
|
6
|
+
* of this:
|
|
7
|
+
*
|
|
8
|
+
* - {@link OxyServicesNodesMixin.registerNode} registers (or re-registers) a
|
|
9
|
+
* SELF-HOSTED node. Registration is NOT a bespoke endpoint — it is a signed
|
|
10
|
+
* `type:'node'` v2 record (`collection: 'app.oxy.node'`, `rkey: 'self'`,
|
|
11
|
+
* last-writer-wins) published through the EXISTING `POST /identity/records`
|
|
12
|
+
* path; the server verifies it and materializes the operational
|
|
13
|
+
* {@link UserNodeStatus} cache as a side effect, so the registration's
|
|
14
|
+
* authority is the user's own signature, never an Oxy grant.
|
|
15
|
+
* - {@link OxyServicesNodesMixin.getMyNode} reads the caller's cached node
|
|
16
|
+
* status (`GET /nodes/me`) — the fast, stale-but-instant projection plus the
|
|
17
|
+
* live liveness badge Oxy maintains with background probes.
|
|
18
|
+
* - {@link OxyServicesNodesMixin.removeMyNode} revokes the registration
|
|
19
|
+
* (`DELETE /nodes/me`) so the node leaves the DID document and the liveness
|
|
20
|
+
* sweeps.
|
|
21
|
+
* - {@link OxyServicesNodesMixin.provisionManagedVault} asks Oxy to operate a
|
|
22
|
+
* MANAGED vault on the caller's behalf (`POST /nodes/managed`) — the
|
|
23
|
+
* "Create your vault" convenience for non-technical users (Oxy custodial-signs
|
|
24
|
+
* the node record; `managed:true, controller:'oxy'`).
|
|
25
|
+
* - {@link OxyServicesNodesMixin.notifyNodeIngest} sends an unauthenticated
|
|
26
|
+
* HINT (`POST /nodes/ingest/notify/:userId`) that a user's node has new
|
|
27
|
+
* records; the server fully re-verifies before ingesting, so the hint can
|
|
28
|
+
* never inject data.
|
|
29
|
+
*
|
|
30
|
+
* `registerNode` signs on the caller's per-subject hash chain with the on-device
|
|
31
|
+
* identity key (reusing {@link SignatureService.signRecordV2} — the same
|
|
32
|
+
* `ES256K-DER-SHA256` scheme + {@link signedRecordSigningInput} the identity and
|
|
33
|
+
* civic mixins use), so it is NATIVE-ONLY: it throws on web (where `KeyManager`
|
|
34
|
+
* has no key) and when no user is authenticated. Reading the node status,
|
|
35
|
+
* revoking, provisioning a managed vault, and sending an ingest hint are plain
|
|
36
|
+
* authenticated/public requests with no signing.
|
|
37
|
+
*
|
|
38
|
+
* The wire shapes here are API-INTERNAL (the F5 user-node surface is not yet a
|
|
39
|
+
* published `@oxyhq/contracts` schema), so {@link UserNodeStatus} mirrors the
|
|
40
|
+
* server's `serializeNode` projection exactly. Dates cross the wire as ISO
|
|
41
|
+
* strings.
|
|
42
|
+
*/
|
|
43
|
+
import type { OxyServicesBase } from '../OxyServices.base';
|
|
44
|
+
/** How Oxy and the node move records: the node pulls (default), or Oxy pushes. */
|
|
45
|
+
export type UserNodeMode = 'pull' | 'push';
|
|
46
|
+
/**
|
|
47
|
+
* Who operates the node:
|
|
48
|
+
* - `self` — the user self-hosts the node (registered by their own signed
|
|
49
|
+
* `type:'node'` record).
|
|
50
|
+
* - `oxy` — Oxy operates a MANAGED vault on the user's behalf (custodial-signed
|
|
51
|
+
* `type:'node'` record; the `controller:[OXY_DID]` model).
|
|
52
|
+
*/
|
|
53
|
+
export type UserNodeController = 'self' | 'oxy';
|
|
54
|
+
/**
|
|
55
|
+
* Liveness badge of a node, maintained ONLY by Oxy's background probes:
|
|
56
|
+
* - `active` — the last probe reached the node's liveness manifest.
|
|
57
|
+
* - `unreachable` — the last probe failed (DNS/connect/timeout/non-2xx); the
|
|
58
|
+
* cached row is still served, only the badge changes.
|
|
59
|
+
* - `revoked` — the user removed the registration; excluded from the DID
|
|
60
|
+
* document and from liveness sweeps.
|
|
61
|
+
*/
|
|
62
|
+
export type UserNodeLivenessStatus = 'active' | 'unreachable' | 'revoked';
|
|
63
|
+
/**
|
|
64
|
+
* The caller's registered node, as projected by the server's `serializeNode`
|
|
65
|
+
* (`GET /nodes/me`, `POST /nodes/managed`). A denormalised, fast-to-read copy of
|
|
66
|
+
* the authoritative signed `type:'node'` record plus the live liveness state Oxy
|
|
67
|
+
* maintains in the background.
|
|
68
|
+
*
|
|
69
|
+
* `mode` / `managed` / `controller` / `status` are always present (server fields
|
|
70
|
+
* with defaults); the probe/sync fields and `nodeDid` are present only once set.
|
|
71
|
+
* The `Date` fields cross the wire as ISO-8601 strings.
|
|
72
|
+
*/
|
|
73
|
+
export interface UserNodeStatus {
|
|
74
|
+
/** Optional DID the node advertises for itself (informational). */
|
|
75
|
+
nodeDid?: string;
|
|
76
|
+
/** The node's public HTTPS base URL (where its liveness manifest lives). */
|
|
77
|
+
endpoint: string;
|
|
78
|
+
/** The node's secp256k1 public key (hex) — records it signs verify against this. */
|
|
79
|
+
nodePublicKey: string;
|
|
80
|
+
/** Transport direction. `pull` (the node paces its own sync) by default. */
|
|
81
|
+
mode: UserNodeMode;
|
|
82
|
+
/** Whether Oxy operates this node on the user's behalf (managed vault). */
|
|
83
|
+
managed: boolean;
|
|
84
|
+
/** Operator of the node — `self` (user self-hosts) or `oxy` (managed vault). */
|
|
85
|
+
controller: UserNodeController;
|
|
86
|
+
/** Liveness badge — maintained only by background probes, never a read handler. */
|
|
87
|
+
status: UserNodeLivenessStatus;
|
|
88
|
+
/** Last time a probe reached the node successfully (ISO-8601). */
|
|
89
|
+
lastSeenAt?: string;
|
|
90
|
+
/** Last time a probe ran, success or failure (ISO-8601). */
|
|
91
|
+
lastProbeAt?: string;
|
|
92
|
+
/** Human-readable reason the last probe OR ingest failed (cleared on success). */
|
|
93
|
+
lastError?: string;
|
|
94
|
+
/** Last synced chain `seq` for two-way sync (advanced only by the ingest worker). */
|
|
95
|
+
cursor?: number;
|
|
96
|
+
/** Last time the ingest worker ran a pull for this node (ISO-8601). */
|
|
97
|
+
lastSyncedAt?: string;
|
|
98
|
+
/** When the node was first registered (ISO-8601). */
|
|
99
|
+
createdAt: string;
|
|
100
|
+
/** When the node row was last updated (ISO-8601). */
|
|
101
|
+
updatedAt: string;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Input for {@link OxyServicesNodesMixin.registerNode} — the operational facts of
|
|
105
|
+
* the user's self-hosted node that go into the signed `type:'node'` record.
|
|
106
|
+
*/
|
|
107
|
+
export interface RegisterNodeInput {
|
|
108
|
+
/** The node's public HTTPS base URL (where its liveness manifest is served). */
|
|
109
|
+
endpoint: string;
|
|
110
|
+
/** The node's secp256k1 public key (hex) — records the node signs verify against this. */
|
|
111
|
+
nodePublicKey: string;
|
|
112
|
+
/** Transport direction; defaults to `'pull'` when omitted. */
|
|
113
|
+
mode?: UserNodeMode;
|
|
114
|
+
}
|
|
115
|
+
/** Result of {@link OxyServicesNodesMixin.removeMyNode} (`DELETE /nodes/me`). */
|
|
116
|
+
export interface RemoveNodeResult {
|
|
117
|
+
/** `true` when an active registration was flipped to `revoked`. */
|
|
118
|
+
revoked: boolean;
|
|
119
|
+
}
|
|
120
|
+
export declare function OxyServicesNodesMixin<T extends typeof OxyServicesBase>(Base: T): {
|
|
121
|
+
new (...args: any[]): {
|
|
122
|
+
/**
|
|
123
|
+
* Register (or re-register) the caller's SELF-HOSTED personal data node.
|
|
124
|
+
*
|
|
125
|
+
* Builds the `{ endpoint, nodePublicKey, mode }` node record, signs a v2
|
|
126
|
+
* envelope on the caller's own per-subject hash chain (fetching the current
|
|
127
|
+
* chain head first so `seq`/`prev` are never stale), and publishes it through
|
|
128
|
+
* the EXISTING `POST /identity/records` path — which verifies the signature
|
|
129
|
+
* and materializes the operational node cache as a side effect. The signed
|
|
130
|
+
* record (not this call) is the authority; re-registering over-writes the
|
|
131
|
+
* single `self` record (last-writer-wins).
|
|
132
|
+
*
|
|
133
|
+
* NATIVE-ONLY: signs with the on-device identity key (throws on web / when no
|
|
134
|
+
* identity or no authenticated user — the guard fires before any network).
|
|
135
|
+
* `mode` defaults to `'pull'`. After a successful publish the node + `/users/me`
|
|
136
|
+
* GET caches are swept, then the freshly-materialized status is returned.
|
|
137
|
+
*
|
|
138
|
+
* Throws if the chain record stored but the server skipped materialization
|
|
139
|
+
* (e.g. a malformed endpoint the server rejected) — an unexpected state rather
|
|
140
|
+
* than a silent `null`.
|
|
141
|
+
*
|
|
142
|
+
* @param input - The node's endpoint, public key, and optional transport mode.
|
|
143
|
+
*/
|
|
144
|
+
registerNode(input: RegisterNodeInput): Promise<UserNodeStatus>;
|
|
145
|
+
/**
|
|
146
|
+
* Read the caller's registered node status (`GET /nodes/me`), or `null` when
|
|
147
|
+
* the caller has no node. Auth required; short-TTL cached (the liveness badge
|
|
148
|
+
* is background-maintained) and swept after the caller's own
|
|
149
|
+
* register / revoke / managed-provision.
|
|
150
|
+
*/
|
|
151
|
+
getMyNode(): Promise<UserNodeStatus | null>;
|
|
152
|
+
/**
|
|
153
|
+
* Revoke the caller's node registration (`DELETE /nodes/me`). The node flips
|
|
154
|
+
* to `revoked` server-side (leaving the DID document and liveness sweeps).
|
|
155
|
+
* Auth required; the node + `/users/me` GET caches are swept on success.
|
|
156
|
+
*
|
|
157
|
+
* Maps the server's `{ success }` to the SDK's `{ revoked }` semantic.
|
|
158
|
+
*/
|
|
159
|
+
removeMyNode(): Promise<RemoveNodeResult>;
|
|
160
|
+
/**
|
|
161
|
+
* Provision (or refresh) an Oxy-operated MANAGED vault for the caller
|
|
162
|
+
* (`POST /nodes/managed`) — the "Create your vault" convenience for
|
|
163
|
+
* non-technical users. Oxy custodial-signs the node registration onto the
|
|
164
|
+
* caller's chain and returns the materialized node (`managed:true,
|
|
165
|
+
* controller:'oxy'`). Idempotent server-side. Auth required; the owner id is
|
|
166
|
+
* resolved from the session, never the body. The node + `/users/me` GET caches
|
|
167
|
+
* are swept on success.
|
|
168
|
+
*/
|
|
169
|
+
provisionManagedVault(): Promise<UserNodeStatus>;
|
|
170
|
+
/**
|
|
171
|
+
* Send an ingest HINT that a user's node has new records
|
|
172
|
+
* (`POST /nodes/ingest/notify/:userId`). Unauthenticated by design and
|
|
173
|
+
* fire-and-forget on the server (it only schedules a background re-pull of the
|
|
174
|
+
* named user's OWN node, then fully re-verifies — a notify can never inject
|
|
175
|
+
* data), so this resolves once the 202 hint is accepted and returns nothing.
|
|
176
|
+
*
|
|
177
|
+
* @param userId - The user whose node may have new records. URL-encoded.
|
|
178
|
+
*/
|
|
179
|
+
notifyNodeIngest(userId: string): Promise<void>;
|
|
180
|
+
/**
|
|
181
|
+
* Sweep the GET caches a node mutation invalidates: every node read
|
|
182
|
+
* (`GET:/nodes/`) so a re-read reflects the new node / its absence, and
|
|
183
|
+
* `/users/me` because the user's derived DID document embeds an `#oxy-node`
|
|
184
|
+
* service entry that changes on register / revoke / manage. Public rather
|
|
185
|
+
* than `private` because mixins compose into an exported anonymous class
|
|
186
|
+
* where TypeScript cannot represent a private member in the emitted
|
|
187
|
+
* declaration file (TS4094) — mirrors the civic / identity cache sweepers.
|
|
188
|
+
*/
|
|
189
|
+
_sweepNodeCaches(): void;
|
|
190
|
+
httpService: import("../HttpService").HttpService;
|
|
191
|
+
cloudURL: string;
|
|
192
|
+
config: import("../OxyServices.base").OxyConfig;
|
|
193
|
+
__resetTokensForTests(): void;
|
|
194
|
+
makeRequest<T_1>(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", url: string, data?: any, options?: import("../HttpService").RequestOptions): Promise<T_1>;
|
|
195
|
+
getBaseURL(): string;
|
|
196
|
+
getSessionBaseUrl(): string;
|
|
197
|
+
getClient(): import("../HttpService").HttpService;
|
|
198
|
+
createLinkedClient(config: import("../OxyServices.base").OxyConfig): import("..").LinkedHttpClient;
|
|
199
|
+
getMetrics(): {
|
|
200
|
+
totalRequests: number;
|
|
201
|
+
successfulRequests: number;
|
|
202
|
+
failedRequests: number;
|
|
203
|
+
cacheHits: number;
|
|
204
|
+
cacheMisses: number;
|
|
205
|
+
averageResponseTime: number;
|
|
206
|
+
};
|
|
207
|
+
clearCache(): void;
|
|
208
|
+
clearCacheEntry(key: string): void;
|
|
209
|
+
clearCacheByPrefix(prefix: string): number;
|
|
210
|
+
getCacheStats(): {
|
|
211
|
+
size: number;
|
|
212
|
+
hits: number;
|
|
213
|
+
misses: number;
|
|
214
|
+
hitRate: number;
|
|
215
|
+
};
|
|
216
|
+
getCloudURL(): string;
|
|
217
|
+
setTokens(accessToken: string): void;
|
|
218
|
+
clearTokens(): void;
|
|
219
|
+
onTokensChanged(listener: (accessToken: string | null) => void): () => void;
|
|
220
|
+
_cachedUserId: string | null | undefined;
|
|
221
|
+
_cachedAccessToken: string | null;
|
|
222
|
+
getCurrentUserId(): string | null;
|
|
223
|
+
hasValidToken(): boolean;
|
|
224
|
+
getAccessToken(): string | null;
|
|
225
|
+
setActingAs(userId: string | null): void;
|
|
226
|
+
getActingAs(): string | null;
|
|
227
|
+
waitForAuth(timeoutMs?: number): Promise<boolean>;
|
|
228
|
+
withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
|
|
229
|
+
maxRetries?: number;
|
|
230
|
+
retryDelay?: number;
|
|
231
|
+
authTimeoutMs?: number;
|
|
232
|
+
}): Promise<T_1>;
|
|
233
|
+
validate(): Promise<boolean>;
|
|
234
|
+
handleError(error: unknown): Error;
|
|
235
|
+
healthCheck(): Promise<{
|
|
236
|
+
status: string;
|
|
237
|
+
users?: number;
|
|
238
|
+
timestamp?: string;
|
|
239
|
+
[key: string]: any;
|
|
240
|
+
}>;
|
|
241
|
+
};
|
|
242
|
+
} & T;
|
|
@@ -30,6 +30,7 @@ import { OxyServicesManagedAccountsMixin } from './OxyServices.managedAccounts';
|
|
|
30
30
|
import { OxyServicesContactsMixin } from './OxyServices.contacts';
|
|
31
31
|
import { OxyServicesAppDataMixin } from './OxyServices.appData';
|
|
32
32
|
import { OxyServicesCivicMixin } from './OxyServices.civic';
|
|
33
|
+
import { OxyServicesNodesMixin } from './OxyServices.nodes';
|
|
33
34
|
/**
|
|
34
35
|
* Instance shape of every mixin in the pipeline, intersected. The runtime
|
|
35
36
|
* `composeOxyServices()` produces a class whose instances expose all of
|
|
@@ -39,7 +40,7 @@ import { OxyServicesCivicMixin } from './OxyServices.civic';
|
|
|
39
40
|
* If you add a new mixin to `MIXIN_PIPELINE`, add it here too so its methods
|
|
40
41
|
* are visible without a cast.
|
|
41
42
|
*/
|
|
42
|
-
type AllMixinInstances = InstanceType<ReturnType<typeof OxyServicesAuthMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesFedCMMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesSilentAuthMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesRedirectAuthMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesSsoMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUserMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPrivacyMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLanguageMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPaymentMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesReputationMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAssetsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesApplicationsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesWorkspacesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLocationMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAnalyticsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDevicesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesSecurityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesFeaturesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesTopicsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesManagedAccountsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesContactsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAppDataMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesCivicMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUtilityMixin<typeof OxyServicesBase>>>;
|
|
43
|
+
type AllMixinInstances = InstanceType<ReturnType<typeof OxyServicesAuthMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesFedCMMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesSilentAuthMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesRedirectAuthMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesSsoMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUserMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPrivacyMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLanguageMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPaymentMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesReputationMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAssetsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesApplicationsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesWorkspacesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLocationMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAnalyticsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDevicesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesSecurityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesFeaturesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesTopicsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesManagedAccountsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesContactsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAppDataMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesCivicMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesNodesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUtilityMixin<typeof OxyServicesBase>>>;
|
|
43
44
|
/**
|
|
44
45
|
* Constructor type for the fully composed mixin pipeline. Each mixin returns
|
|
45
46
|
* a new constructor that augments its input; reducing across the pipeline
|
|
@@ -79,6 +79,21 @@ export declare function ssoNoSessionKey(origin: string): string;
|
|
|
79
79
|
* centrally) can probe again.
|
|
80
80
|
*/
|
|
81
81
|
export declare function ssoAttemptedKey(origin: string): string;
|
|
82
|
+
/**
|
|
83
|
+
* Per-origin DURABLE "this device/origin has had a signed-in Oxy session
|
|
84
|
+
* before" hint.
|
|
85
|
+
*
|
|
86
|
+
* Unlike every other key in this module — which lives in per-tab
|
|
87
|
+
* `sessionStorage` — this hint is written to DURABLE storage (web
|
|
88
|
+
* `localStorage`; the services provider uses its own `storageKeyPrefix`-scoped
|
|
89
|
+
* key in `@oxyhq/services`). It is set whenever a session is established or
|
|
90
|
+
* restored and survives a session expiring; it is cleared ONLY on an explicit
|
|
91
|
+
* full sign-out. It exists purely to drive {@link allowSsoBounce}: a returning
|
|
92
|
+
* visitor (hint present) whose local session has lapsed still gets ONE terminal
|
|
93
|
+
* `/sso` establish bounce to recover a session that lives only at the central
|
|
94
|
+
* IdP, while a truly first-time anonymous visitor is never force-bounced.
|
|
95
|
+
*/
|
|
96
|
+
export declare function ssoPriorSessionKey(origin: string): string;
|
|
82
97
|
/**
|
|
83
98
|
* Per-origin marker written by the pre-hydration callback bootstrap.
|
|
84
99
|
*
|
|
@@ -159,3 +174,49 @@ export declare function isCentralIdPOrigin(origin: string): boolean;
|
|
|
159
174
|
* `Date.now()`.
|
|
160
175
|
*/
|
|
161
176
|
export declare function guardActive(storage: Pick<Storage, 'getItem'>, origin: string, now?: number): boolean;
|
|
177
|
+
/**
|
|
178
|
+
* Inputs to the smart {@link allowSsoBounce} gate.
|
|
179
|
+
*/
|
|
180
|
+
export interface SsoBounceGate {
|
|
181
|
+
/**
|
|
182
|
+
* Whether this device/origin has had a signed-in Oxy session before (the
|
|
183
|
+
* durable {@link ssoPriorSessionKey} hint). Set whenever a session is
|
|
184
|
+
* established or restored; survives session expiry; cleared only on explicit
|
|
185
|
+
* full sign-out. `true` ⇒ a returning visitor.
|
|
186
|
+
*/
|
|
187
|
+
readonly hasPriorSession: boolean;
|
|
188
|
+
/**
|
|
189
|
+
* Whether a local/stored session was recovered earlier this cold boot. At the
|
|
190
|
+
* terminal bounce gate this is effectively always `false` (an earlier step
|
|
191
|
+
* would have won and short-circuited), but it is part of the contract — "no
|
|
192
|
+
* prior hint AND no local session" — so it is passed explicitly for fidelity
|
|
193
|
+
* and robustness.
|
|
194
|
+
*/
|
|
195
|
+
readonly hasLocalSession: boolean;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Decide whether the terminal `/sso` establish-bounce is ALLOWED for this
|
|
199
|
+
* visitor (the smart `enabled` gate for the `sso-bounce` cold-boot step).
|
|
200
|
+
*
|
|
201
|
+
* The terminal bounce is the ONLY cold-boot step that can recover a session
|
|
202
|
+
* that lives SOLELY at the central IdP — the cross-apex Relying-Party case
|
|
203
|
+
* (e.g. `mention.earth`, a different apex from `oxy.so`) whose device-local
|
|
204
|
+
* session has expired and whose `Domain=oxy.so` refresh cookie never reaches
|
|
205
|
+
* `api.<apex>`. It is also what plants the first-party per-apex `fedcm_session`
|
|
206
|
+
* cookie that the EARLIER `silent-iframe` step later relies on. So it must fire
|
|
207
|
+
* for a RETURNING user, yet it must NOT force a truly first-time anonymous
|
|
208
|
+
* visitor off to the IdP.
|
|
209
|
+
*
|
|
210
|
+
* - ALLOW when there is a prior-signed-in hint OR a local session was
|
|
211
|
+
* recovered this boot (a returning user) — so a central-only cross-domain
|
|
212
|
+
* session recovers via ONE bounce, after which the per-apex cookie is
|
|
213
|
+
* planted and subsequent loads restore silently with no bounce.
|
|
214
|
+
* - else (no hint, no local session) SUPPRESS — a first-time anonymous
|
|
215
|
+
* visitor browses without a forced redirect.
|
|
216
|
+
*
|
|
217
|
+
* This is the smart DEFAULT and the ONLY behaviour: apps never configure it.
|
|
218
|
+
* It is also the GATE DECISION ONLY — callers still apply the per-tab loop
|
|
219
|
+
* guards (`ssoAttemptedKey`, `ssoNoSessionKey`, {@link guardActive}) so an
|
|
220
|
+
* allowed bounce still fires at most once per cold boot.
|
|
221
|
+
*/
|
|
222
|
+
export declare function allowSsoBounce(gate: SsoBounceGate): boolean;
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -98,6 +98,7 @@ export type { ProfileLink, ProfileLinkMetadata } from './utils/profileLinks';
|
|
|
98
98
|
export type {
|
|
99
99
|
Application,
|
|
100
100
|
PublicApplication,
|
|
101
|
+
ConnectedApp,
|
|
101
102
|
ApplicationMember,
|
|
102
103
|
ApplicationCredential,
|
|
103
104
|
ApplicationRole,
|
|
@@ -210,6 +211,7 @@ export type {
|
|
|
210
211
|
IssueCredentialInput,
|
|
211
212
|
RevokeCredentialResult,
|
|
212
213
|
} from './mixins/OxyServices.civic';
|
|
214
|
+
export type { UserNodeStatus, UserNodeMode, UserNodeController, UserNodeLivenessStatus, RegisterNodeInput, RemoveNodeResult } from './mixins/OxyServices.nodes';
|
|
213
215
|
|
|
214
216
|
// ---------------------------------------------------------------------------
|
|
215
217
|
// Auth helpers (token refresh, error normalisation, retry policies)
|
|
@@ -532,13 +534,16 @@ export {
|
|
|
532
534
|
ssoDestKey,
|
|
533
535
|
ssoNoSessionKey,
|
|
534
536
|
ssoAttemptedKey,
|
|
537
|
+
ssoPriorSessionKey,
|
|
535
538
|
ssoCallbackBootstrapKey,
|
|
536
539
|
ssoNavigate,
|
|
537
540
|
getSsoCallbackBootstrapScript,
|
|
538
541
|
buildSsoBounceUrl,
|
|
539
542
|
isCentralIdPOrigin,
|
|
540
543
|
guardActive,
|
|
544
|
+
allowSsoBounce,
|
|
541
545
|
} from './utils/ssoBounce';
|
|
546
|
+
export type { SsoBounceGate } from './utils/ssoBounce';
|
|
542
547
|
|
|
543
548
|
export { runColdBoot } from './utils/coldBoot';
|
|
544
549
|
export type {
|
|
@@ -164,6 +164,32 @@ export interface PublicApplication {
|
|
|
164
164
|
developerName?: string;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* A connected (OAuth-authorized) application from the current user's point of
|
|
169
|
+
* view: an application the user has granted access to via the consent flow.
|
|
170
|
+
*
|
|
171
|
+
* Returned by `GET /auth/grants` and rendered in the user-facing "Connected
|
|
172
|
+
* apps" management surface. Keyed by `applicationId` (the application's Mongo
|
|
173
|
+
* `_id`) rather than a credential/client id, so the grant — and a subsequent
|
|
174
|
+
* {@link OxyServicesApplicationsMixin.revokeAppGrant} — survive credential
|
|
175
|
+
* rotation. This is a display shape: it carries the application's name/logo and
|
|
176
|
+
* the granted scopes, never any membership or credential material.
|
|
177
|
+
*/
|
|
178
|
+
export interface ConnectedApp {
|
|
179
|
+
/** The connected application's Mongo `_id`. Use this to revoke the grant. */
|
|
180
|
+
applicationId: string;
|
|
181
|
+
/** Human-readable application name shown to the user. */
|
|
182
|
+
name: string;
|
|
183
|
+
/** Optional logo URL for the application. */
|
|
184
|
+
logoUrl?: string;
|
|
185
|
+
/** OAuth scopes the user has granted to the application. */
|
|
186
|
+
scopes: string[];
|
|
187
|
+
/** ISO timestamp of when the user first authorized the application. */
|
|
188
|
+
firstGrantedAt: string;
|
|
189
|
+
/** ISO timestamp of when the grant was last exercised. */
|
|
190
|
+
lastUsedAt: string;
|
|
191
|
+
}
|
|
192
|
+
|
|
167
193
|
/** Input accepted by `createApplication`. Staff-only fields are not settable here. */
|
|
168
194
|
export interface CreateApplicationInput {
|
|
169
195
|
name: string;
|
|
@@ -309,6 +335,59 @@ export function OxyServicesApplicationsMixin<T extends typeof OxyServicesBase>(B
|
|
|
309
335
|
}
|
|
310
336
|
}
|
|
311
337
|
|
|
338
|
+
/**
|
|
339
|
+
* List the OAuth-authorized applications the current user has connected —
|
|
340
|
+
* the third-party apps the user granted access to via the consent flow.
|
|
341
|
+
* Each entry is a {@link ConnectedApp} carrying the application's display
|
|
342
|
+
* identity, the granted scopes, and when the grant was first made and last
|
|
343
|
+
* exercised. Requires an authenticated session.
|
|
344
|
+
*
|
|
345
|
+
* Backed by `GET /auth/grants`. The response is briefly cached
|
|
346
|
+
* (identity-scoped); {@link revokeAppGrant} busts that cache so a revoke is
|
|
347
|
+
* reflected on the next read.
|
|
348
|
+
*/
|
|
349
|
+
async listConnectedApps(): Promise<ConnectedApp[]> {
|
|
350
|
+
try {
|
|
351
|
+
return await this.makeRequest<ConnectedApp[]>(
|
|
352
|
+
'GET',
|
|
353
|
+
'/auth/grants',
|
|
354
|
+
undefined,
|
|
355
|
+
{ cache: true, cacheTTL: CACHE_TIMES.SHORT },
|
|
356
|
+
);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
throw this.handleError(error);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Revoke the current user's grant for a connected application, identified by
|
|
364
|
+
* its application `_id` (a {@link ConnectedApp.applicationId}, NOT a
|
|
365
|
+
* credential/client id — keyed by application so the revocation survives
|
|
366
|
+
* credential rotation). After this the application can no longer act on the
|
|
367
|
+
* user's behalf until it is re-authorized.
|
|
368
|
+
*
|
|
369
|
+
* Backed by `DELETE /auth/grants/:applicationId`. On success the cached
|
|
370
|
+
* connected-apps list (`GET:/auth/grants`) is invalidated so the next
|
|
371
|
+
* {@link listConnectedApps} read reflects the removal.
|
|
372
|
+
*
|
|
373
|
+
* @param applicationId - The connected application's Mongo `_id`.
|
|
374
|
+
*/
|
|
375
|
+
async revokeAppGrant(applicationId: string): Promise<void> {
|
|
376
|
+
try {
|
|
377
|
+
await this.makeRequest<{ revoked: boolean }>(
|
|
378
|
+
'DELETE',
|
|
379
|
+
`/auth/grants/${applicationId}`,
|
|
380
|
+
undefined,
|
|
381
|
+
{ cache: false },
|
|
382
|
+
);
|
|
383
|
+
// A revoke removes an entry from the user's connected-apps list; bust
|
|
384
|
+
// the cached `GET /auth/grants` so the next read re-fetches.
|
|
385
|
+
this.clearCacheEntry('GET:/auth/grants');
|
|
386
|
+
} catch (error) {
|
|
387
|
+
throw this.handleError(error);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
312
391
|
/**
|
|
313
392
|
* List applications the current user is an active member of.
|
|
314
393
|
*
|