@oxy.so/core 1.10.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/crypto/deviceBackup.js +75 -0
- package/dist/cjs/crypto/keyManager.js +285 -12
- package/dist/cjs/mixins/OxyServices.linkedAccounts.js +79 -0
- package/dist/cjs/mixins/OxyServices.user.js +5 -1
- package/dist/cjs/mixins/index.js +4 -0
- package/dist/cjs/server/securityHeaders.js +3 -0
- package/dist/cjs/session/accountDialogController.js +41 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/crypto/deviceBackup.js +70 -0
- package/dist/esm/crypto/keyManager.js +252 -12
- package/dist/esm/mixins/OxyServices.linkedAccounts.js +76 -0
- package/dist/esm/mixins/OxyServices.user.js +5 -1
- package/dist/esm/mixins/index.js +4 -0
- package/dist/esm/server/securityHeaders.js +3 -0
- package/dist/esm/session/accountDialogController.js +41 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/crypto/deviceBackup.d.ts +60 -0
- package/dist/types/crypto/keyManager.d.ts +79 -12
- package/dist/types/index.d.ts +2 -1
- package/dist/types/mixins/OxyServices.linkedAccounts.d.ts +107 -0
- package/dist/types/mixins/OxyServices.user.d.ts +7 -2
- package/dist/types/mixins/index.d.ts +2 -1
- package/dist/types/models/interfaces.d.ts +30 -0
- package/dist/types/server/securityHeaders.d.ts +5 -0
- package/dist/types/session/accountDialogController.d.ts +45 -0
- package/package.json +2 -2
- package/src/crypto/__tests__/keyManager.deviceBackup.test.ts +310 -0
- package/src/crypto/deviceBackup.ts +108 -0
- package/src/crypto/keyManager.ts +278 -13
- package/src/index.ts +2 -0
- package/src/mixins/OxyServices.linkedAccounts.ts +129 -0
- package/src/mixins/OxyServices.user.ts +7 -2
- package/src/mixins/__tests__/linkedAccounts.test.ts +109 -0
- package/src/mixins/index.ts +6 -0
- package/src/models/interfaces.ts +30 -1
- package/src/server/__tests__/securityHeaders.test.ts +7 -0
- package/src/server/securityHeaders.ts +8 -0
- package/src/session/__tests__/accountDialogController.test.ts +173 -0
- package/src/session/__tests__/accountDialogShape.test.ts +1 -0
- package/src/session/accountDialogController.ts +65 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The device backup of the self-custody identity: a copy that lives OUTSIDE this
|
|
3
|
+
* app's own keystore, so a keystore wipe cannot take it along with the keys.
|
|
4
|
+
*
|
|
5
|
+
* Why it exists (OxyHQ/oxy#1388): on Android every Oxy app shares the Linux UID
|
|
6
|
+
* `so.oxy.shared`, and the Android Keystore is per UID. Clearing the storage of
|
|
7
|
+
* ANY sibling app (`pm clear`, or Settings › Apps › Storage › Clear storage)
|
|
8
|
+
* wipes the Keystore of the whole UID. Every copy the SDK keeps in
|
|
9
|
+
* expo-secure-store (primary, backup slot, phrase slot) and in the androidx
|
|
10
|
+
* encrypted prefs (the cross-app shared slot) is wrapped by a key in that
|
|
11
|
+
* Keystore, so they all die together, and until now the only way back was the
|
|
12
|
+
* recovery phrase.
|
|
13
|
+
*
|
|
14
|
+
* `@oxy.so/core` never imports a native module, so the store is injected by the
|
|
15
|
+
* app that owns the identity ({@link KeyManager.setDeviceBackupStore}). Commons
|
|
16
|
+
* registers Android Block Store; every other app registers nothing, which keeps
|
|
17
|
+
* every code path below a no-op for them. The design and the threat model are in
|
|
18
|
+
* `docs/identity/device-backup.md`.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Where the device backup is kept. Implementations must hold the value outside
|
|
22
|
+
* the calling app's keystore (that is the whole point) and must never send it
|
|
23
|
+
* anywhere Oxy can read it.
|
|
24
|
+
*
|
|
25
|
+
* `read` resolves `null` when nothing is stored AND when the store cannot be
|
|
26
|
+
* reached (no Google Play services, an old binary without the native module):
|
|
27
|
+
* the caller treats both as "no device backup" and falls back to the phrase.
|
|
28
|
+
*/
|
|
29
|
+
export interface IdentityDeviceBackupStore {
|
|
30
|
+
/** Short name for logs, e.g. `android-block-store`. Never contains secrets. */
|
|
31
|
+
readonly name: string;
|
|
32
|
+
read(): Promise<string | null>;
|
|
33
|
+
write(value: string): Promise<void>;
|
|
34
|
+
clear(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
/** The record format. Bump {@link DEVICE_BACKUP_VERSION} on a breaking change. */
|
|
37
|
+
export declare const DEVICE_BACKUP_VERSION = 1;
|
|
38
|
+
export interface DeviceBackupRecord {
|
|
39
|
+
version: typeof DEVICE_BACKUP_VERSION;
|
|
40
|
+
/** Canonical lowercase 64-hex secp256k1 private key. */
|
|
41
|
+
privateKey: string;
|
|
42
|
+
/** Canonical lowercase public key. */
|
|
43
|
+
publicKey: string;
|
|
44
|
+
/**
|
|
45
|
+
* The BIP-39 phrase, present only when it was verified to derive
|
|
46
|
+
* {@link publicKey}. Carried so a restored identity can still reveal its
|
|
47
|
+
* phrase in Settings and create the phrase-keyed Oxy backup; the phrase is
|
|
48
|
+
* never more powerful than the private key already in the record.
|
|
49
|
+
*/
|
|
50
|
+
mnemonic?: string;
|
|
51
|
+
/** ISO-8601 time of the last write. */
|
|
52
|
+
updatedAt: string;
|
|
53
|
+
}
|
|
54
|
+
export declare function serializeDeviceBackup(record: DeviceBackupRecord): string;
|
|
55
|
+
/**
|
|
56
|
+
* Parse a stored record, or `null` for anything that is not a well-formed
|
|
57
|
+
* version-1 record. Checks SHAPE only; the caller still proves the key pair is
|
|
58
|
+
* healthy (well-formed, public derives from private) before trusting it.
|
|
59
|
+
*/
|
|
60
|
+
export declare function parseDeviceBackup(raw: string | null | undefined): DeviceBackupRecord | null;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Private keys are stored securely using expo-secure-store and never leave the device.
|
|
6
6
|
*/
|
|
7
7
|
import { type IdentityMarker } from './identityMarker';
|
|
8
|
+
import { type IdentityDeviceBackupStore } from './deviceBackup';
|
|
8
9
|
/**
|
|
9
10
|
* Thrown when an identity-mutating operation (createIdentity / importKeyPair)
|
|
10
11
|
* is invoked while a valid identity already exists on the device.
|
|
@@ -79,10 +80,14 @@ export type IdentityStatus = {
|
|
|
79
80
|
* which independent, `key_v1`-surviving source restored the identity. On failure
|
|
80
81
|
* `reason` distinguishes "wasn't lost", "no surviving source", "a source held a
|
|
81
82
|
* DIFFERENT account" (never silently switched), and "storage unavailable".
|
|
83
|
+
*
|
|
84
|
+
* `device-backup` is the copy held outside this app's keystore (Android Block
|
|
85
|
+
* Store in Commons, see {@link KeyManager.setDeviceBackupStore}); it is the only
|
|
86
|
+
* source that survives a wipe of the whole shared-UID keystore.
|
|
82
87
|
*/
|
|
83
88
|
export type IdentityRecoveryResult = {
|
|
84
89
|
recovered: true;
|
|
85
|
-
source: 'backup' | 'shared';
|
|
90
|
+
source: 'backup' | 'shared' | 'device-backup';
|
|
86
91
|
publicKey: string;
|
|
87
92
|
} | {
|
|
88
93
|
recovered: false;
|
|
@@ -114,6 +119,23 @@ export declare class KeyManager {
|
|
|
114
119
|
*/
|
|
115
120
|
private static slotMigrationPromise;
|
|
116
121
|
private static slotMigrationResult;
|
|
122
|
+
/**
|
|
123
|
+
* The keystore-independent device backup, registered by the app that owns the
|
|
124
|
+
* identity (Commons). `null` in every other app, which makes every device
|
|
125
|
+
* backup path a no-op there. See `./deviceBackup`.
|
|
126
|
+
*/
|
|
127
|
+
private static deviceBackupStore;
|
|
128
|
+
/**
|
|
129
|
+
* Register (or, with `null`, unregister) where the identity's device backup is
|
|
130
|
+
* kept. Call once at startup, before the first identity read, ONLY from the
|
|
131
|
+
* app that holds the self-custody identity: the backup is a full copy of the
|
|
132
|
+
* private key, and it belongs to exactly one app.
|
|
133
|
+
*
|
|
134
|
+
* From then on every create, import, rotation and restore refreshes the
|
|
135
|
+
* backup, a forced delete clears it, and {@link attemptIdentityRecovery}
|
|
136
|
+
* restores from it when the keystore copies are gone.
|
|
137
|
+
*/
|
|
138
|
+
static setDeviceBackupStore(store: IdentityDeviceBackupStore | null): void;
|
|
117
139
|
/**
|
|
118
140
|
* Invalidate cached identity state
|
|
119
141
|
* Called internally when identity is created/deleted/imported
|
|
@@ -347,6 +369,33 @@ export declare class KeyManager {
|
|
|
347
369
|
* @internal
|
|
348
370
|
*/
|
|
349
371
|
private static _persistIdentityAtomic;
|
|
372
|
+
/**
|
|
373
|
+
* Write the device backup for `(privateKey, publicKey)`, keeping the phrase an
|
|
374
|
+
* existing record already carries for the SAME identity. `mnemonic` replaces
|
|
375
|
+
* it, and must already be verified against `publicKey` by the caller.
|
|
376
|
+
* Best-effort: never throws, returns whether the store now holds this pair.
|
|
377
|
+
*
|
|
378
|
+
* @internal
|
|
379
|
+
*/
|
|
380
|
+
private static _writeDeviceBackup;
|
|
381
|
+
/** Remove the device backup. Best-effort: never throws. @internal */
|
|
382
|
+
private static _clearDeviceBackup;
|
|
383
|
+
/** True when `phrase` is a valid BIP-39 phrase that derives `publicKey`. @internal */
|
|
384
|
+
private static _phraseDerivesPublicKey;
|
|
385
|
+
/** The primary pair when it reads back healthy, else `null`. Never throws. @internal */
|
|
386
|
+
private static _readHealthyPrimary;
|
|
387
|
+
/**
|
|
388
|
+
* Make sure the device backup holds the identity on this device, writing it
|
|
389
|
+
* when it is missing or stale. Commons calls this once per launch once the
|
|
390
|
+
* identity reads back healthy: it backfills identities created before the
|
|
391
|
+
* device backup existed, and repairs a backup whose last write failed (a
|
|
392
|
+
* failed write after a key rotation would otherwise leave the OLD key there).
|
|
393
|
+
*
|
|
394
|
+
* Carries the stored recovery phrase along only when it provably derives the
|
|
395
|
+
* current key. Resolves `true` when the backup is current, `false` when there
|
|
396
|
+
* is no store, no healthy identity, or the write failed. Never throws.
|
|
397
|
+
*/
|
|
398
|
+
static ensureDeviceBackup(): Promise<boolean>;
|
|
350
399
|
/**
|
|
351
400
|
* Write/refresh the identity marker after a successful persist. A same-identity
|
|
352
401
|
* re-persist (e.g. backup refresh, idempotent re-import) preserves `createdAt`
|
|
@@ -534,25 +583,43 @@ export declare class KeyManager {
|
|
|
534
583
|
*/
|
|
535
584
|
static restoreIdentityFromBackup(): Promise<boolean>;
|
|
536
585
|
/**
|
|
537
|
-
* Recovery ladder — restore
|
|
538
|
-
*
|
|
586
|
+
* Recovery ladder — restore an identity whose keys are gone from an
|
|
587
|
+
* independent source WITHOUT the user re-entering their recovery phrase.
|
|
539
588
|
*
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
* `publicKey === marker.publicKey`, so a source holding a DIFFERENT account is
|
|
546
|
-
* SKIPPED, never restored):
|
|
547
|
-
* 1. the v2 backup slot (independent keychain key from the primary), then
|
|
589
|
+
* For a `lost` verdict (marker present, keys empty) the rungs are tried in
|
|
590
|
+
* order, each fully validated (well-formed + derive-match + `publicKey ===
|
|
591
|
+
* marker.publicKey`, so a source holding a DIFFERENT account is SKIPPED, never
|
|
592
|
+
* restored):
|
|
593
|
+
* 1. the v2 backup slot (independent keychain key from the primary),
|
|
548
594
|
* 2. the cross-app shared slot (Android bridge `getShared` / iOS keychain
|
|
549
|
-
* group) — the copy that survives a primary+backup `key_v1` death
|
|
595
|
+
* group) — the copy that survives a primary+backup `key_v1` death,
|
|
596
|
+
* 3. the device backup ({@link setDeviceBackupStore}; Android Block Store in
|
|
597
|
+
* Commons) — the only copy that survives a wipe of the whole shared-UID
|
|
598
|
+
* Android Keystore, which takes rungs 1 and 2 with it (oxy#1388).
|
|
599
|
+
*
|
|
600
|
+
* For an `absent` verdict (no keys AND no marker: this app's own data was
|
|
601
|
+
* cleared, or it was reinstalled) only rung 3 applies: a device backup there
|
|
602
|
+
* means an identity lived on this device and was never deleted (every delete
|
|
603
|
+
* clears the backup), so it is restored instead of letting onboarding create a
|
|
604
|
+
* new identity over it. Without a registered store the result stays
|
|
605
|
+
* `not-lost`, exactly as before.
|
|
606
|
+
*
|
|
607
|
+
* `present` → `not-lost`; `unavailable` (keychain locked) → `unavailable`.
|
|
608
|
+
* Everything here is on-device: no network, so it works offline.
|
|
550
609
|
*
|
|
551
610
|
* On success it re-persists via {@link _persistIdentityAtomic} (origin
|
|
552
611
|
* `'restore'`) and invalidates the cache so routing re-reads `present`. When no
|
|
553
612
|
* rung matches, the UI proceeds to recovery-phrase entry.
|
|
554
613
|
*/
|
|
555
614
|
static attemptIdentityRecovery(): Promise<IdentityRecoveryResult>;
|
|
615
|
+
/** Read the device backup as a healthy, canonical candidate, or null. Never throws. @internal */
|
|
616
|
+
private static _readDeviceBackupCandidate;
|
|
617
|
+
/**
|
|
618
|
+
* Persist a device-backup candidate, then put its phrase back in the
|
|
619
|
+
* re-reveal slot (which died with the keystore) when it derives the key.
|
|
620
|
+
* @internal
|
|
621
|
+
*/
|
|
622
|
+
private static _commitDeviceBackupRecovery;
|
|
556
623
|
/** Read the active-layout backup slot as a healthy candidate, or null. @internal */
|
|
557
624
|
private static _readBackupCandidate;
|
|
558
625
|
/** Read the cross-app shared slot as a healthy candidate, or null. @internal */
|
package/dist/types/index.d.ts
CHANGED
|
@@ -61,6 +61,7 @@ export { KeyManager, IdentityAlreadyExistsError, IdentityPersistError, IdentityU
|
|
|
61
61
|
export type { KeyPair, IdentityStatus, IdentityRecoveryResult } from './crypto/keyManager';
|
|
62
62
|
export { readIdentityMarker, updateIdentityMarker, } from './crypto/identityMarker';
|
|
63
63
|
export type { IdentityMarker } from './crypto/identityMarker';
|
|
64
|
+
export type { IdentityDeviceBackupStore } from './crypto/deviceBackup';
|
|
64
65
|
export { SignatureService } from './crypto/signatureService';
|
|
65
66
|
export type { SignedMessage, AuthChallenge } from './crypto/signatureService';
|
|
66
67
|
export { RecoveryPhraseService } from './crypto/recoveryPhrase';
|
|
@@ -129,7 +130,7 @@ export { isSwitchTargetAccount, canSwitchIntoAccount, } from './session/accountS
|
|
|
129
130
|
export { resolveAccountDelegationAccess } from './session/accountDelegationAccess';
|
|
130
131
|
export type { AccountDelegationAccess, AccountDelegationNode, } from './session/accountDelegationAccess';
|
|
131
132
|
export { AccountDialogController, createAccountDialogController, } from './session/accountDialogController';
|
|
132
|
-
export type { AccountDialogControllerOptions, AccountDialogSnapshot, AccountDialogView, CommonsAvailability, PopupWindowHandle, SignInFailureReason, SignInFlowPhase, SignInFlowState, SignInProgress, } from './session/accountDialogController';
|
|
133
|
+
export type { AccountDialogControllerOptions, AccountDialogSnapshot, AccountDialogView, CommonsAvailability, ContextChoiceOutcome, PopupWindowHandle, SignInFailureReason, SignInFlowPhase, SignInFlowState, SignInProgress, } from './session/accountDialogController';
|
|
133
134
|
export { createWebAuthStateStore, createNativeAuthStateStore, createMemoryAuthStateStore, AUTH_STATE_STORAGE_KEY, } from './session/authStateStore';
|
|
134
135
|
export type { PersistedAuthState, AuthStateStore, NativeKeyValueStorage, } from './session/authStateStore';
|
|
135
136
|
export { createSharedMirroringAuthStateStore, decideSharedDeviceJoin, decideSharedDevicePublish, normalizeSharedDeviceSessionRead, publishProvenDeviceCredential, readLocalDeviceCredential, } from './session/sharedDeviceCredential';
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linked Accounts Methods Mixin
|
|
3
|
+
*
|
|
4
|
+
* External accounts (any Mastodon-API server, Bluesky) the current user has
|
|
5
|
+
* PROVEN they own by completing an OAuth authorization there. Oxy uses that
|
|
6
|
+
* authorization only to learn which account granted it and then discards the
|
|
7
|
+
* token — nothing here ever carries a third-party token.
|
|
8
|
+
*
|
|
9
|
+
* The flow is a top-level browser navigation: {@link startLinkedAccount} returns
|
|
10
|
+
* the other network's `authorizeUrl`; open it (a new tab, or
|
|
11
|
+
* `expo-web-browser`'s auth session on native); the network sends the browser
|
|
12
|
+
* to Oxy's callback, and Oxy sends it to `returnTo` with `?link_code=<code>` or
|
|
13
|
+
* `?link_error=<code>`. Pass the code to {@link completeLinkedAccount} with the
|
|
14
|
+
* same user signed in: that call, not the callback, creates the link, so an
|
|
15
|
+
* authorization someone else started can never link to them. `returnTo` must
|
|
16
|
+
* be a redirect URI registered on the first-party application `clientId` names.
|
|
17
|
+
*
|
|
18
|
+
* Wire types live in `@oxy.so/contracts` (`linkedAccounts.ts`).
|
|
19
|
+
*/
|
|
20
|
+
import type { LinkedAccount, LinkedAccountNetwork, ServiceLinkedAccountListResponse, StartLinkedAccountRequest, StartLinkedAccountResponse } from '@oxy.so/contracts';
|
|
21
|
+
import type { OxyServicesBase } from '../OxyServices.base';
|
|
22
|
+
export declare function OxyServicesLinkedAccountsMixin<T extends typeof OxyServicesBase>(Base: T): {
|
|
23
|
+
new (...args: any[]): {
|
|
24
|
+
/** Inherited from the auth mixin at runtime. */
|
|
25
|
+
makeServiceRequest: <R = unknown>(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", url: string, data?: unknown, userId?: string) => Promise<R>;
|
|
26
|
+
/**
|
|
27
|
+
* Begin linking an external account for the current user.
|
|
28
|
+
*
|
|
29
|
+
* @param network - `'activitypub'` (any Mastodon-API server) or `'atproto'` (Bluesky).
|
|
30
|
+
* @param options - `instance` for ActivityPub (`mastodon.social`, or
|
|
31
|
+
* `@user@mastodon.social`), `handle` for atproto; `clientId` + `returnTo`
|
|
32
|
+
* to be sent back to your app afterwards.
|
|
33
|
+
* @returns The URL to open, and when the attempt expires (ten minutes).
|
|
34
|
+
*/
|
|
35
|
+
startLinkedAccount(network: LinkedAccountNetwork, options: StartLinkedAccountRequest): Promise<StartLinkedAccountResponse>;
|
|
36
|
+
/**
|
|
37
|
+
* Finish a link with the `link_code` the callback appended to `returnTo`.
|
|
38
|
+
* Must be the user who started the flow: for anyone else the API answers
|
|
39
|
+
* 403 and burns the code. 409 when the account is already someone else's
|
|
40
|
+
* link. Repeating it within the code's five minutes returns the same link.
|
|
41
|
+
*/
|
|
42
|
+
completeLinkedAccount(code: string): Promise<LinkedAccount>;
|
|
43
|
+
/** The current user's live linked accounts, oldest first. */
|
|
44
|
+
listLinkedAccounts(): Promise<LinkedAccount[]>;
|
|
45
|
+
/**
|
|
46
|
+
* Unlink one of the current user's accounts. An ActivityPub link stops being
|
|
47
|
+
* published as an `alsoKnownAs` alias, and the external account can then be
|
|
48
|
+
* linked to another Oxy account.
|
|
49
|
+
*/
|
|
50
|
+
revokeLinkedAccount(linkedAccountId: string): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* SERVICE: a user's live linked accounts, each with the federated shadow
|
|
53
|
+
* user Oxy already holds for it (`federatedUserId`). Requires a service
|
|
54
|
+
* credential whose application holds the privileged `linked-accounts:read`.
|
|
55
|
+
*/
|
|
56
|
+
getLinkedAccountsForUser(userId: string): Promise<ServiceLinkedAccountListResponse>;
|
|
57
|
+
httpService: import("../HttpService").HttpService;
|
|
58
|
+
cloudURL: string;
|
|
59
|
+
config: import("../OxyServices.base").OxyConfig;
|
|
60
|
+
__resetTokensForTests(): void;
|
|
61
|
+
makeRequest<T_1>(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", url: string, data?: any, options?: import("../HttpService").RequestOptions): Promise<T_1>;
|
|
62
|
+
getBaseURL(): string;
|
|
63
|
+
getClient(): import("../HttpService").HttpService;
|
|
64
|
+
createLinkedClient(config: import("../OxyServices.base").OxyConfig): import("..").LinkedHttpClient;
|
|
65
|
+
getMetrics(): {
|
|
66
|
+
totalRequests: number;
|
|
67
|
+
successfulRequests: number;
|
|
68
|
+
failedRequests: number;
|
|
69
|
+
cacheHits: number;
|
|
70
|
+
cacheMisses: number;
|
|
71
|
+
averageResponseTime: number;
|
|
72
|
+
};
|
|
73
|
+
clearCache(): void;
|
|
74
|
+
clearCacheEntry(key: string): void;
|
|
75
|
+
clearCacheByPrefix(prefix: string): number;
|
|
76
|
+
getCacheStats(): {
|
|
77
|
+
size: number;
|
|
78
|
+
hits: number;
|
|
79
|
+
misses: number;
|
|
80
|
+
hitRate: number;
|
|
81
|
+
};
|
|
82
|
+
getCloudURL(): string;
|
|
83
|
+
setTokens(accessToken: string): void;
|
|
84
|
+
clearTokens(): void;
|
|
85
|
+
onTokensChanged(listener: (accessToken: string | null) => void): () => void;
|
|
86
|
+
_cachedUserId: string | null | undefined;
|
|
87
|
+
_cachedAccessToken: string | null;
|
|
88
|
+
getCurrentUserId(): string | null;
|
|
89
|
+
hasValidToken(): boolean;
|
|
90
|
+
getAccessToken(): string | null;
|
|
91
|
+
getAccessTokenExpiry(): number | null;
|
|
92
|
+
waitForAuth(timeoutMs?: number): Promise<boolean>;
|
|
93
|
+
withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
|
|
94
|
+
maxRetries?: number;
|
|
95
|
+
retryDelay?: number;
|
|
96
|
+
authTimeoutMs?: number;
|
|
97
|
+
}): Promise<T_1>;
|
|
98
|
+
validate(): Promise<boolean>;
|
|
99
|
+
handleError(error: unknown): Error;
|
|
100
|
+
healthCheck(): Promise<{
|
|
101
|
+
status: string;
|
|
102
|
+
users?: number;
|
|
103
|
+
timestamp?: string;
|
|
104
|
+
[key: string]: any;
|
|
105
|
+
}>;
|
|
106
|
+
};
|
|
107
|
+
} & T;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* User Management Methods Mixin
|
|
3
3
|
*/
|
|
4
|
+
import type { CreateOxyNotificationRequest } from '@oxy.so/contracts';
|
|
4
5
|
import type { User, Notification, NotificationPreferences, UserPreferences, SearchProfilesResponse, PrivacySettings } from '../models/interfaces';
|
|
5
6
|
import type { UserNameResponse, UserProfileUpdate, RecommendationRequest, RecommendationItem, ThemePreference } from '@oxy.so/contracts';
|
|
6
7
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
@@ -495,9 +496,13 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
|
|
|
495
496
|
*/
|
|
496
497
|
getUnreadCount(): Promise<number>;
|
|
497
498
|
/**
|
|
498
|
-
* Create notification
|
|
499
|
+
* Create a notification (`POST /notifications`; the API requires a service
|
|
500
|
+
* token whose application holds `notifications:write`). `type` is one of
|
|
501
|
+
* `OXY_NOTIFICATION_TYPES` from `@oxy.so/contracts` — `system` for a message
|
|
502
|
+
* from an Oxy service about the recipient's own account, with the recipient
|
|
503
|
+
* as actor and their profile as the entity.
|
|
499
504
|
*/
|
|
500
|
-
createNotification(data: Partial<Notification>): Promise<Notification>;
|
|
505
|
+
createNotification(data: CreateOxyNotificationRequest | Partial<Notification>): Promise<Notification>;
|
|
501
506
|
/**
|
|
502
507
|
* Mark notification as read
|
|
503
508
|
*/
|
|
@@ -34,6 +34,7 @@ import { OxyServicesNodesMixin } from './OxyServices.nodes';
|
|
|
34
34
|
import { OxyServicesFollowGraphMixin } from './OxyServices.followGraph';
|
|
35
35
|
import { OxyServicesInferenceMixin } from './OxyServices.inference';
|
|
36
36
|
import { OxyServicesDeviceBootMixin } from './OxyServices.deviceBoot';
|
|
37
|
+
import { OxyServicesLinkedAccountsMixin } from './OxyServices.linkedAccounts';
|
|
37
38
|
/**
|
|
38
39
|
* Instance shape of every mixin in the pipeline, intersected. The runtime
|
|
39
40
|
* `composeOxyServices()` produces a class whose instances expose all of
|
|
@@ -43,7 +44,7 @@ import { OxyServicesDeviceBootMixin } from './OxyServices.deviceBoot';
|
|
|
43
44
|
* If you add a new mixin to `MIXIN_PIPELINE`, add it here too so its methods
|
|
44
45
|
* are visible without a cast.
|
|
45
46
|
*/
|
|
46
|
-
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 OxyServicesAgencyMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesStoreMixin<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 OxyServicesChainsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesNodesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesFollowGraphMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesInferenceMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDeviceBootMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUtilityMixin<typeof OxyServicesBase>>>;
|
|
47
|
+
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 OxyServicesAgencyMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesStoreMixin<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 OxyServicesChainsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesNodesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesFollowGraphMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesInferenceMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDeviceBootMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLinkedAccountsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUtilityMixin<typeof OxyServicesBase>>>;
|
|
47
48
|
/**
|
|
48
49
|
* Constructor type for the fully composed mixin pipeline. Each mixin returns
|
|
49
50
|
* a new constructor that augments its input; reducing across the pipeline
|
|
@@ -99,6 +99,13 @@ export interface User {
|
|
|
99
99
|
* lists, profile search) — the two are separate fields on the User document.
|
|
100
100
|
*/
|
|
101
101
|
description?: string;
|
|
102
|
+
/**
|
|
103
|
+
* ActivityPub actor URIs this account is also known as — its live,
|
|
104
|
+
* OAuth-proven linked Mastodon-API accounts. Emitted by
|
|
105
|
+
* `GET /profiles/username/:username`; a relying app passes it to the shared
|
|
106
|
+
* actor builder (`@oxy.so/federation`) so its actor publishes it.
|
|
107
|
+
*/
|
|
108
|
+
alsoKnownAs?: string[];
|
|
102
109
|
phone?: string;
|
|
103
110
|
address?: string;
|
|
104
111
|
/** Legacy free-text birthday, kept for backward compatibility. Prefer `dateOfBirth`. */
|
|
@@ -237,6 +244,22 @@ export interface LoginResponse {
|
|
|
237
244
|
export interface Notification {
|
|
238
245
|
id: string;
|
|
239
246
|
message: string;
|
|
247
|
+
/** One of `OXY_NOTIFICATION_TYPES` (`@oxy.so/contracts`). */
|
|
248
|
+
type?: string;
|
|
249
|
+
/**
|
|
250
|
+
* `system` notifications only (with `message`): the text an Oxy service sent
|
|
251
|
+
* about the recipient's own account. Render these two instead of composing a
|
|
252
|
+
* sentence from actor + entity.
|
|
253
|
+
*/
|
|
254
|
+
title?: string;
|
|
255
|
+
/** `system` notifications only: an optional deep link (https or the sending app's scheme). */
|
|
256
|
+
url?: string;
|
|
257
|
+
/**
|
|
258
|
+
* What `entityId` names: `post` | `reply` | `profile`, or `app` — an opaque id
|
|
259
|
+
* in the notifying application's namespace (`system` notifications only).
|
|
260
|
+
*/
|
|
261
|
+
entityType?: string;
|
|
262
|
+
entityId?: string;
|
|
240
263
|
}
|
|
241
264
|
export interface Wallet {
|
|
242
265
|
id: string;
|
|
@@ -594,6 +617,13 @@ export interface ServiceAssetMetadata {
|
|
|
594
617
|
* play. Treat absence as "no adaptive stream, play the progressive original".
|
|
595
618
|
*/
|
|
596
619
|
hlsReadyAt?: string;
|
|
620
|
+
/**
|
|
621
|
+
* The Oxy user who owns the file (`null` for a system-owned file such as the
|
|
622
|
+
* federated media cache). PRESENT ONLY for Oxy's own applications (service
|
|
623
|
+
* tokens with `tier: 'internal'`), so a first-party service can check that an
|
|
624
|
+
* asset a user attached is really that user's. Absent for every other caller.
|
|
625
|
+
*/
|
|
626
|
+
ownerUserId?: string | null;
|
|
597
627
|
}
|
|
598
628
|
/**
|
|
599
629
|
* Reverse-lookup asset metadata returned by `POST /assets/service/by-sha256`.
|
|
@@ -161,6 +161,11 @@ export interface OxyPagesHeadersOptions {
|
|
|
161
161
|
* the app — is REMOVED from `script-src` and `connect-src`, so third-party
|
|
162
162
|
* measurement cannot run there even when the zone has Web Analytics enabled.
|
|
163
163
|
* A structural block in the policy the page is served with, not a build flag.
|
|
164
|
+
*
|
|
165
|
+
* It also sends `Cache-Control: no-transform`, which stops the edge from
|
|
166
|
+
* injecting the beacon at all — otherwise every page load logs a CSP
|
|
167
|
+
* violation for a script we never asked for. The rest of the value is Pages'
|
|
168
|
+
* own default, so caching is unchanged.
|
|
164
169
|
*/
|
|
165
170
|
sensitive?: boolean;
|
|
166
171
|
/**
|
|
@@ -233,6 +233,16 @@ export interface AccountDialogSnapshot {
|
|
|
233
233
|
* table of their own.
|
|
234
234
|
*/
|
|
235
235
|
backView: AccountDialogView | null;
|
|
236
|
+
/**
|
|
237
|
+
* Whether THIS client is signed in: a bearer is planted.
|
|
238
|
+
*
|
|
239
|
+
* Not the same question as "does the directory list anyone". The directory is
|
|
240
|
+
* the DEVICE's, and a device can go on listing a shared identity (Commons, or a
|
|
241
|
+
* sibling app) after this app signed out. A host names its sign-in entry, and
|
|
242
|
+
* whether a listed account reads as the current one, from this — never from
|
|
243
|
+
* the directory's size.
|
|
244
|
+
*/
|
|
245
|
+
hasSession: boolean;
|
|
236
246
|
/**
|
|
237
247
|
* The server-authoritative device directory — principals and the contexts
|
|
238
248
|
* each may act as (ADR 0002) — or `null` before the first read.
|
|
@@ -259,6 +269,20 @@ export interface AccountDialogSnapshot {
|
|
|
259
269
|
/** Whether Commons is installed on this device. See {@link CommonsAvailability}. */
|
|
260
270
|
commonsAvailability: CommonsAvailability;
|
|
261
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* What {@link AccountDialogController.chooseContext} did with a chosen row.
|
|
274
|
+
*
|
|
275
|
+
* - `'current'` — the row is already the active account; nothing to do.
|
|
276
|
+
* - `'switched'` — the switch to the row happened (signed in; or signed out,
|
|
277
|
+
* after the silent sign-in landed on a different pair and the chosen one was
|
|
278
|
+
* then activated).
|
|
279
|
+
* - `'signing-in'` — signed out: the row started "Continue with Oxy", and the
|
|
280
|
+
* sign-in flow (its own view, its own completion) owns what happens next —
|
|
281
|
+
* including closing the dialog, through `onSignedIn`.
|
|
282
|
+
* - `'failed'` — the switch was attempted and failed (the reason is `error`).
|
|
283
|
+
* - `'busy'` — another device mutation is in flight; the press was dropped.
|
|
284
|
+
*/
|
|
285
|
+
export type ContextChoiceOutcome = 'current' | 'switched' | 'signing-in' | 'failed' | 'busy';
|
|
262
286
|
/** Construction options for {@link AccountDialogController}. */
|
|
263
287
|
export interface AccountDialogControllerOptions {
|
|
264
288
|
/** The API client. Source of graph accounts, profiles, and the sign-in methods. */
|
|
@@ -525,6 +549,27 @@ export declare class AccountDialogController {
|
|
|
525
549
|
* device mutation is in flight ({@link isDeviceMutationInFlight}).
|
|
526
550
|
*/
|
|
527
551
|
activateContext(contextId: string): Promise<boolean>;
|
|
552
|
+
/**
|
|
553
|
+
* A device account row was chosen — the one entry point for "use this
|
|
554
|
+
* account", whether or not anybody is signed in here.
|
|
555
|
+
*
|
|
556
|
+
* Signed in, it is a switch: the active row is already the answer
|
|
557
|
+
* (`'current'`), any other row is {@link activateContext}.
|
|
558
|
+
*
|
|
559
|
+
* Signed out, it is NOT a switch. The row is on the list because the DEVICE
|
|
560
|
+
* still holds that identity (Commons' shared identity, or a sibling app's
|
|
561
|
+
* session), not because this app does, so there is no bearer to activate a
|
|
562
|
+
* context with — and the active-row short-circuit used to read that stale
|
|
563
|
+
* "active" as "already signed in" and close the sheet on a signed-out app
|
|
564
|
+
* (OxyHQ/oxy#1375 item 20). Choosing the row is "Continue as @handle": the
|
|
565
|
+
* same {@link signInWithOxy} path as the "Continue with Oxy" button, which
|
|
566
|
+
* mints silently from the shared identity and otherwise falls back to the
|
|
567
|
+
* request. When that silent mint lands on a different pair from the one
|
|
568
|
+
* chosen (a device holding more than one person, or an organization row), the
|
|
569
|
+
* chosen row is then activated under the new bearer, so the press ends where
|
|
570
|
+
* it pointed.
|
|
571
|
+
*/
|
|
572
|
+
chooseContext(contextId: string): Promise<ContextChoiceOutcome>;
|
|
528
573
|
/**
|
|
529
574
|
* Remove ONE `principal → account` pair, and only that pair.
|
|
530
575
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxy.so/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.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",
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
"@oxy.so/telemetry": "^1.1.1",
|
|
119
119
|
"@noble/ciphers": "^1.3.0",
|
|
120
120
|
"@noble/hashes": "^1.8.0",
|
|
121
|
-
"@oxy.so/contracts": "^1.
|
|
121
|
+
"@oxy.so/contracts": "^1.5.0",
|
|
122
122
|
"@oxy.so/protocol": "^1.1.1",
|
|
123
123
|
"@scure/bip39": "^1.6.0",
|
|
124
124
|
"buffer": "^6.0.3",
|