@oxy.so/core 1.10.0 → 1.11.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.
Files changed (35) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/crypto/deviceBackup.js +75 -0
  3. package/dist/cjs/crypto/keyManager.js +285 -12
  4. package/dist/cjs/mixins/OxyServices.linkedAccounts.js +79 -0
  5. package/dist/cjs/mixins/OxyServices.user.js +5 -1
  6. package/dist/cjs/mixins/index.js +4 -0
  7. package/dist/cjs/server/securityHeaders.js +3 -0
  8. package/dist/esm/.tsbuildinfo +1 -1
  9. package/dist/esm/crypto/deviceBackup.js +70 -0
  10. package/dist/esm/crypto/keyManager.js +252 -12
  11. package/dist/esm/mixins/OxyServices.linkedAccounts.js +76 -0
  12. package/dist/esm/mixins/OxyServices.user.js +5 -1
  13. package/dist/esm/mixins/index.js +4 -0
  14. package/dist/esm/server/securityHeaders.js +3 -0
  15. package/dist/types/.tsbuildinfo +1 -1
  16. package/dist/types/crypto/deviceBackup.d.ts +60 -0
  17. package/dist/types/crypto/keyManager.d.ts +79 -12
  18. package/dist/types/index.d.ts +1 -0
  19. package/dist/types/mixins/OxyServices.linkedAccounts.d.ts +107 -0
  20. package/dist/types/mixins/OxyServices.user.d.ts +7 -2
  21. package/dist/types/mixins/index.d.ts +2 -1
  22. package/dist/types/models/interfaces.d.ts +30 -0
  23. package/dist/types/server/securityHeaders.d.ts +5 -0
  24. package/package.json +2 -2
  25. package/src/crypto/__tests__/keyManager.deviceBackup.test.ts +310 -0
  26. package/src/crypto/deviceBackup.ts +108 -0
  27. package/src/crypto/keyManager.ts +278 -13
  28. package/src/index.ts +1 -0
  29. package/src/mixins/OxyServices.linkedAccounts.ts +129 -0
  30. package/src/mixins/OxyServices.user.ts +7 -2
  31. package/src/mixins/__tests__/linkedAccounts.test.ts +109 -0
  32. package/src/mixins/index.ts +6 -0
  33. package/src/models/interfaces.ts +30 -1
  34. package/src/server/__tests__/securityHeaders.test.ts +7 -0
  35. package/src/server/securityHeaders.ts +8 -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 a `lost` identity from an independent,
538
- * `key_v1`-surviving source WITHOUT the user re-entering their recovery phrase.
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
- * Gated on {@link getIdentityStatus} being `lost` (marker present, keys empty):
541
- * - `present` / `absent` → `not-lost` (nothing to recover / nothing lost)
542
- * - `unavailable` → `unavailable` (keychain locked; retry later)
543
- *
544
- * Rungs, tried in order, each fully validated (well-formed + derive-match +
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 */
@@ -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';
@@ -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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxy.so/core",
3
- "version": "1.10.0",
3
+ "version": "1.11.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.4.0",
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",