@oxyhq/core 20.0.0 → 21.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/NOTICE +10 -9
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/boot/sessionColdBoot.js +107 -8
- package/dist/cjs/i18n/locales/en-US.json +19 -2
- package/dist/cjs/i18n/locales/es-ES.json +19 -2
- package/dist/cjs/i18n/locales/locales/en-US.json +19 -2
- package/dist/cjs/i18n/locales/locales/es-ES.json +19 -2
- package/dist/cjs/index.js +50 -16
- package/dist/cjs/mixins/OxyServices.auth.js +27 -3
- package/dist/cjs/mixins/OxyServices.chains.js +73 -0
- package/dist/cjs/mixins/OxyServices.store.js +266 -0
- package/dist/cjs/mixins/OxyServices.utility.js +159 -104
- package/dist/cjs/mixins/index.js +7 -0
- package/dist/cjs/server/rateLimit.js +15 -6
- package/dist/cjs/session/SessionClient.js +361 -1
- package/dist/cjs/session/accountDialogController.js +121 -147
- package/dist/cjs/session/accountSwitchTargets.js +75 -0
- package/dist/cjs/session/deviceDirectory.js +143 -0
- package/dist/cjs/session/deviceSwitcherRows.js +76 -0
- package/dist/cjs/session/projectSessionState.js +8 -1
- package/dist/cjs/session/sharedDeviceCredential.js +247 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/boot/sessionColdBoot.js +107 -8
- package/dist/esm/i18n/locales/en-US.json +19 -2
- package/dist/esm/i18n/locales/es-ES.json +19 -2
- package/dist/esm/i18n/locales/locales/en-US.json +19 -2
- package/dist/esm/i18n/locales/locales/es-ES.json +19 -2
- package/dist/esm/index.js +32 -10
- package/dist/esm/mixins/OxyServices.auth.js +27 -3
- package/dist/esm/mixins/OxyServices.chains.js +70 -0
- package/dist/esm/mixins/OxyServices.store.js +263 -0
- package/dist/esm/mixins/OxyServices.utility.js +159 -104
- package/dist/esm/mixins/index.js +7 -0
- package/dist/esm/server/rateLimit.js +15 -6
- package/dist/esm/session/SessionClient.js +362 -2
- package/dist/esm/session/accountDialogController.js +121 -147
- package/dist/esm/session/accountSwitchTargets.js +71 -0
- package/dist/esm/session/deviceDirectory.js +135 -0
- package/dist/esm/session/deviceSwitcherRows.js +72 -0
- package/dist/esm/session/projectSessionState.js +8 -2
- package/dist/esm/session/sharedDeviceCredential.js +239 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/boot/sessionColdBoot.d.ts +24 -4
- package/dist/types/index.d.ts +15 -3
- package/dist/types/mixins/OxyServices.auth.d.ts +75 -3
- package/dist/types/mixins/OxyServices.chains.d.ts +156 -0
- package/dist/types/mixins/OxyServices.store.d.ts +334 -0
- package/dist/types/mixins/OxyServices.utility.d.ts +31 -8
- package/dist/types/mixins/index.d.ts +3 -1
- package/dist/types/models/session.d.ts +11 -0
- package/dist/types/session/SessionClient.d.ts +202 -1
- package/dist/types/session/accountDialogController.d.ts +76 -64
- package/dist/types/session/accountSwitchTargets.d.ts +64 -0
- package/dist/types/session/deviceDirectory.d.ts +182 -0
- package/dist/types/session/deviceSwitcherRows.d.ts +92 -0
- package/dist/types/session/projectSessionState.d.ts +29 -0
- package/dist/types/session/sharedDeviceCredential.d.ts +202 -0
- package/package.json +3 -3
- package/src/boot/__tests__/sessionColdBoot.sharedDevice.test.ts +325 -0
- package/src/boot/sessionColdBoot.ts +133 -9
- package/src/i18n/locales/en-US.json +19 -2
- package/src/i18n/locales/es-ES.json +19 -2
- package/src/index.ts +105 -18
- package/src/mixins/OxyServices.auth.ts +67 -5
- package/src/mixins/OxyServices.chains.ts +134 -0
- package/src/mixins/OxyServices.store.ts +585 -0
- package/src/mixins/OxyServices.utility.ts +161 -108
- package/src/mixins/__tests__/chains.test.ts +113 -0
- package/src/mixins/__tests__/preSessionSkipAuth.test.ts +54 -1
- package/src/mixins/__tests__/store.test.ts +304 -0
- package/src/mixins/__tests__/userTokenAuth.test.ts +746 -0
- package/src/mixins/index.ts +9 -0
- package/src/models/session.ts +11 -0
- package/src/server/__tests__/rateLimit.test.ts +47 -0
- package/src/server/rateLimit.ts +18 -8
- package/src/session/SessionClient.ts +386 -1
- package/src/session/__tests__/SessionClient.directory.test.ts +688 -0
- package/src/session/__tests__/accountDialogController.test.ts +411 -278
- package/src/session/__tests__/accountSwitchTargets.test.ts +132 -0
- package/src/session/__tests__/deviceDirectory.test.ts +422 -0
- package/src/session/__tests__/deviceSwitcherRows.test.ts +223 -0
- package/src/session/__tests__/projectSessionState.test.ts +17 -0
- package/src/session/__tests__/sharedDeviceCredential.test.ts +300 -0
- package/src/session/accountDialogController.ts +141 -179
- package/src/session/accountSwitchTargets.ts +87 -0
- package/src/session/deviceDirectory.ts +269 -0
- package/src/session/deviceSwitcherRows.ts +145 -0
- package/src/session/projectSessionState.ts +9 -3
- package/src/session/sharedDeviceCredential.ts +349 -0
- package/dist/cjs/session/accountProjection.js +0 -213
- package/dist/esm/session/accountProjection.js +0 -207
- package/dist/types/session/accountProjection.d.ts +0 -198
- package/src/session/__tests__/accountProjection.test.ts +0 -447
- package/src/session/accountProjection.ts +0 -354
|
@@ -15,10 +15,17 @@
|
|
|
15
15
|
* origin persisted a `deviceId` + `deviceSecret`, mint a short access token
|
|
16
16
|
* with a single bearer-less POST to `/session/device/token` (no cookie, no
|
|
17
17
|
* navigation) and rotate the secret in-use.
|
|
18
|
-
* 3. `shared-
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* 3. `shared-device-adopt` (native, ACCOUNT mode) — this app has no credential
|
|
19
|
+
* of its own but a sibling official app already put one in the shared native
|
|
20
|
+
* slot: adopt it and mint. This is how a newly installed official app joins
|
|
21
|
+
* the device's existing session WITHOUT another QR and without ever touching
|
|
22
|
+
* the Commons private key.
|
|
23
|
+
* 4. `shared-key-signin` (native, ACCOUNT mode) — the legacy lane: re-mint by
|
|
24
|
+
* signing with the shared-keychain IDENTITY key. Retained as a recovery /
|
|
25
|
+
* compatibility path for devices whose apps have not yet published a shared
|
|
26
|
+
* device credential — OR `identity-key-signin` (IDENTITY mode) — re-mint
|
|
27
|
+
* from THIS device's primary identity key.
|
|
28
|
+
* 5. Signed out.
|
|
22
29
|
*
|
|
23
30
|
* Two session modes (see {@link RunSessionColdBootOptions.sessionMode}):
|
|
24
31
|
* - `account` (default) — the device's ACTIVE account owns the session. Every
|
|
@@ -33,6 +40,7 @@
|
|
|
33
40
|
*/
|
|
34
41
|
import { type ColdBootOutcome } from '../utils/coldBoot';
|
|
35
42
|
import { type IdentityBinding } from '../session/identitySession';
|
|
43
|
+
import { type SharedDeviceCredentialStore } from '../session/sharedDeviceCredential';
|
|
36
44
|
import type { OxyServices } from '../OxyServices';
|
|
37
45
|
import type { AuthStateStore } from '../session/authStateStore';
|
|
38
46
|
/**
|
|
@@ -108,6 +116,18 @@ export interface RunSessionColdBootOptions {
|
|
|
108
116
|
* `sessionMode: 'identity'`. Ignored in `'account'` mode.
|
|
109
117
|
*/
|
|
110
118
|
identity?: IdentityBinding;
|
|
119
|
+
/**
|
|
120
|
+
* The cross-app native slot holding this device's shared DeviceSession
|
|
121
|
+
* credential, enabling the `shared-device-adopt` lane. Supplied by
|
|
122
|
+
* `@oxyhq/services` on native; absent on web, where each origin is its own
|
|
123
|
+
* device by design.
|
|
124
|
+
*
|
|
125
|
+
* IGNORED in `sessionMode: 'identity'`. The shared slot belongs to whichever
|
|
126
|
+
* principal signed in on this device; an identity-bound client must resolve
|
|
127
|
+
* its session from the local key alone, and adopting a device credential is
|
|
128
|
+
* exactly the drift that mode exists to prevent.
|
|
129
|
+
*/
|
|
130
|
+
sharedDeviceCredential?: SharedDeviceCredentialStore;
|
|
111
131
|
}
|
|
112
132
|
/**
|
|
113
133
|
* Run the device-first cold boot. Resolves to the `runColdBoot` outcome and, as
|
package/dist/types/index.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export { ServiceCredentialMismatchError, } from './mixins/OxyServices.auth';
|
|
|
25
25
|
export { getCommonsApprovalBlockingReason, parseCommonsApprovalExpiresAt, } from './utils/commonsApproval';
|
|
26
26
|
export { selectCommonsDelivery, pushTargetsFromDelivery, commonsDeliveryPlatform } from './utils/commonsDelivery';
|
|
27
27
|
export type { CommonsDeliveryFacts, CommonsDeliveryPlatform, CommonsDeliveryRoute, } from './utils/commonsDelivery';
|
|
28
|
-
export type { ServiceTokenResponse, OAuthUserInfoResponse } from './mixins/OxyServices.auth';
|
|
28
|
+
export type { ServiceTokenResponse, OAuthUserInfoResponse, OAuthTokenExchangeResult, } from './mixins/OxyServices.auth';
|
|
29
29
|
export type { CommonsSignInHandle, CommonsSignInStatus, CommonsSignInPurpose, CommonsOAuthContext, CommonsApprovalInfo, CommonsApprovalSubjectAccount, CommonsSignInActionResult, CommonsOAuthFinalizeResult, CommonsDeliveryResult, } from './mixins/OxyServices.auth';
|
|
30
30
|
export type { PushTokenPlatform, RegisterPushTokenInput, } from './mixins/OxyServices.notifications';
|
|
31
31
|
export type { ServiceApp, ServiceActingAsVerification } from './mixins/OxyServices.utility';
|
|
@@ -39,6 +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 { StoreCategory, StoreRating, StoreListingSummary, StoreListingDetail, StoreScreenshot, StoreScreenshotPlatform, StoreReview, StoreOwnReview, WriteStoreReviewInput, StoreListingStatus, PublisherListing, WriteListingInput, AddScreenshotInput, UpdateScreenshotInput, StorePage, StorePageOptions, StoreReviewsOptions, } from './mixins/OxyServices.store';
|
|
42
43
|
export type { AccountKind, AccountCategoryId, AccountRelationship, AccountRole, AccountMemberStatus, AccountMemberSource, AccountMember, AccountNode, AccountCredentialType, AccountCredentialEnvironment, AccountCredentialStatus, AccountCredential, AccountCredentialWithSecret, RotateAccountCredentialResult, ListAccountsOptions, CreateAccountInput, UpdateAccountInput, ProvisionChannelInput, ProvisionChannelMemberInput, ProvisionChannelResult, InviteAccountMemberInput, UpdateAccountMemberInput, TransferAccountOwnershipInput, CreateAccountCredentialInput, AccountSuccessResult, SwitchAccountResult, Application, ApplicationType, ApplicationStatus, ApplicationCredential, ApplicationCredentialType, ApplicationCredentialStatus, ApplicationEnvironment, CreateApplicationInput, UpdateApplicationInput, CreateApplicationCredentialInput, ApplicationCredentialWithSecret, RotateApplicationCredentialResult, ApplicationUsagePeriod, ApplicationUsageSummary, ApplicationUsageByDay, ApplicationUsageByEndpoint, ApplicationUsageStats, } from './mixins/OxyServices.accounts';
|
|
43
44
|
export { ACCOUNT_CATEGORY_IDS, MAX_ACCOUNT_CATEGORIES, SELECTABLE_ACCOUNT_CATEGORY_IDS, isSelectableAccountCategoryId, kindAcceptsAccountCategories, } from './mixins/OxyServices.accounts';
|
|
44
45
|
export { buildUserDid } from './mixins/OxyServices.identity';
|
|
@@ -46,6 +47,12 @@ export type { IdentityRecordType, UnlinkableAuthMethodType, LinkAuthMethodResult
|
|
|
46
47
|
export { parseIdPayload, parseAttestPayload, verifyPublicCardAttestation, } from './mixins/OxyServices.civic';
|
|
47
48
|
export type { CivicCardResult, IdCardRef, AttestQrPayload, ParsedAttestPayload, SubmitRealLifeAttestationInput, DenyValidationResult, VouchForPersonInput, WithdrawVouchResult, IssueCredentialInput, RevokeCredentialResult, } from './mixins/OxyServices.civic';
|
|
48
49
|
export type { UserNodeStatus, UserNodeMode, UserNodeController, UserNodeLivenessStatus, RegisterNodeInput, RemoveNodeResult } from './mixins/OxyServices.nodes';
|
|
50
|
+
/**
|
|
51
|
+
* Chains — the shared per-person record log. `ChainRecord` is generic over the
|
|
52
|
+
* app's own lexicon payload, so a consumer types its records without Oxy
|
|
53
|
+
* knowing any app's schema.
|
|
54
|
+
*/
|
|
55
|
+
export type { ChainRecord, ChainRecordPage, AppendedChainRecord } from './mixins/OxyServices.chains';
|
|
49
56
|
export { SessionSyncRequiredError, AuthenticationFailedError, ensureValidToken, isAuthenticationError, withAuthErrorHandling, authenticatedApiCall, } from './utils/authHelpers';
|
|
50
57
|
export type { HandleApiErrorOptions } from './utils/authHelpers';
|
|
51
58
|
export { mergeSessions, normalizeAndSortSessions, sessionsArraysEqual, } from './utils/sessionUtils';
|
|
@@ -109,12 +116,17 @@ export type { SocketIOFactory, MinimalSocket } from './session/socketLoader';
|
|
|
109
116
|
export { createSessionClientHost } from './session/sessionClientHost';
|
|
110
117
|
export { createSessionClient } from './session/createSessionClient';
|
|
111
118
|
export { deviceStateToClientSessions, activeSessionIdOf, activeUserOf, accountIdsOf, } from './session/projectSessionState';
|
|
112
|
-
export {
|
|
113
|
-
export type {
|
|
119
|
+
export { canActivateContext, directoryDisplayName, directoryHandle, projectDevicePrincipals, resolveActiveContext, resolveDeviceContext, } from './session/deviceDirectory';
|
|
120
|
+
export type { DeviceContext, DeviceContextActor, DeviceContextSubject, DevicePrincipalGroup, } from './session/deviceDirectory';
|
|
121
|
+
export { buildSwitcherRows, showsPrincipalHeaders } from './session/deviceSwitcherRows';
|
|
122
|
+
export type { ResolveAvatarUrl, SwitcherContextRow, SwitcherPrincipalRow, } from './session/deviceSwitcherRows';
|
|
123
|
+
export { isSwitchTargetAccount, canSwitchIntoAccount, } from './session/accountSwitchTargets';
|
|
114
124
|
export { AccountDialogController, createAccountDialogController, } from './session/accountDialogController';
|
|
115
125
|
export type { AccountDialogControllerOptions, AccountDialogSnapshot, AccountDialogView, CommonsAvailability, PopupWindowHandle, SignInFlowPhase, SignInFlowState, SignInProgress, } from './session/accountDialogController';
|
|
116
126
|
export { createWebAuthStateStore, createNativeAuthStateStore, createMemoryAuthStateStore, AUTH_STATE_STORAGE_KEY, } from './session/authStateStore';
|
|
117
127
|
export type { PersistedAuthState, AuthStateStore, NativeKeyValueStorage, } from './session/authStateStore';
|
|
128
|
+
export { createSharedMirroringAuthStateStore, decideSharedDeviceJoin, decideSharedDevicePublish, normalizeSharedDeviceSessionRead, publishProvenDeviceCredential, readLocalDeviceCredential, } from './session/sharedDeviceCredential';
|
|
129
|
+
export type { SharedDeviceCredential, SharedDeviceCredentialRead, SharedDeviceCredentialStore, SharedDeviceJoinDecision, SharedDeviceJoinSkipReason, SharedDevicePublishDecision, SharedDevicePublishOutcome, SharedDevicePublishSkipReason, } from './session/sharedDeviceCredential';
|
|
118
130
|
export { createWebIdentityPinStore, createNativeIdentityPinStore, createMemoryIdentityPinStore, identityPinMatches, IDENTITY_PIN_STORAGE_KEY, } from './session/identityPin';
|
|
119
131
|
export type { IdentityPin, IdentityPinStore } from './session/identityPin';
|
|
120
132
|
export { resolveIdentityPin, establishIdentitySession, } from './session/identitySession';
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Supports password-based login (email/username) and public key challenge-response.
|
|
5
5
|
*/
|
|
6
6
|
import type { User } from '../models/interfaces';
|
|
7
|
-
import type { LoginResult,
|
|
7
|
+
import type { LoginResult, CommonsDenyReason } from '@oxyhq/contracts';
|
|
8
8
|
import type { SessionLoginResponse } from '../models/session';
|
|
9
9
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
10
10
|
import type { PublicApplication } from './OxyServices.connectedApps';
|
|
@@ -39,6 +39,41 @@ export interface OAuthUserInfoResponse {
|
|
|
39
39
|
name?: string;
|
|
40
40
|
picture?: string;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* The session an OAuth authorization-code exchange yields.
|
|
44
|
+
*
|
|
45
|
+
* Deliberately NOT `LoginSessionResult`. That type mirrors the API's
|
|
46
|
+
* `buildSessionAuthResponse`, which every FIRST-PARTY sign-in lane emits, and it
|
|
47
|
+
* requires `deviceId` because those lanes always join the origin's DeviceSession.
|
|
48
|
+
* `POST /auth/oauth/token` is the RFC 6749 token endpoint and serves third
|
|
49
|
+
* parties, whose grant is deliberately ISOLATED: an untrusted application must be
|
|
50
|
+
* able to receive a session carrying NO DeviceSession credential at all.
|
|
51
|
+
*
|
|
52
|
+
* Both device fields are therefore optional here, and a response omitting them is
|
|
53
|
+
* a well-formed device-less grant rather than a malformed payload. What that
|
|
54
|
+
* costs the session is spelled out on `exchangeOAuthCode` below.
|
|
55
|
+
*/
|
|
56
|
+
export interface OAuthTokenExchangeResult {
|
|
57
|
+
sessionId: string;
|
|
58
|
+
/** ISO-8601 expiry of {@link accessToken}, derived from RFC 6749 `expires_in`. */
|
|
59
|
+
expiresAt: string;
|
|
60
|
+
accessToken?: string;
|
|
61
|
+
/**
|
|
62
|
+
* The DeviceSession this grant joined, when the server issued one. ABSENT for
|
|
63
|
+
* an isolated third-party grant — never assume a string.
|
|
64
|
+
*/
|
|
65
|
+
deviceId?: string;
|
|
66
|
+
/**
|
|
67
|
+
* The zero-cookie mint credential for {@link deviceId}. Present only alongside
|
|
68
|
+
* it; absent for an isolated third-party grant.
|
|
69
|
+
*/
|
|
70
|
+
deviceSecret?: string;
|
|
71
|
+
user: {
|
|
72
|
+
id: string;
|
|
73
|
+
username?: string;
|
|
74
|
+
avatar?: string;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
42
77
|
/**
|
|
43
78
|
* How a "Sign in with Oxy" request finalizes once the approver authorizes it.
|
|
44
79
|
*
|
|
@@ -793,13 +828,30 @@ export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(B
|
|
|
793
828
|
* response this method used before were an Oxy invention no OAuth library
|
|
794
829
|
* could interoperate with; the endpoint no longer accepts them. The method's
|
|
795
830
|
* OWN signature is unchanged, so callers are unaffected.
|
|
831
|
+
*
|
|
832
|
+
* `deviceId` + `deviceSecret` are OPTIONAL and their absence is a valid
|
|
833
|
+
* outcome, not an error. A third-party grant is meant to be isolated from the
|
|
834
|
+
* browser's shared DeviceSession, so the token endpoint must be free to return
|
|
835
|
+
* no device credential at all — the guard that used to require the pair made
|
|
836
|
+
* that omission unshippable, since it turned every third-party sign-in through
|
|
837
|
+
* the SDK into a silent `exchange-failed`.
|
|
838
|
+
*
|
|
839
|
+
* The cost is real and deliberate: a DEVICE-LESS session cannot use the
|
|
840
|
+
* zero-cookie mint lane (`POST /session/device/token`), because that lane's
|
|
841
|
+
* whole proof is possession of a `deviceSecret`. Its lifetime is therefore the
|
|
842
|
+
* access token itself — nothing persists a restore credential, the cold boot's
|
|
843
|
+
* `device-secret-mint` step reports `no-secret` and skips, and the refresh
|
|
844
|
+
* scheduler has nothing to re-mint from. When the token expires the session
|
|
845
|
+
* ends LOUDLY: the 401 lane clears the tokens and the provider resolves signed
|
|
846
|
+
* out, so the app can run the OAuth flow again. It never degrades into a
|
|
847
|
+
* session that looks alive and cannot refresh.
|
|
796
848
|
*/
|
|
797
849
|
exchangeOAuthCode(params: {
|
|
798
850
|
code: string;
|
|
799
851
|
clientId: string;
|
|
800
852
|
redirectUri: string;
|
|
801
853
|
codeVerifier: string;
|
|
802
|
-
}): Promise<
|
|
854
|
+
}): Promise<OAuthTokenExchangeResult>;
|
|
803
855
|
/**
|
|
804
856
|
* Fetch OpenID Connect userinfo for the current bearer (`GET /auth/oauth/userinfo`).
|
|
805
857
|
* The response is a flat JSON document — no `{ data }` wrapper.
|
|
@@ -850,7 +902,27 @@ export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(B
|
|
|
850
902
|
handleError(error: unknown): Error;
|
|
851
903
|
healthCheck(): Promise<{
|
|
852
904
|
status: string;
|
|
853
|
-
users
|
|
905
|
+
users
|
|
906
|
+
/**
|
|
907
|
+
* @internal Narrow an untrusted delivery-progress timestamp from the status
|
|
908
|
+
* response.
|
|
909
|
+
*
|
|
910
|
+
* Returns the ISO-8601 string unchanged when it is a real, parseable instant,
|
|
911
|
+
* and `null` for everything else — absent (an older API that has no delivery
|
|
912
|
+
* progress at all), empty, non-string, or unparseable. Progress is advisory, so
|
|
913
|
+
* degrading to "no progress yet" is always safe; surfacing a garbage timestamp
|
|
914
|
+
* to the waiting UI is not.
|
|
915
|
+
*/
|
|
916
|
+
? /**
|
|
917
|
+
* @internal Narrow an untrusted delivery-progress timestamp from the status
|
|
918
|
+
* response.
|
|
919
|
+
*
|
|
920
|
+
* Returns the ISO-8601 string unchanged when it is a real, parseable instant,
|
|
921
|
+
* and `null` for everything else — absent (an older API that has no delivery
|
|
922
|
+
* progress at all), empty, non-string, or unparseable. Progress is advisory, so
|
|
923
|
+
* degrading to "no progress yet" is always safe; surfacing a garbage timestamp
|
|
924
|
+
* to the waiting UI is not.
|
|
925
|
+
*/: number;
|
|
854
926
|
timestamp?: string;
|
|
855
927
|
[key: string]: any;
|
|
856
928
|
}>;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chains — the shared record log every Oxy app reads and writes.
|
|
3
|
+
*
|
|
4
|
+
* A person has ONE chain. An app appends its own records to it and projects its
|
|
5
|
+
* feeds from what it reads back, instead of keeping a private copy of the same
|
|
6
|
+
* person's activity. This mixin is the client half of `/chains` in oxy-api, and
|
|
7
|
+
* it exists so that adopting the chain costs an app no HTTP of its own — the
|
|
8
|
+
* whole point of the shared substrate is that the second app writes less code
|
|
9
|
+
* than the first, not the same amount in a different file.
|
|
10
|
+
*
|
|
11
|
+
* ## Both calls are SERVICE-authenticated
|
|
12
|
+
*
|
|
13
|
+
* They go through `makeServiceRequest`, so they only work on a backend that has
|
|
14
|
+
* called `configureServiceAuth()`. That is not an accident of implementation: an
|
|
15
|
+
* append writes to someone else's chain and a read spans many subjects, so
|
|
16
|
+
* neither belongs in a browser holding a user session. A frontend that needs
|
|
17
|
+
* this asks its own backend.
|
|
18
|
+
*
|
|
19
|
+
* The authority is checked server-side and cannot be talked out of from here:
|
|
20
|
+
* `chains:write` plus the application's own `chainNamespaces` for an append,
|
|
21
|
+
* `chains:read` plus the public-collection policy for a read. A call that
|
|
22
|
+
* violates either gets a 403 or an empty page — this client adds no
|
|
23
|
+
* pre-validation that could drift from the server's answer.
|
|
24
|
+
*/
|
|
25
|
+
import type { OxyServicesBase } from '../OxyServices.base';
|
|
26
|
+
/** A signed record as it comes back from a read. */
|
|
27
|
+
export interface ChainRecord<TRecord = Record<string, unknown>> {
|
|
28
|
+
recordId: string;
|
|
29
|
+
/** The subject whose chain it is — the person the record is about. */
|
|
30
|
+
oxyUserId: string;
|
|
31
|
+
/** The lexicon NSID, e.g. `app.mention.feed.post`. */
|
|
32
|
+
collection: string;
|
|
33
|
+
envelope: {
|
|
34
|
+
version: number;
|
|
35
|
+
type: string;
|
|
36
|
+
subject: string;
|
|
37
|
+
issuer: string;
|
|
38
|
+
record: TRecord;
|
|
39
|
+
issuedAt: number;
|
|
40
|
+
seq?: number;
|
|
41
|
+
prev?: string | null;
|
|
42
|
+
collection?: string;
|
|
43
|
+
rkey?: string;
|
|
44
|
+
publicKey: string;
|
|
45
|
+
alg: string;
|
|
46
|
+
signature: string;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** One page of a multi-subject read. */
|
|
50
|
+
export interface ChainRecordPage<TRecord = Record<string, unknown>> {
|
|
51
|
+
records: ChainRecord<TRecord>[];
|
|
52
|
+
/**
|
|
53
|
+
* Opaque. Hand it back as `since` to continue; `null` at the end of the
|
|
54
|
+
* stream as of this snapshot. Never construct one.
|
|
55
|
+
*/
|
|
56
|
+
nextCursor: string | null;
|
|
57
|
+
}
|
|
58
|
+
/** What an append returns once the record is on the chain. */
|
|
59
|
+
export interface AppendedChainRecord {
|
|
60
|
+
recordId: string;
|
|
61
|
+
seq: number;
|
|
62
|
+
envelope: ChainRecord['envelope'];
|
|
63
|
+
verified: boolean;
|
|
64
|
+
}
|
|
65
|
+
export declare function OxyServicesChainsMixin<T extends typeof OxyServicesBase>(Base: T): {
|
|
66
|
+
new (...args: any[]): {
|
|
67
|
+
/** Service-token request, implemented by the auth mixin earlier in the pipeline. */
|
|
68
|
+
makeServiceRequest: <R = unknown>(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", url: string, data?: unknown, userId?: string) => Promise<R>;
|
|
69
|
+
/**
|
|
70
|
+
* Append a record to `oxyUserId`'s chain under `collection`/`rkey`.
|
|
71
|
+
*
|
|
72
|
+
* Oxy issues and signs it; the calling app never holds a chain signing key.
|
|
73
|
+
* `rkey` is the app's own id for the thing — reusing it later supersedes the
|
|
74
|
+
* earlier record for that key, which is how an edit works.
|
|
75
|
+
*
|
|
76
|
+
* Requires the `chains:write` scope AND `collection` falling under one of
|
|
77
|
+
* this application's granted `chainNamespaces`. Both are enforced by the
|
|
78
|
+
* server; a violation throws with a 403.
|
|
79
|
+
*/
|
|
80
|
+
appendChainRecord(params: {
|
|
81
|
+
oxyUserId: string;
|
|
82
|
+
collection: string;
|
|
83
|
+
rkey: string;
|
|
84
|
+
record: Record<string, unknown>;
|
|
85
|
+
}): Promise<AppendedChainRecord>;
|
|
86
|
+
/**
|
|
87
|
+
* Records published by any of `oxyUserIds` under any of `collections`,
|
|
88
|
+
* oldest first — the read a cross-app feed is projected from.
|
|
89
|
+
*
|
|
90
|
+
* Only collections Oxy declares PUBLIC come back, whatever is asked for; a
|
|
91
|
+
* private one yields nothing rather than an error.
|
|
92
|
+
*
|
|
93
|
+
* **Re-poll from slightly BEFORE your last cursor and dedupe by
|
|
94
|
+
* `recordId`.** The chain's pagination axis is a transaction-start
|
|
95
|
+
* timestamp, so a record can commit behind a cursor that already passed it.
|
|
96
|
+
* Re-delivering one costs bytes; skipping one costs a record that never
|
|
97
|
+
* appears. Projections are expected to be idempotent for exactly this
|
|
98
|
+
* reason.
|
|
99
|
+
*/
|
|
100
|
+
readChainRecords<TRecord = Record<string, unknown>>(params: {
|
|
101
|
+
oxyUserIds: readonly string[];
|
|
102
|
+
collections: readonly string[];
|
|
103
|
+
since?: string | null;
|
|
104
|
+
limit?: number;
|
|
105
|
+
}): Promise<ChainRecordPage<TRecord>>;
|
|
106
|
+
httpService: import("../HttpService").HttpService;
|
|
107
|
+
cloudURL: string;
|
|
108
|
+
config: import("../OxyServices.base").OxyConfig;
|
|
109
|
+
__resetTokensForTests(): void;
|
|
110
|
+
makeRequest<T_1>(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", url: string, data?: any, options?: import("../HttpService").RequestOptions): Promise<T_1>;
|
|
111
|
+
getBaseURL(): string;
|
|
112
|
+
getClient(): import("../HttpService").HttpService;
|
|
113
|
+
createLinkedClient(config: import("../OxyServices.base").OxyConfig): import("..").LinkedHttpClient;
|
|
114
|
+
getMetrics(): {
|
|
115
|
+
totalRequests: number;
|
|
116
|
+
successfulRequests: number;
|
|
117
|
+
failedRequests: number;
|
|
118
|
+
cacheHits: number;
|
|
119
|
+
cacheMisses: number;
|
|
120
|
+
averageResponseTime: number;
|
|
121
|
+
};
|
|
122
|
+
clearCache(): void;
|
|
123
|
+
clearCacheEntry(key: string): void;
|
|
124
|
+
clearCacheByPrefix(prefix: string): number;
|
|
125
|
+
getCacheStats(): {
|
|
126
|
+
size: number;
|
|
127
|
+
hits: number;
|
|
128
|
+
misses: number;
|
|
129
|
+
hitRate: number;
|
|
130
|
+
};
|
|
131
|
+
getCloudURL(): string;
|
|
132
|
+
setTokens(accessToken: string): void;
|
|
133
|
+
clearTokens(): void;
|
|
134
|
+
onTokensChanged(listener: (accessToken: string | null) => void): () => void;
|
|
135
|
+
_cachedUserId: string | null | undefined;
|
|
136
|
+
_cachedAccessToken: string | null;
|
|
137
|
+
getCurrentUserId(): string | null;
|
|
138
|
+
hasValidToken(): boolean;
|
|
139
|
+
getAccessToken(): string | null;
|
|
140
|
+
getAccessTokenExpiry(): number | null;
|
|
141
|
+
waitForAuth(timeoutMs?: number): Promise<boolean>;
|
|
142
|
+
withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
|
|
143
|
+
maxRetries?: number;
|
|
144
|
+
retryDelay?: number;
|
|
145
|
+
authTimeoutMs?: number;
|
|
146
|
+
}): Promise<T_1>;
|
|
147
|
+
validate(): Promise<boolean>;
|
|
148
|
+
handleError(error: unknown): Error;
|
|
149
|
+
healthCheck(): Promise<{
|
|
150
|
+
status: string;
|
|
151
|
+
users?: number;
|
|
152
|
+
timestamp?: string;
|
|
153
|
+
[key: string]: any;
|
|
154
|
+
}>;
|
|
155
|
+
};
|
|
156
|
+
} & T;
|