@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "3.14.0",
3
+ "version": "3.15.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",
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
  *