@oxyhq/core 12.10.6 → 12.11.1
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/HttpService.js +66 -6
- package/dist/cjs/boot/sessionColdBoot.js +142 -37
- package/dist/cjs/i18n/locales/ar-SA.json +13 -1
- package/dist/cjs/i18n/locales/ca-ES.json +13 -1
- package/dist/cjs/i18n/locales/de-DE.json +13 -1
- package/dist/cjs/i18n/locales/en-US.json +13 -4
- package/dist/cjs/i18n/locales/es-ES.json +13 -4
- package/dist/cjs/i18n/locales/fr-FR.json +13 -1
- package/dist/cjs/i18n/locales/it-IT.json +13 -1
- package/dist/cjs/i18n/locales/ja-JP.json +13 -1
- package/dist/cjs/i18n/locales/ko-KR.json +13 -1
- package/dist/cjs/i18n/locales/locales/ar-SA.json +13 -1
- package/dist/cjs/i18n/locales/locales/ca-ES.json +13 -1
- package/dist/cjs/i18n/locales/locales/de-DE.json +13 -1
- package/dist/cjs/i18n/locales/locales/en-US.json +13 -4
- package/dist/cjs/i18n/locales/locales/es-ES.json +13 -4
- package/dist/cjs/i18n/locales/locales/fr-FR.json +13 -1
- package/dist/cjs/i18n/locales/locales/it-IT.json +13 -1
- package/dist/cjs/i18n/locales/locales/ja-JP.json +13 -1
- package/dist/cjs/i18n/locales/locales/ko-KR.json +13 -1
- package/dist/cjs/i18n/locales/locales/pt-PT.json +13 -1
- package/dist/cjs/i18n/locales/locales/zh-CN.json +13 -1
- package/dist/cjs/i18n/locales/pt-PT.json +13 -1
- package/dist/cjs/i18n/locales/zh-CN.json +13 -1
- package/dist/cjs/index.js +31 -5
- package/dist/cjs/mixins/OxyServices.auth.js +257 -17
- package/dist/cjs/mixins/OxyServices.deviceBoot.js +54 -3
- package/dist/cjs/mixins/OxyServices.notifications.js +63 -0
- package/dist/cjs/mixins/OxyServices.privacy.js +6 -0
- package/dist/cjs/mixins/OxyServices.user.js +1 -0
- package/dist/cjs/mixins/index.js +5 -0
- package/dist/cjs/session/SessionClient.js +40 -4
- package/dist/cjs/session/accountDialogController.js +288 -41
- package/dist/cjs/session/identityPin.js +256 -0
- package/dist/cjs/session/identitySession.js +135 -0
- package/dist/cjs/session/projectSessionState.js +54 -15
- package/dist/cjs/session/refresh.js +73 -8
- package/dist/cjs/utils/commonsDelivery.js +59 -0
- package/dist/cjs/utils/oauthPkce.js +33 -3
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +66 -6
- package/dist/esm/boot/sessionColdBoot.js +142 -37
- package/dist/esm/i18n/locales/ar-SA.json +13 -1
- package/dist/esm/i18n/locales/ca-ES.json +13 -1
- package/dist/esm/i18n/locales/de-DE.json +13 -1
- package/dist/esm/i18n/locales/en-US.json +13 -4
- package/dist/esm/i18n/locales/es-ES.json +13 -4
- package/dist/esm/i18n/locales/fr-FR.json +13 -1
- package/dist/esm/i18n/locales/it-IT.json +13 -1
- package/dist/esm/i18n/locales/ja-JP.json +13 -1
- package/dist/esm/i18n/locales/ko-KR.json +13 -1
- package/dist/esm/i18n/locales/locales/ar-SA.json +13 -1
- package/dist/esm/i18n/locales/locales/ca-ES.json +13 -1
- package/dist/esm/i18n/locales/locales/de-DE.json +13 -1
- package/dist/esm/i18n/locales/locales/en-US.json +13 -4
- package/dist/esm/i18n/locales/locales/es-ES.json +13 -4
- package/dist/esm/i18n/locales/locales/fr-FR.json +13 -1
- package/dist/esm/i18n/locales/locales/it-IT.json +13 -1
- package/dist/esm/i18n/locales/locales/ja-JP.json +13 -1
- package/dist/esm/i18n/locales/locales/ko-KR.json +13 -1
- package/dist/esm/i18n/locales/locales/pt-PT.json +13 -1
- package/dist/esm/i18n/locales/locales/zh-CN.json +13 -1
- package/dist/esm/i18n/locales/pt-PT.json +13 -1
- package/dist/esm/i18n/locales/zh-CN.json +13 -1
- package/dist/esm/index.js +14 -1
- package/dist/esm/mixins/OxyServices.auth.js +257 -17
- package/dist/esm/mixins/OxyServices.deviceBoot.js +52 -3
- package/dist/esm/mixins/OxyServices.notifications.js +60 -0
- package/dist/esm/mixins/OxyServices.privacy.js +6 -0
- package/dist/esm/mixins/OxyServices.user.js +1 -0
- package/dist/esm/mixins/index.js +5 -0
- package/dist/esm/session/SessionClient.js +40 -4
- package/dist/esm/session/accountDialogController.js +288 -41
- package/dist/esm/session/identityPin.js +249 -0
- package/dist/esm/session/identitySession.js +131 -0
- package/dist/esm/session/projectSessionState.js +54 -15
- package/dist/esm/session/refresh.js +73 -8
- package/dist/esm/utils/commonsDelivery.js +54 -0
- package/dist/esm/utils/oauthPkce.js +31 -2
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +18 -1
- package/dist/types/boot/sessionColdBoot.d.ts +38 -1
- package/dist/types/index.d.ts +12 -4
- package/dist/types/mixins/OxyServices.auth.d.ts +262 -11
- package/dist/types/mixins/OxyServices.deviceBoot.d.ts +31 -1
- package/dist/types/mixins/OxyServices.notifications.d.ts +143 -0
- package/dist/types/mixins/OxyServices.user.d.ts +2 -0
- package/dist/types/mixins/index.d.ts +2 -1
- package/dist/types/session/SessionClient.d.ts +28 -0
- package/dist/types/session/accountDialogController.d.ts +187 -9
- package/dist/types/session/identityPin.d.ts +87 -0
- package/dist/types/session/identitySession.d.ts +105 -0
- package/dist/types/session/projectSessionState.d.ts +21 -19
- package/dist/types/session/refresh.d.ts +32 -1
- package/dist/types/utils/commonsDelivery.d.ts +105 -0
- package/dist/types/utils/oauthPkce.d.ts +21 -1
- package/package.json +3 -3
- package/src/HttpService.ts +67 -6
- package/src/__tests__/inSessionRefresh.test.ts +67 -0
- package/src/boot/__tests__/sessionColdBoot.identity.test.ts +393 -0
- package/src/boot/sessionColdBoot.ts +183 -37
- package/src/i18n/locales/ar-SA.json +13 -1
- package/src/i18n/locales/ca-ES.json +13 -1
- package/src/i18n/locales/de-DE.json +13 -1
- package/src/i18n/locales/en-US.json +13 -4
- package/src/i18n/locales/es-ES.json +13 -4
- package/src/i18n/locales/fr-FR.json +13 -1
- package/src/i18n/locales/it-IT.json +13 -1
- package/src/i18n/locales/ja-JP.json +13 -1
- package/src/i18n/locales/ko-KR.json +13 -1
- package/src/i18n/locales/pt-PT.json +13 -1
- package/src/i18n/locales/zh-CN.json +13 -1
- package/src/index.ts +49 -0
- package/src/mixins/OxyServices.auth.ts +464 -24
- package/src/mixins/OxyServices.deviceBoot.ts +54 -2
- package/src/mixins/OxyServices.notifications.ts +142 -0
- package/src/mixins/OxyServices.privacy.ts +6 -0
- package/src/mixins/OxyServices.user.ts +3 -0
- package/src/mixins/__tests__/commonsSignIn.test.ts +754 -2
- package/src/mixins/__tests__/deviceTokenMintPinned.test.ts +113 -0
- package/src/mixins/__tests__/preSessionSkipAuth.test.ts +18 -0
- package/src/mixins/__tests__/privacyCacheInvalidation.test.ts +2 -0
- package/src/mixins/index.ts +6 -0
- package/src/session/SessionClient.ts +57 -4
- package/src/session/__tests__/SessionClient.identityPin.test.ts +175 -0
- package/src/session/__tests__/accountDialogController.test.ts +812 -1
- package/src/session/__tests__/identityPin.test.ts +131 -0
- package/src/session/__tests__/identitySession.test.ts +194 -0
- package/src/session/__tests__/projectSessionState.test.ts +88 -0
- package/src/session/__tests__/refresh.identityPin.test.ts +306 -0
- package/src/session/accountDialogController.ts +435 -44
- package/src/session/identityPin.ts +310 -0
- package/src/session/identitySession.ts +206 -0
- package/src/session/projectSessionState.ts +61 -13
- package/src/session/refresh.ts +103 -9
- package/src/utils/__tests__/oauthPkce.test.ts +51 -0
- package/src/utils/commonsDelivery.ts +132 -0
- package/src/utils/oauthPkce.ts +56 -3
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Push Notification Registration Mixin
|
|
3
|
+
*
|
|
4
|
+
* Registers this installation's push token with Oxy so the platform can reach
|
|
5
|
+
* the signed-in identity — most importantly to deliver a "Sign in with Oxy"
|
|
6
|
+
* approval request to a known Commons installation (issue #691, Phase 4)
|
|
7
|
+
* instead of forcing the user onto a QR.
|
|
8
|
+
*
|
|
9
|
+
* This lives in `@oxyhq/core` because push registration is cross-cutting: every
|
|
10
|
+
* Oxy app that wants to receive platform notifications performs the exact same
|
|
11
|
+
* bearer-authenticated register/unregister pair. App-local copies drift (see the
|
|
12
|
+
* token-kind hazard below), so there is ONE implementation here.
|
|
13
|
+
*
|
|
14
|
+
* ## The token is an EXPO PUSH TOKEN
|
|
15
|
+
*
|
|
16
|
+
* `registerPushToken` takes the token returned by
|
|
17
|
+
* `Notifications.getExpoPushTokenAsync()` — the `ExponentPushToken[…]` handle —
|
|
18
|
+
* because the server delivers through Expo's push service, which only accepts
|
|
19
|
+
* that form.
|
|
20
|
+
*
|
|
21
|
+
* It does NOT take `getDevicePushTokenAsync()`'s raw APNs/FCM device token.
|
|
22
|
+
* Registering one of those looks completely successful (the row is stored, the
|
|
23
|
+
* endpoint returns 200) and then every push silently fails at delivery time,
|
|
24
|
+
* which is exactly the failure mode this surface is written to make impossible:
|
|
25
|
+
* the token is validated here, at the call site, so a raw device token is
|
|
26
|
+
* rejected immediately with an explicit error rather than becoming a dead
|
|
27
|
+
* registration nobody notices.
|
|
28
|
+
*
|
|
29
|
+
* Acquiring the token (permission prompt, `expo-notifications`) is the app's
|
|
30
|
+
* job — `@oxyhq/core` never imports an `expo-*` module.
|
|
31
|
+
*/
|
|
32
|
+
import type { OxyServicesBase } from '../OxyServices.base';
|
|
33
|
+
/**
|
|
34
|
+
* Platform the push token was minted on. Mirrors the enum the server accepts;
|
|
35
|
+
* a value outside it is rejected server-side.
|
|
36
|
+
*/
|
|
37
|
+
export type PushTokenPlatform = 'ios' | 'android' | 'web';
|
|
38
|
+
/** Registration input for {@link OxyServicesNotificationsMixin.registerPushToken}. */
|
|
39
|
+
export interface RegisterPushTokenInput {
|
|
40
|
+
/**
|
|
41
|
+
* The **Expo push token** for this installation — `ExponentPushToken[…]`,
|
|
42
|
+
* from `Notifications.getExpoPushTokenAsync()`.
|
|
43
|
+
*
|
|
44
|
+
* NOT the raw APNs/FCM token from `getDevicePushTokenAsync()`; that form is
|
|
45
|
+
* rejected before any request is sent.
|
|
46
|
+
*/
|
|
47
|
+
expoPushToken: string;
|
|
48
|
+
/** Platform this token was minted on. */
|
|
49
|
+
platform: PushTokenPlatform;
|
|
50
|
+
/**
|
|
51
|
+
* Optional device-first `deviceId` this installation is registered under, so
|
|
52
|
+
* the server can retire the token when that device session goes away instead
|
|
53
|
+
* of pushing to an installation that has signed out.
|
|
54
|
+
*/
|
|
55
|
+
deviceId?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Optional registered OAuth client id (the caller's credential `publicKey`,
|
|
58
|
+
* `oxy_dk_…`) identifying WHICH application this installation is. The server
|
|
59
|
+
* resolves it to the registering `Application` and rejects an unusable one.
|
|
60
|
+
*
|
|
61
|
+
* This is what makes "deliver the approval request to a known Commons
|
|
62
|
+
* installation" possible: delivery targets the Commons application's
|
|
63
|
+
* registrations, never every push token the identity owns.
|
|
64
|
+
*/
|
|
65
|
+
clientId?: string;
|
|
66
|
+
}
|
|
67
|
+
export declare function OxyServicesNotificationsMixin<T extends typeof OxyServicesBase>(Base: T): {
|
|
68
|
+
new (...args: any[]): {
|
|
69
|
+
/**
|
|
70
|
+
* Register this installation's **Expo** push token for the authenticated
|
|
71
|
+
* identity (`POST /notifications/push-token`, bearer required).
|
|
72
|
+
*
|
|
73
|
+
* Idempotent server-side: re-registering the same token for the same
|
|
74
|
+
* identity refreshes the existing row rather than creating a duplicate, so
|
|
75
|
+
* callers can safely re-register on every cold boot.
|
|
76
|
+
*
|
|
77
|
+
* @throws When `expoPushToken` is not an Expo push token — a raw APNs/FCM
|
|
78
|
+
* device token fails here rather than becoming a silently undeliverable
|
|
79
|
+
* registration.
|
|
80
|
+
*/
|
|
81
|
+
registerPushToken(input: RegisterPushTokenInput): Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* Retire an **Expo** push token for the authenticated identity
|
|
84
|
+
* (`DELETE /notifications/push-token`, bearer required).
|
|
85
|
+
*
|
|
86
|
+
* Call it when the user turns notifications off, when the vault identity is
|
|
87
|
+
* replaced, and on sign-out — otherwise the installation keeps receiving
|
|
88
|
+
* approval requests for an identity it no longer holds.
|
|
89
|
+
*
|
|
90
|
+
* @param expoPushToken - The exact token previously registered.
|
|
91
|
+
*/
|
|
92
|
+
unregisterPushToken(expoPushToken: string): Promise<void>;
|
|
93
|
+
httpService: import("../HttpService").HttpService;
|
|
94
|
+
cloudURL: string;
|
|
95
|
+
config: import("../OxyServices.base").OxyConfig;
|
|
96
|
+
__resetTokensForTests(): void;
|
|
97
|
+
makeRequest<T_1>(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", url: string, data?: any, options?: import("../HttpService").RequestOptions): Promise<T_1>;
|
|
98
|
+
getBaseURL(): string;
|
|
99
|
+
getClient(): import("../HttpService").HttpService;
|
|
100
|
+
createLinkedClient(config: import("../OxyServices.base").OxyConfig): import("..").LinkedHttpClient;
|
|
101
|
+
getMetrics(): {
|
|
102
|
+
totalRequests: number;
|
|
103
|
+
successfulRequests: number;
|
|
104
|
+
failedRequests: number;
|
|
105
|
+
cacheHits: number;
|
|
106
|
+
cacheMisses: number;
|
|
107
|
+
averageResponseTime: number;
|
|
108
|
+
};
|
|
109
|
+
clearCache(): void;
|
|
110
|
+
clearCacheEntry(key: string): void;
|
|
111
|
+
clearCacheByPrefix(prefix: string): number;
|
|
112
|
+
getCacheStats(): {
|
|
113
|
+
size: number;
|
|
114
|
+
hits: number;
|
|
115
|
+
misses: number;
|
|
116
|
+
hitRate: number;
|
|
117
|
+
};
|
|
118
|
+
getCloudURL(): string;
|
|
119
|
+
setTokens(accessToken: string): void;
|
|
120
|
+
clearTokens(): void;
|
|
121
|
+
onTokensChanged(listener: (accessToken: string | null) => void): () => void;
|
|
122
|
+
_cachedUserId: string | null | undefined;
|
|
123
|
+
_cachedAccessToken: string | null;
|
|
124
|
+
getCurrentUserId(): string | null;
|
|
125
|
+
hasValidToken(): boolean;
|
|
126
|
+
getAccessToken(): string | null;
|
|
127
|
+
getAccessTokenExpiry(): number | null;
|
|
128
|
+
waitForAuth(timeoutMs?: number): Promise<boolean>;
|
|
129
|
+
withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
|
|
130
|
+
maxRetries?: number;
|
|
131
|
+
retryDelay?: number;
|
|
132
|
+
authTimeoutMs?: number;
|
|
133
|
+
}): Promise<T_1>;
|
|
134
|
+
validate(): Promise<boolean>;
|
|
135
|
+
handleError(error: unknown): Error;
|
|
136
|
+
healthCheck(): Promise<{
|
|
137
|
+
status: string;
|
|
138
|
+
users?: number;
|
|
139
|
+
timestamp?: string;
|
|
140
|
+
[key: string]: any;
|
|
141
|
+
}>;
|
|
142
|
+
};
|
|
143
|
+
} & T;
|
|
@@ -55,6 +55,8 @@ export interface ViewerGraph {
|
|
|
55
55
|
mutualIds: string[];
|
|
56
56
|
/** Accounts the viewer has blocked (bounded). */
|
|
57
57
|
blockedIds: string[];
|
|
58
|
+
/** Accounts the viewer has restricted (bounded). */
|
|
59
|
+
restrictedIds: string[];
|
|
58
60
|
}
|
|
59
61
|
/** Per-user outcome returned by `POST /users/unfollow/bulk`. */
|
|
60
62
|
export interface BulkUnfollowEntry {
|
|
@@ -24,6 +24,7 @@ import { OxyServicesUtilityMixin } from './OxyServices.utility';
|
|
|
24
24
|
import { OxyServicesFeaturesMixin } from './OxyServices.features';
|
|
25
25
|
import { OxyServicesTopicsMixin } from './OxyServices.topics';
|
|
26
26
|
import { OxyServicesContactsMixin } from './OxyServices.contacts';
|
|
27
|
+
import { OxyServicesNotificationsMixin } from './OxyServices.notifications';
|
|
27
28
|
import { OxyServicesAppDataMixin } from './OxyServices.appData';
|
|
28
29
|
import { OxyServicesCivicMixin } from './OxyServices.civic';
|
|
29
30
|
import { OxyServicesNodesMixin } from './OxyServices.nodes';
|
|
@@ -39,7 +40,7 @@ import { OxyServicesDeviceTransferMixin } from './OxyServices.deviceTransfer';
|
|
|
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 OxyServicesUserMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityBackupMixin<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 OxyServicesAccountsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesConnectedAppsMixin<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 OxyServicesContactsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAppDataMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesCivicMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesNodesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLinksMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDeviceBootMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDeviceTransferMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUtilityMixin<typeof OxyServicesBase>>>;
|
|
43
|
+
type AllMixinInstances = InstanceType<ReturnType<typeof OxyServicesAuthMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUserMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityBackupMixin<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 OxyServicesAccountsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesConnectedAppsMixin<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 OxyServicesContactsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesNotificationsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAppDataMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesCivicMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesNodesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLinksMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDeviceBootMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDeviceTransferMixin<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
|
|
@@ -66,6 +66,22 @@ export interface SessionClientOptions {
|
|
|
66
66
|
* absent it falls back to `getSocketIO()`.
|
|
67
67
|
*/
|
|
68
68
|
socketFactory?: SocketIOFactory;
|
|
69
|
+
/**
|
|
70
|
+
* The PINNED account id for an IDENTITY-BOUND client (the identity vault),
|
|
71
|
+
* or `null` for every ordinary account-mode client. Read as a function because
|
|
72
|
+
* the pin is resolved asynchronously at boot and can move when the identity
|
|
73
|
+
* session is re-established.
|
|
74
|
+
*
|
|
75
|
+
* While pinned this client tracks device state TRUTHFULLY — other apps'
|
|
76
|
+
* accounts and the device's real `activeAccountId` stay visible in `getState()`
|
|
77
|
+
* — but it NEVER re-binds its bearer to that active account: no `activeToken`
|
|
78
|
+
* is planted for a non-pinned account (from a sync response or a pushed
|
|
79
|
+
* `session_state`), the `TokenTransport` (whose entire job is converging on
|
|
80
|
+
* `activeAccountId`) is bypassed, and a push that switches the device does not
|
|
81
|
+
* trigger a token re-fetch. The pinned token's lifecycle belongs solely to the
|
|
82
|
+
* cold boot / re-mint lane, which mints it with an explicit `accountId`.
|
|
83
|
+
*/
|
|
84
|
+
getPinnedAccountId?: () => string | null;
|
|
69
85
|
}
|
|
70
86
|
type StateListener = (state: DeviceSessionState | null) => void;
|
|
71
87
|
export declare class SessionClient {
|
|
@@ -84,6 +100,13 @@ export declare class SessionClient {
|
|
|
84
100
|
private readonly boundServerEvents;
|
|
85
101
|
constructor(host: SessionClientHost, options?: SessionClientOptions);
|
|
86
102
|
getState(): DeviceSessionState | null;
|
|
103
|
+
/**
|
|
104
|
+
* The account this client's bearer is pinned to, or `null` when it follows the
|
|
105
|
+
* device's active account (the default). Resolvers are expected to be a plain
|
|
106
|
+
* synchronous read of already-resolved state (see
|
|
107
|
+
* {@link SessionClientOptions.getPinnedAccountId}).
|
|
108
|
+
*/
|
|
109
|
+
private pinnedAccountId;
|
|
87
110
|
subscribe(listener: StateListener): () => void;
|
|
88
111
|
/**
|
|
89
112
|
* Subscribe to a named server-pushed Socket.IO event (e.g. `civic:attested`).
|
|
@@ -138,6 +161,11 @@ export declare class SessionClient {
|
|
|
138
161
|
* sign-in. This adds, then switches to the target so the UI lands on the
|
|
139
162
|
* account the user just authenticated.
|
|
140
163
|
*
|
|
164
|
+
* An identity-bound (pinned) client only ADDS: its own session is minted for
|
|
165
|
+
* the pinned account explicitly, so switching the device would gratuitously
|
|
166
|
+
* re-elect the active account under every OTHER app on this device — a
|
|
167
|
+
* mutation a pinned client must never make.
|
|
168
|
+
*
|
|
141
169
|
* @param accountId - The signed-in account id (e.g. `session.user.id`). When
|
|
142
170
|
* omitted, falls back to the host's current-account ref. If neither
|
|
143
171
|
* resolves, the add still applies and no switch is performed.
|
|
@@ -21,6 +21,17 @@
|
|
|
21
21
|
* - the "Sign in with Oxy" device flow (same-device shared-keychain via
|
|
22
22
|
* `oxyServices.signInWithSharedIdentity`, else the cross-device QR handoff
|
|
23
23
|
* via `startCommonsSignIn` → poll → `claimSessionByToken`);
|
|
24
|
+
* - AUTOMATIC delivery selection for that flow (issue #691): the user presses
|
|
25
|
+
* ONE primary action and the controller — not the user — picks how the
|
|
26
|
+
* request reaches their Commons identity, by gathering the facts
|
|
27
|
+
* (`platform`, `commonsAvailability`, and the `targets` a bearer-authorized
|
|
28
|
+
* `deliverCommonsSignIn` reached) and handing them to the pure
|
|
29
|
+
* `selectCommonsDelivery`. Exactly one route is primary; alternatives are
|
|
30
|
+
* never chained behind it, they are state (`signIn.route`,
|
|
31
|
+
* `signIn.routeFailed`) the UI reveals on its own terms;
|
|
32
|
+
* - honest, non-sensitive PROGRESS for that flow (`signIn.progress`), derived
|
|
33
|
+
* only from real signals — the chosen route, `pushSentAt`, `openedAt`,
|
|
34
|
+
* `authorized`, and the commit — never from an optimistic timeline;
|
|
24
35
|
* - `commonsAvailability` — whether Commons is installed on this device
|
|
25
36
|
* (native only, via the injected `canOpenApp` probe), so the QR view can
|
|
26
37
|
* offer a "Get Commons" fallback instead of a same-device dead end.
|
|
@@ -48,6 +59,7 @@ import type { SessionLoginResponse, MinimalUserData } from '../models/session';
|
|
|
48
59
|
import type { SessionClient } from './SessionClient';
|
|
49
60
|
import type { SocketIOFactory } from './socketLoader';
|
|
50
61
|
import { type SwitchableAccount } from './accountProjection';
|
|
62
|
+
import { type CommonsDeliveryPlatform, type CommonsDeliveryRoute } from '../utils/commonsDelivery';
|
|
51
63
|
/** The dialog's top-level view. */
|
|
52
64
|
export type AccountDialogView = 'accounts' | 'signin' | 'qr' | 'add' | 'signup';
|
|
53
65
|
/**
|
|
@@ -62,8 +74,40 @@ export type AccountDialogView = 'accounts' | 'signin' | 'qr' | 'add' | 'signup';
|
|
|
62
74
|
* (native only). A probe error is treated as `'unavailable'` (fail-closed).
|
|
63
75
|
*/
|
|
64
76
|
export type CommonsAvailability = 'unknown' | 'checking' | 'available' | 'unavailable';
|
|
65
|
-
/**
|
|
66
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Lifecycle phase of the "Sign in with Oxy" device flow — the RESOURCE state
|
|
79
|
+
* (is there a live request, a claim in flight, a terminal outcome), as opposed
|
|
80
|
+
* to {@link SignInProgress}, which is what the user is told.
|
|
81
|
+
*
|
|
82
|
+
* `'completed'` is terminal-but-successful: the session was claimed and
|
|
83
|
+
* committed, nothing is in flight, and the surface may show "Identity
|
|
84
|
+
* confirmed" before it goes away. It is cleared back to `'idle'` the moment the
|
|
85
|
+
* dialog moves to another view (a new intention), so a later sign-in entry can
|
|
86
|
+
* never inherit the previous flow's terminal state.
|
|
87
|
+
*/
|
|
88
|
+
export type SignInFlowPhase = 'idle' | 'starting' | 'waiting' | 'authorized' | 'completed' | 'error';
|
|
89
|
+
/**
|
|
90
|
+
* Ordered, non-sensitive progress of the active request — the ONE thing the
|
|
91
|
+
* sign-in surface renders its status line from (issue #691, Phase 5).
|
|
92
|
+
*
|
|
93
|
+
* Every step is DERIVED from a real fact the controller observed; there is no
|
|
94
|
+
* optimistic sequence and no timer that advances it. The mapping the UI is
|
|
95
|
+
* expected to render (copy lives in the UI layer, never here):
|
|
96
|
+
*
|
|
97
|
+
* | value | shown as | advanced by |
|
|
98
|
+
* |------------------------|--------------------------------|-------------|
|
|
99
|
+
* | `idle` | (nothing) | no live request, or a terminal error |
|
|
100
|
+
* | `preparing` | "Preparing request" | the request is being created / its route is still being resolved |
|
|
101
|
+
* | `awaiting-approval` | route-specific waiting copy | the primary route is known (`qr` → the QR itself, `open-commons` → "Continue in Commons") |
|
|
102
|
+
* | `delivered-to-commons` | "Check Commons on your phone" | the server confirmed a push to ≥1 known Commons install (`await-push`, or `pushSentAt`) |
|
|
103
|
+
* | `opened-in-commons` | "Opened in Commons" | the approver reported `openedAt` |
|
|
104
|
+
* | `confirming-identity` | "Confirming identity" | the request reported `authorized`; the claim/commit is running |
|
|
105
|
+
* | `identity-confirmed` | "Identity confirmed" | the session was claimed and committed |
|
|
106
|
+
*
|
|
107
|
+
* `opened-in-commons` and `delivered-to-commons` are PROGRESS only — neither is
|
|
108
|
+
* evidence of an approval. Only `authorized` moves the flow forward.
|
|
109
|
+
*/
|
|
110
|
+
export type SignInProgress = 'idle' | 'preparing' | 'awaiting-approval' | 'delivered-to-commons' | 'opened-in-commons' | 'confirming-identity' | 'identity-confirmed';
|
|
67
111
|
/**
|
|
68
112
|
* Minimal structural handle over a popup `Window` — just enough for the
|
|
69
113
|
* cross-origin passkey hub flow ({@link AccountDialogController.startPasskeyHubSignIn}):
|
|
@@ -79,7 +123,14 @@ export interface PopupWindowHandle {
|
|
|
79
123
|
href: string;
|
|
80
124
|
};
|
|
81
125
|
}
|
|
82
|
-
/**
|
|
126
|
+
/**
|
|
127
|
+
* State of the "Sign in with Oxy" (shared-key / QR) device flow.
|
|
128
|
+
*
|
|
129
|
+
* Everything here is safe to render. The flow's SECRET credential (the
|
|
130
|
+
* device-flow `sessionToken`) is deliberately absent — it never leaves the
|
|
131
|
+
* controller's private field, so no surface, log, or serialized snapshot can
|
|
132
|
+
* leak it.
|
|
133
|
+
*/
|
|
83
134
|
export interface SignInFlowState {
|
|
84
135
|
phase: SignInFlowPhase;
|
|
85
136
|
/**
|
|
@@ -96,6 +147,46 @@ export interface SignInFlowState {
|
|
|
96
147
|
expiresAt: number | null;
|
|
97
148
|
/** Human-readable error for the retry UI, or `null`. */
|
|
98
149
|
error: string | null;
|
|
150
|
+
/**
|
|
151
|
+
* The ONE primary delivery route the controller chose for this request
|
|
152
|
+
* ({@link selectCommonsDelivery}), or `null` while it is still being resolved.
|
|
153
|
+
*
|
|
154
|
+
* There is no chain: exactly one route is primary, and the UI renders exactly
|
|
155
|
+
* one action for it. Alternatives stay behind a "Having trouble?" affordance
|
|
156
|
+
* that the UI reveals on its own terms — see {@link routeFailed}.
|
|
157
|
+
*/
|
|
158
|
+
route: CommonsDeliveryRoute | null;
|
|
159
|
+
/**
|
|
160
|
+
* `true` when the chosen primary route could NOT be carried out on this
|
|
161
|
+
* device — today only `'open-commons'` can fail this way (no URL opener was
|
|
162
|
+
* injected, or opening the verified Commons link threw). It is the signal the
|
|
163
|
+
* UI needs to reveal its alternatives; the controller never cascades to
|
|
164
|
+
* another route by itself.
|
|
165
|
+
*
|
|
166
|
+
* A push that reached zero installations is NOT a failure: no capable Commons
|
|
167
|
+
* install is a normal outcome that simply resolves the primary route to
|
|
168
|
+
* `'qr'`.
|
|
169
|
+
*/
|
|
170
|
+
routeFailed: boolean;
|
|
171
|
+
/**
|
|
172
|
+
* Server-reported ISO-8601 timestamp of when the request was pushed to a
|
|
173
|
+
* known Commons installation, or `null`. Progress only — never evidence of an
|
|
174
|
+
* approval. Monotone: once observed it is never cleared by a later, emptier
|
|
175
|
+
* poll response.
|
|
176
|
+
*/
|
|
177
|
+
pushSentAt: string | null;
|
|
178
|
+
/**
|
|
179
|
+
* Server-reported ISO-8601 timestamp of when the approval route was OPENED in
|
|
180
|
+
* Commons, or `null`. Progress only — never evidence of an approval. Monotone,
|
|
181
|
+
* like {@link pushSentAt}.
|
|
182
|
+
*/
|
|
183
|
+
openedAt: string | null;
|
|
184
|
+
/**
|
|
185
|
+
* The derived, ordered progress the surface renders. Always computed from the
|
|
186
|
+
* facts above by {@link deriveSignInProgress} — never assigned directly, so it
|
|
187
|
+
* cannot drift from them or run ahead of a real signal.
|
|
188
|
+
*/
|
|
189
|
+
progress: SignInProgress;
|
|
99
190
|
}
|
|
100
191
|
/** Immutable snapshot consumed by `useSyncExternalStore`. */
|
|
101
192
|
export interface AccountDialogSnapshot {
|
|
@@ -208,6 +299,18 @@ export interface AccountDialogControllerOptions {
|
|
|
208
299
|
* `https://auth.${CENTRAL_IDP_APEX}`). Overridable for local/staging testing.
|
|
209
300
|
*/
|
|
210
301
|
hubBaseUrl?: string;
|
|
302
|
+
/**
|
|
303
|
+
* Which surface the sign-in is initiated from — a FACT supplied by the
|
|
304
|
+
* consumer, because only the consumer can classify its own environment
|
|
305
|
+
* (native → `'mobile'`; web → `'mobile'` for a mobile browser, `'desktop'`
|
|
306
|
+
* otherwise). Headless core never sniffs a user agent or a platform global.
|
|
307
|
+
*
|
|
308
|
+
* Feeds {@link selectCommonsDelivery} verbatim. Defaults to `'unknown'`,
|
|
309
|
+
* which is a first-class value there: an unclassified surface never opts into
|
|
310
|
+
* the deep-link route, because a custom-scheme navigation that does not
|
|
311
|
+
* resolve is a dead end with no automatic way back.
|
|
312
|
+
*/
|
|
313
|
+
platform?: CommonsDeliveryPlatform;
|
|
211
314
|
}
|
|
212
315
|
type SnapshotListener = (snapshot: AccountDialogSnapshot) => void;
|
|
213
316
|
export declare class AccountDialogController {
|
|
@@ -224,6 +327,7 @@ export declare class AccountDialogController {
|
|
|
224
327
|
private readonly socketFactory?;
|
|
225
328
|
private readonly openPopup?;
|
|
226
329
|
private readonly hubBaseUrl;
|
|
330
|
+
private readonly platform;
|
|
227
331
|
private readonly listeners;
|
|
228
332
|
private view;
|
|
229
333
|
private graph;
|
|
@@ -375,8 +479,57 @@ export declare class AccountDialogController {
|
|
|
375
479
|
* same `startCommonsSignIn` → poll/socket wiring either way. Returns the
|
|
376
480
|
* handle on success (already reflected in `signIn`), or `null` on failure
|
|
377
481
|
* (already set as `signIn.error`).
|
|
482
|
+
*
|
|
483
|
+
* @param opts.deliver - Whether to run automatic Commons delivery selection
|
|
484
|
+
* ({@link resolveDeliveryRoute}). `true` for the normal one-primary-action
|
|
485
|
+
* entry; `false` when the caller already owns the primary surface (the
|
|
486
|
+
* passkey hub popup), where the request's Commons route is simply the QR.
|
|
378
487
|
*/
|
|
379
488
|
private startDeviceFlowSession;
|
|
489
|
+
/**
|
|
490
|
+
* Choose and carry out the ONE primary delivery route for the active request
|
|
491
|
+
* (issue #691, "Automatic delivery selection").
|
|
492
|
+
*
|
|
493
|
+
* The controller gathers the facts — is a verified Commons link openable on
|
|
494
|
+
* THIS device, and how many known Commons installations did Oxy actually push
|
|
495
|
+
* to — and hands them to the pure {@link selectCommonsDelivery}. The selector
|
|
496
|
+
* owns the decision; this method owns only the observations and the single
|
|
497
|
+
* action the chosen route implies. It never cascades: a route that yields
|
|
498
|
+
* nothing resolves to QR *before* a route is chosen, never after.
|
|
499
|
+
*/
|
|
500
|
+
private resolveDeliveryRoute;
|
|
501
|
+
/**
|
|
502
|
+
* Whether `sessionToken` is still THE request this surface is waiting on.
|
|
503
|
+
*
|
|
504
|
+
* Guards every step of the asynchronous route resolution: a cancelled,
|
|
505
|
+
* superseded, failed, or already-approved flow must neither open Commons nor
|
|
506
|
+
* mutate the surface — an approval that lands mid-resolution would otherwise
|
|
507
|
+
* be followed by a pointless app switch.
|
|
508
|
+
*/
|
|
509
|
+
private isAwaitingApproval;
|
|
510
|
+
/**
|
|
511
|
+
* Ask Oxy to deliver the pending request to the identity's known Commons
|
|
512
|
+
* installations, returning how many it reached (`0` when delivery is not
|
|
513
|
+
* applicable, not permitted, or reached nobody).
|
|
514
|
+
*
|
|
515
|
+
* Two hard rules, both from the issue:
|
|
516
|
+
* - **Never push from an unauthenticated surface.** `deliverCommonsSignIn`
|
|
517
|
+
* is bearer-required precisely because a request that merely carries a
|
|
518
|
+
* typed-in username must never be able to ring somebody's phone. Without a
|
|
519
|
+
* planted bearer the call is not made AT ALL — not made-and-failed.
|
|
520
|
+
* - **Zero targets and a failed delivery are the same normal outcome.** Both
|
|
521
|
+
* return `0`, which resolves the primary route to QR silently. Neither is
|
|
522
|
+
* surfaced as an error: there is nothing the user did wrong and nothing for
|
|
523
|
+
* them to fix.
|
|
524
|
+
*/
|
|
525
|
+
private deliverToKnownCommons;
|
|
526
|
+
/**
|
|
527
|
+
* Open the verified Commons link for the `'open-commons'` route. Returns
|
|
528
|
+
* whether the navigation was actually handed off, so a missing opener or a
|
|
529
|
+
* throwing one becomes an observable `routeFailed` instead of a silent dead
|
|
530
|
+
* end the user stares at.
|
|
531
|
+
*/
|
|
532
|
+
private openCommonsLink;
|
|
380
533
|
/**
|
|
381
534
|
* Poll {@link PopupWindowHandle.closed} so a user who dismisses the hub
|
|
382
535
|
* popup without completing sign-in gets prompt feedback (there is no DOM
|
|
@@ -402,14 +555,22 @@ export declare class AccountDialogController {
|
|
|
402
555
|
*/
|
|
403
556
|
private resolveCommonsAvailability;
|
|
404
557
|
/**
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
558
|
+
* Tear down the active sign-in device flow (timers + socket + popup + token),
|
|
559
|
+
* WITHDRAW the request server-side, and reset to idle.
|
|
560
|
+
*
|
|
561
|
+
* Cancellation has to converge in both directions: the surface closing must
|
|
562
|
+
* cancel the request, not just stop listening to it. Without the withdrawal a
|
|
563
|
+
* dismissed QR would stay approvable until it expired, so a later scan of a
|
|
564
|
+
* stale code could authorize a session nobody is waiting for.
|
|
409
565
|
*/
|
|
410
|
-
private deepLinkIntoCommonsIfAvailable;
|
|
411
|
-
/** Tear down the active sign-in device flow (timers + socket + popup + token) and reset to idle. */
|
|
412
566
|
cancelSignIn(): void;
|
|
567
|
+
/**
|
|
568
|
+
* Best-effort server-side withdrawal of a request this surface abandoned
|
|
569
|
+
* (`POST /auth/session/deny/:authorizeCode`). Fire-and-forget by design: the
|
|
570
|
+
* local teardown already happened, and a race with an approval that just
|
|
571
|
+
* landed legitimately rejects here — neither outcome is worth surfacing.
|
|
572
|
+
*/
|
|
573
|
+
private withdrawRequest;
|
|
413
574
|
private scheduleNextPoll;
|
|
414
575
|
/**
|
|
415
576
|
* Run one status check + (on approval) claim. Triggered by the fallback timer
|
|
@@ -419,6 +580,15 @@ export declare class AccountDialogController {
|
|
|
419
580
|
* re-checks the authoritative status via `pollCommonsSignIn`.
|
|
420
581
|
*/
|
|
421
582
|
private pollOnce;
|
|
583
|
+
/**
|
|
584
|
+
* Record server-reported delivery progress on the active flow.
|
|
585
|
+
*
|
|
586
|
+
* Monotone and additive: a timestamp is only ever adopted, never replaced or
|
|
587
|
+
* cleared, so an older API build (or a partial payload) that omits a field can
|
|
588
|
+
* at most fail to advance the surface — it can never walk it backwards.
|
|
589
|
+
* Emits only on a real change, so a steady poll does not churn the snapshot.
|
|
590
|
+
*/
|
|
591
|
+
private recordDeliveryProgress;
|
|
422
592
|
private claimAndComplete;
|
|
423
593
|
/**
|
|
424
594
|
* Commit an authorized session, notify, and return to the account list. Shared
|
|
@@ -454,7 +624,15 @@ export declare class AccountDialogController {
|
|
|
454
624
|
private openAuthSessionSocket;
|
|
455
625
|
/** Tear down the `/auth-session` socket, if any. Idempotent. */
|
|
456
626
|
private closeAuthSessionSocket;
|
|
627
|
+
/**
|
|
628
|
+
* Replace the device-flow state from its FACTS, re-deriving `progress`. The
|
|
629
|
+
* only writer of `this.signIn` besides the two terminal constants — which is
|
|
630
|
+
* what keeps `progress` impossible to set by hand, and therefore impossible
|
|
631
|
+
* to advance without a fact behind it.
|
|
632
|
+
*/
|
|
457
633
|
private setSignIn;
|
|
634
|
+
/** Update a subset of the device-flow facts, re-deriving `progress`. */
|
|
635
|
+
private patchSignIn;
|
|
458
636
|
private computeSnapshot;
|
|
459
637
|
/** Recompute the snapshot and notify subscribers. */
|
|
460
638
|
private emit;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity pin — the durable binding between THIS device's PRIMARY identity key
|
|
3
|
+
* and the account that key authenticates as.
|
|
4
|
+
*
|
|
5
|
+
* An identity-bound client (Commons, the identity vault) must always act as the
|
|
6
|
+
* owner of the local signing key — permanently. The `DeviceSession` it shares
|
|
7
|
+
* with every other Oxy app on the same device tracks a MUTABLE `activeAccountId`
|
|
8
|
+
* that any sibling app can switch; following it silently changes both the user
|
|
9
|
+
* such a client renders and the bearer it sends. The pin is the client-side
|
|
10
|
+
* record that breaks that coupling: `{ publicKey, accountId }`, written when an
|
|
11
|
+
* identity-mode session is established (the only moment both facts are known
|
|
12
|
+
* first-hand), read on every boot, and reconciled against the live
|
|
13
|
+
* `KeyManager.getPublicKey()` before it is trusted (see
|
|
14
|
+
* `resolveIdentityPin` in `identitySession.ts`).
|
|
15
|
+
*
|
|
16
|
+
* It holds NO secret — a public key and an account id, both already known to the
|
|
17
|
+
* server — so it lives in the same per-origin storage as {@link PersistedAuthState}
|
|
18
|
+
* (`authStateStore.ts`) rather than the keychain, and mirrors that module's
|
|
19
|
+
* store shape: `load / save / clear`, an in-memory mirror, a read-back-verified
|
|
20
|
+
* durable write, and graceful degradation when storage is unavailable.
|
|
21
|
+
*
|
|
22
|
+
* ESM-safe (no `require()`); no react/react-native/expo imports.
|
|
23
|
+
*/
|
|
24
|
+
import type { NativeKeyValueStorage } from './authStateStore';
|
|
25
|
+
/**
|
|
26
|
+
* The persisted identity binding.
|
|
27
|
+
*
|
|
28
|
+
* `publicKey` is the device's PRIMARY identity public key (lower-case hex, as
|
|
29
|
+
* `KeyManager` stores it) — never the cross-app shared-slot key, which may hold
|
|
30
|
+
* a different identity. `accountId` is the account the server resolved for that
|
|
31
|
+
* key when the session was established.
|
|
32
|
+
*/
|
|
33
|
+
export interface IdentityPin {
|
|
34
|
+
publicKey: string;
|
|
35
|
+
accountId: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The storage seam for {@link IdentityPin}. Async throughout so one interface
|
|
39
|
+
* fits both synchronous web `localStorage` and asynchronous native
|
|
40
|
+
* SecureStore/AsyncStorage.
|
|
41
|
+
*/
|
|
42
|
+
export interface IdentityPinStore {
|
|
43
|
+
load(): Promise<IdentityPin | null>;
|
|
44
|
+
/**
|
|
45
|
+
* Persist the pin and report whether it durably landed.
|
|
46
|
+
*
|
|
47
|
+
* `true` means the value is retained consistent with this store's durability
|
|
48
|
+
* guarantee (a durable backing whose read-back matched, or a degraded/in-memory
|
|
49
|
+
* store that held it in memory). `false` means a DURABLE backing was expected
|
|
50
|
+
* but the write did not land — the in-memory mirror keeps this process pinned,
|
|
51
|
+
* but the next cold boot will have to re-establish the identity session (which
|
|
52
|
+
* rewrites the pin) instead of taking the fast pinned-mint lane.
|
|
53
|
+
*/
|
|
54
|
+
save(pin: IdentityPin): Promise<boolean>;
|
|
55
|
+
clear(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Versioned storage key. Deliberately distinct from the `oxy.auth.*` keys so a
|
|
59
|
+
* session sign-out (which clears the auth blob) and an identity change (which
|
|
60
|
+
* clears the pin) can never take each other down.
|
|
61
|
+
*/
|
|
62
|
+
export declare const IDENTITY_PIN_STORAGE_KEY = "oxy.identity.pin.v1";
|
|
63
|
+
/**
|
|
64
|
+
* Whether `pin` still describes the identity currently on this device.
|
|
65
|
+
*
|
|
66
|
+
* `publicKey` comparison is case-insensitive (hex). A `null` local key is a
|
|
67
|
+
* definitive "no identity here" and therefore never a match — the caller clears
|
|
68
|
+
* the pin rather than binding to an account whose key is gone.
|
|
69
|
+
*/
|
|
70
|
+
export declare function identityPinMatches(pin: IdentityPin | null, localPublicKey: string | null): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* A process-lifetime, in-memory {@link IdentityPinStore}. Used directly for
|
|
73
|
+
* tests/SSR and as the degraded fallback of the web store.
|
|
74
|
+
*/
|
|
75
|
+
export declare function createMemoryIdentityPinStore(): IdentityPinStore;
|
|
76
|
+
/**
|
|
77
|
+
* A `localStorage`-backed {@link IdentityPinStore}. Degrades to an in-memory
|
|
78
|
+
* store when `localStorage` is unreachable, and keeps an in-memory mirror so a
|
|
79
|
+
* failed persist still pins THIS page's lifetime.
|
|
80
|
+
*/
|
|
81
|
+
export declare function createWebIdentityPinStore(): IdentityPinStore;
|
|
82
|
+
/**
|
|
83
|
+
* A native {@link IdentityPinStore} over an injected async key/value store —
|
|
84
|
+
* the same seam `createNativeAuthStateStore` uses, so `@oxyhq/core` never
|
|
85
|
+
* imports `expo-secure-store`.
|
|
86
|
+
*/
|
|
87
|
+
export declare function createNativeIdentityPinStore(storage: NativeKeyValueStorage): IdentityPinStore;
|