@oxyhq/services 24.0.0 → 24.0.2
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/android/src/main/java/so/oxy/session/OxyBackgroundSessionModule.kt +42 -27
- package/android/src/main/java/so/oxy/session/OxyBackgroundSessionStore.kt +11 -2
- package/lib/commonjs/ui/screens/ProfileScreen.js +15 -4
- package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -1
- package/lib/commonjs/ui/session/backgroundSession.js +37 -12
- package/lib/commonjs/ui/session/backgroundSession.js.map +1 -1
- package/lib/commonjs/ui/session/useBackgroundSessionSync.js +4 -5
- package/lib/commonjs/ui/session/useBackgroundSessionSync.js.map +1 -1
- package/lib/commonjs/ui/utils/userUtils.js +11 -21
- package/lib/commonjs/ui/utils/userUtils.js.map +1 -1
- package/lib/module/ui/screens/ProfileScreen.js +15 -4
- package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
- package/lib/module/ui/session/backgroundSession.js +37 -12
- package/lib/module/ui/session/backgroundSession.js.map +1 -1
- package/lib/module/ui/session/useBackgroundSessionSync.js +4 -5
- package/lib/module/ui/session/useBackgroundSessionSync.js.map +1 -1
- package/lib/module/ui/utils/userUtils.js +12 -22
- package/lib/module/ui/utils/userUtils.js.map +1 -1
- package/lib/typescript/commonjs/ui/screens/ProfileScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/session/backgroundSession.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/session/useBackgroundSessionSync.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/userUtils.d.ts +5 -7
- package/lib/typescript/commonjs/ui/utils/userUtils.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/ProfileScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/session/backgroundSession.d.ts.map +1 -1
- package/lib/typescript/module/ui/session/useBackgroundSessionSync.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/userUtils.d.ts +5 -7
- package/lib/typescript/module/ui/utils/userUtils.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/ui/screens/ProfileScreen.tsx +17 -3
- package/src/ui/session/__tests__/backgroundSession.test.ts +63 -8
- package/src/ui/session/backgroundSession.ts +65 -20
- package/src/ui/session/useBackgroundSessionSync.ts +4 -5
- package/src/ui/utils/userUtils.ts +10 -29
|
@@ -27,12 +27,16 @@
|
|
|
27
27
|
* live in different stores with one writer each, so there is nothing to keep in
|
|
28
28
|
* lockstep and no state that can disagree.
|
|
29
29
|
*
|
|
30
|
-
* Android only
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
30
|
+
* ## Android only, and web is refused rather than merely unsupported
|
|
31
|
+
*
|
|
32
|
+
* Web is gated off by an explicit platform check, not by the native module being
|
|
33
|
+
* absent — provisioning in a browser origin would be a security downgrade, so it
|
|
34
|
+
* must not depend on an incidental fact. See {@link loadNativeModule}. iOS has no
|
|
35
|
+
* implementation yet and resolves to no module, so every function here degrades to
|
|
36
|
+
* a no-op: an app can enable this and lose nothing on other platforms.
|
|
34
37
|
*/
|
|
35
38
|
import { requireOptionalNativeModule } from 'expo-modules-core';
|
|
39
|
+
import { Platform } from 'react-native';
|
|
36
40
|
import { logger } from '@oxyhq/core';
|
|
37
41
|
|
|
38
42
|
/**
|
|
@@ -52,7 +56,30 @@ const RENEW_WHEN_REMAINING_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
52
56
|
let nativeModule;
|
|
53
57
|
|
|
54
58
|
/**
|
|
55
|
-
* Resolve the native module once.
|
|
59
|
+
* Resolve the native module once — and refuse outright on web.
|
|
60
|
+
*
|
|
61
|
+
* ## Web is excluded on purpose, and not merely because the module is absent
|
|
62
|
+
*
|
|
63
|
+
* The credential is deliberately NON-ROTATING and long-lived, which is safe only
|
|
64
|
+
* because native background code is its sole consumer and nobody rotates it. A
|
|
65
|
+
* browser origin already holds the ROTATING `deviceSecret`, and it has no
|
|
66
|
+
* background worker to serve — so provisioning there would add a strictly weaker
|
|
67
|
+
* long-lived secret alongside a stronger short-lived one. That is a downgrade, not
|
|
68
|
+
* a harmless no-op, and the harm lands at the PROVISION CALL (which mints a live
|
|
69
|
+
* server-side credential) regardless of whether anything manages to store it.
|
|
70
|
+
*
|
|
71
|
+
* That property must not rest on `requireOptionalNativeModule` happening to return
|
|
72
|
+
* `null` on web. It does today — the module is registered for android only — but
|
|
73
|
+
* that is an incidental fact a future web shim or a web-build native-module mock
|
|
74
|
+
* would quietly reverse. So the platform is checked FIRST, before we even ask the
|
|
75
|
+
* registry, and every entry point in this module inherits the gate.
|
|
76
|
+
*
|
|
77
|
+
* (A pre-deploy server makes this visible too: oxy-api's `sessionDevice` router
|
|
78
|
+
* applies `requireSameSiteOrigin` path-agnostically, so an unmatched path answers
|
|
79
|
+
* `404` to a native caller but `403 BAD_ORIGIN` to a browser one. Core's degrade is
|
|
80
|
+
* deliberately narrow — `404 → null` only — because a real origin misconfiguration
|
|
81
|
+
* on a CURRENT server also returns 403, and swallowing that would hide a genuine
|
|
82
|
+
* bug. Platform gating belongs here, where the platform is actually known.)
|
|
56
83
|
*
|
|
57
84
|
* `requireOptionalNativeModule` (a static import Metro always resolves) rather
|
|
58
85
|
* than a dynamic `import(moduleName)`: the latter compiles to `require(variable)`
|
|
@@ -62,6 +89,10 @@ let nativeModule;
|
|
|
62
89
|
*/
|
|
63
90
|
function loadNativeModule() {
|
|
64
91
|
if (nativeModule === undefined) {
|
|
92
|
+
if (Platform.OS === 'web') {
|
|
93
|
+
nativeModule = null;
|
|
94
|
+
return nativeModule;
|
|
95
|
+
}
|
|
65
96
|
const native = requireOptionalNativeModule('OxyBackgroundSession');
|
|
66
97
|
nativeModule = native && typeof native.put === 'function' && typeof native.clear === 'function' && typeof native.peek === 'function' ? {
|
|
67
98
|
put: native.put.bind(native),
|
|
@@ -205,13 +236,7 @@ export async function syncBackgroundSession(input) {
|
|
|
205
236
|
});
|
|
206
237
|
return;
|
|
207
238
|
}
|
|
208
|
-
const persisted = await native.put(
|
|
209
|
-
baseUrl: oxyServices.getBaseURL(),
|
|
210
|
-
deviceId: provisioned.deviceId,
|
|
211
|
-
secret: provisioned.secret,
|
|
212
|
-
accountId: provisioned.accountId,
|
|
213
|
-
expiresAt
|
|
214
|
-
});
|
|
239
|
+
const persisted = await native.put(oxyServices.getBaseURL(), provisioned.deviceId, provisioned.secret, provisioned.accountId, expiresAt);
|
|
215
240
|
if (!persisted) {
|
|
216
241
|
logger.warn('[backgroundSession] the native store did not retain the background credential; background refreshes stay signed out until the next attempt', {
|
|
217
242
|
component: 'backgroundSession'
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["requireOptionalNativeModule","logger","RENEW_WHEN_REMAINING_MS","nativeModule","loadNativeModule","undefined","native","put","clear","peek","bind","isBackgroundSessionSupported","clearBackgroundSession","error","warn","component","assertProvisioningAvailable","oxyServices","provisionBackgroundCredential","message","Error","syncBackgroundSession","input","userId","canUsePrivateApi","isCurrent","stored","accountId","isForCurrentAccount","hasLifeLeft","expiresAt","Date","now","provisioned","parse","Number","isFinite","persisted","
|
|
1
|
+
{"version":3,"names":["requireOptionalNativeModule","Platform","logger","RENEW_WHEN_REMAINING_MS","nativeModule","loadNativeModule","undefined","OS","native","put","clear","peek","bind","isBackgroundSessionSupported","clearBackgroundSession","error","warn","component","assertProvisioningAvailable","oxyServices","provisionBackgroundCredential","message","Error","syncBackgroundSession","input","userId","canUsePrivateApi","isCurrent","stored","accountId","isForCurrentAccount","hasLifeLeft","expiresAt","Date","now","provisioned","parse","Number","isFinite","persisted","getBaseURL","deviceId","secret"],"sourceRoot":"../../../../src","sources":["ui/session/backgroundSession.ts"],"mappings":"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","ignoreList":[]}
|
|
@@ -33,11 +33,10 @@ export function useBackgroundSessionSync({
|
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
35
|
if (!enabled) {
|
|
36
|
-
// An app that
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
// memoized keystore open.
|
|
36
|
+
// An app that leaves the feature off must not keep a credential THIS app
|
|
37
|
+
// provisioned earlier. The store is package-scoped (see
|
|
38
|
+
// OxyBackgroundSessionStore), so this cannot revoke a sibling Oxy app's
|
|
39
|
+
// credential under the shared UID.
|
|
41
40
|
void clearBackgroundSession();
|
|
42
41
|
return;
|
|
43
42
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["useEffect","AppState","clearBackgroundSession","isBackgroundSessionSupported","syncBackgroundSession","useBackgroundSessionSync","enabled","oxyServices","userId","canUsePrivateApi","current","run","isCurrent","subscription","addEventListener","state","remove"],"sourceRoot":"../../../../src","sources":["ui/session/useBackgroundSessionSync.ts"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASA,SAAS,QAAQ,OAAO;AACjC,SAASC,QAAQ,QAAQ,cAAc;AAEvC,SACEC,sBAAsB,EACtBC,4BAA4B,EAC5BC,qBAAqB,QAChB,wBAAqB;AAY5B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,wBAAwBA,CAAC;EACvCC,OAAO;EACPC,WAAW;EACXC,MAAM;EACNC;AAC6B,CAAC,EAAQ;EACtCT,SAAS,CAAC,MAAM;IACd,IAAI,CAACG,4BAA4B,CAAC,CAAC,EAAE;MACnC;IACF;IAEA,IAAI,CAACG,OAAO,EAAE;MACZ;MACA;MACA;MACA;MACA
|
|
1
|
+
{"version":3,"names":["useEffect","AppState","clearBackgroundSession","isBackgroundSessionSupported","syncBackgroundSession","useBackgroundSessionSync","enabled","oxyServices","userId","canUsePrivateApi","current","run","isCurrent","subscription","addEventListener","state","remove"],"sourceRoot":"../../../../src","sources":["ui/session/useBackgroundSessionSync.ts"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASA,SAAS,QAAQ,OAAO;AACjC,SAASC,QAAQ,QAAQ,cAAc;AAEvC,SACEC,sBAAsB,EACtBC,4BAA4B,EAC5BC,qBAAqB,QAChB,wBAAqB;AAY5B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,wBAAwBA,CAAC;EACvCC,OAAO;EACPC,WAAW;EACXC,MAAM;EACNC;AAC6B,CAAC,EAAQ;EACtCT,SAAS,CAAC,MAAM;IACd,IAAI,CAACG,4BAA4B,CAAC,CAAC,EAAE;MACnC;IACF;IAEA,IAAI,CAACG,OAAO,EAAE;MACZ;MACA;MACA;MACA;MACA,KAAKJ,sBAAsB,CAAC,CAAC;MAC7B;IACF;;IAEA;IACA;IACA,IAAIQ,OAAO,GAAG,IAAI;IAClB;IACA;IACA;IACA;IACA;IACA,MAAMC,GAAG,GAAGA,CAAA,KAAM;MAChB,KAAKP,qBAAqB,CAAC;QACzBG,WAAW;QACXC,MAAM;QACNC,gBAAgB;QAChBG,SAAS,EAAEA,CAAA,KAAMF;MACnB,CAAC,CAAC;IACJ,CAAC;IAEDC,GAAG,CAAC,CAAC;IACL,MAAME,YAAY,GAAGZ,QAAQ,CAACa,gBAAgB,CAAC,QAAQ,EAAGC,KAAK,IAAK;MAClE,IAAIA,KAAK,KAAK,QAAQ,EAAE;QACtBJ,GAAG,CAAC,CAAC;MACP;IACF,CAAC,CAAC;IAEF,OAAO,MAAM;MACXD,OAAO,GAAG,KAAK;MACfG,YAAY,CAACG,MAAM,CAAC,CAAC;MACrB;MACA;MACA;MACA;IACF,CAAC;EACH,CAAC,EAAE,CAACV,OAAO,EAAEC,WAAW,EAAEC,MAAM,EAAEC,gBAAgB,CAAC,CAAC;AACtD","ignoreList":[]}
|
|
@@ -4,12 +4,11 @@
|
|
|
4
4
|
* User display name and date formatting utilities.
|
|
5
5
|
*
|
|
6
6
|
* Display-name helpers follow the API contract: prefer `name.displayName` when
|
|
7
|
-
* present, otherwise fall back to the normalized handle
|
|
8
|
-
* `
|
|
9
|
-
* account-switcher surfaces without a handle.
|
|
7
|
+
* present, otherwise fall back to the normalized handle via
|
|
8
|
+
* `getNormalizedUserHandle`.
|
|
10
9
|
*/
|
|
11
10
|
|
|
12
|
-
import {
|
|
11
|
+
import { getNormalizedUserHandle } from '@oxyhq/core';
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
14
|
* Formats a date string to a readable format (e.g., "Feb 21, 2025")
|
|
@@ -37,37 +36,28 @@ function readDisplayName(user) {
|
|
|
37
36
|
/**
|
|
38
37
|
* Gets a display name from user data.
|
|
39
38
|
*
|
|
40
|
-
* Prefers API `name.displayName`, then the normalized handle
|
|
41
|
-
* account-switcher fallback chain.
|
|
39
|
+
* Prefers API `name.displayName`, then the normalized handle.
|
|
42
40
|
*/
|
|
43
|
-
export const getDisplayName =
|
|
41
|
+
export const getDisplayName = user => {
|
|
44
42
|
const displayName = readDisplayName(user);
|
|
45
43
|
if (displayName) return displayName;
|
|
46
|
-
|
|
47
|
-
if (handle) return handle;
|
|
48
|
-
return coreGetAccountDisplayName(user, locale);
|
|
44
|
+
return getNormalizedUserHandle(user) ?? '';
|
|
49
45
|
};
|
|
50
46
|
|
|
51
47
|
/**
|
|
52
48
|
* Gets a short display name (first token) for compact UI.
|
|
53
49
|
*/
|
|
54
|
-
export const getShortDisplayName =
|
|
50
|
+
export const getShortDisplayName = user => {
|
|
55
51
|
const displayName = readDisplayName(user);
|
|
56
52
|
if (displayName) {
|
|
57
53
|
const firstToken = displayName.split(/\s+/).find(Boolean);
|
|
58
54
|
if (firstToken) return firstToken;
|
|
59
55
|
}
|
|
60
|
-
const name = user?.name;
|
|
61
|
-
if (name && typeof name === 'object') {
|
|
62
|
-
const first = typeof name.first === 'string' ? name.first.trim() : '';
|
|
63
|
-
if (first) return first.split(/\s+/)[0] ?? first;
|
|
64
|
-
const full = typeof name.full === 'string' ? name.full.trim() : '';
|
|
65
|
-
if (full) return full.split(/\s+/)[0] ?? full;
|
|
66
|
-
} else if (typeof name === 'string' && name.trim()) {
|
|
67
|
-
return name.trim().split(/\s+/)[0] ?? name.trim();
|
|
68
|
-
}
|
|
69
56
|
const handle = getNormalizedUserHandle(user);
|
|
70
|
-
if (handle)
|
|
71
|
-
|
|
57
|
+
if (handle) {
|
|
58
|
+
const firstToken = handle.split(/\s+/).find(Boolean);
|
|
59
|
+
return firstToken ?? handle;
|
|
60
|
+
}
|
|
61
|
+
return '';
|
|
72
62
|
};
|
|
73
63
|
//# sourceMappingURL=userUtils.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["
|
|
1
|
+
{"version":3,"names":["getNormalizedUserHandle","formatDate","dateString","date","Date","Number","isNaN","getTime","toLocaleDateString","month","day","year","readDisplayName","user","name","displayName","trim","getDisplayName","getShortDisplayName","firstToken","split","find","Boolean","handle"],"sourceRoot":"../../../../src","sources":["ui/utils/userUtils.ts"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,SAASA,uBAAuB,QAAmC,aAAa;;AAEhF;AACA;AACA;AACA,OAAO,MAAMC,UAAU,GAAIC,UAA4C,IAAa;EAClF,IAAI,CAACA,UAAU,EAAE,OAAO,EAAE;EAE1B,IAAI;IACF,MAAMC,IAAI,GAAGD,UAAU,YAAYE,IAAI,GAAGF,UAAU,GAAG,IAAIE,IAAI,CAACF,UAAU,CAAC;IAC3E,IAAIG,MAAM,CAACC,KAAK,CAACH,IAAI,CAACI,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE;IAE3C,OAAOJ,IAAI,CAACK,kBAAkB,CAAC,OAAO,EAAE;MACtCC,KAAK,EAAE,OAAO;MACdC,GAAG,EAAE,SAAS;MACdC,IAAI,EAAE;IACR,CAAC,CAAC;EACJ,CAAC,CAAC,MAAM;IACN,OAAO,EAAE;EACX;AACF,CAAC;AAED,SAASC,eAAeA,CAACC,IAA6C,EAAU;EAC9E,MAAMC,IAAI,GAAGD,IAAI,EAAEC,IAAI;EACvB,IAAI,CAACA,IAAI,IAAI,OAAOA,IAAI,KAAK,QAAQ,EAAE,OAAO,EAAE;EAChD,OAAO,OAAOA,IAAI,CAACC,WAAW,KAAK,QAAQ,GAAGD,IAAI,CAACC,WAAW,CAACC,IAAI,CAAC,CAAC,GAAG,EAAE;AAC5E;;AAEA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMC,cAAc,GACzBJ,IAA6C,IAClC;EACX,MAAME,WAAW,GAAGH,eAAe,CAACC,IAAI,CAAC;EACzC,IAAIE,WAAW,EAAE,OAAOA,WAAW;EACnC,OAAOf,uBAAuB,CAACa,IAAI,CAAC,IAAI,EAAE;AAC5C,CAAC;;AAED;AACA;AACA;AACA,OAAO,MAAMK,mBAAmB,GAC9BL,IAA6C,IAClC;EACX,MAAME,WAAW,GAAGH,eAAe,CAACC,IAAI,CAAC;EACzC,IAAIE,WAAW,EAAE;IACf,MAAMI,UAAU,GAAGJ,WAAW,CAACK,KAAK,CAAC,KAAK,CAAC,CAACC,IAAI,CAACC,OAAO,CAAC;IACzD,IAAIH,UAAU,EAAE,OAAOA,UAAU;EACnC;EAEA,MAAMI,MAAM,GAAGvB,uBAAuB,CAACa,IAAI,CAAC;EAC5C,IAAIU,MAAM,EAAE;IACV,MAAMJ,UAAU,GAAGI,MAAM,CAACH,KAAK,CAAC,KAAK,CAAC,CAACC,IAAI,CAACC,OAAO,CAAC;IACpD,OAAOH,UAAU,IAAII,MAAM;EAC7B;EAEA,OAAO,EAAE;AACX,CAAC","ignoreList":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProfileScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/ProfileScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAe3D,UAAU,kBAAmB,SAAQ,eAAe;IAChD,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAOD,QAAA,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,
|
|
1
|
+
{"version":3,"file":"ProfileScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/ProfileScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAe3D,UAAU,kBAAmB,SAAQ,eAAe;IAChD,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAOD,QAAA,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAkT/C,CAAC;AAuCF,eAAe,aAAa,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backgroundSession.d.ts","sourceRoot":"","sources":["../../../../../src/ui/session/backgroundSession.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"backgroundSession.d.ts","sourceRoot":"","sources":["../../../../../src/ui/session/backgroundSession.ts"],"names":[],"mappings":"AAqCA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAyG/C,yEAAyE;AACzE,wBAAgB,4BAA4B,IAAI,OAAO,CAEtD;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC,CAa5D;AAED,MAAM,WAAW,0BAA0B;IACzC,WAAW,EAAE,WAAW,CAAC;IACzB,wDAAwD;IACxD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,qEAAqE;IACrE,gBAAgB,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,SAAS,EAAE,MAAM,OAAO,CAAC;CAC1B;AA8BD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,qBAAqB,CAAC,KAAK,EAAE,0BAA0B,GAAG,OAAO,CAAC,IAAI,CAAC,CAqF5F"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useBackgroundSessionSync.d.ts","sourceRoot":"","sources":["../../../../../src/ui/session/useBackgroundSessionSync.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAO/C,MAAM,WAAW,6BAA6B;IAC5C,4CAA4C;IAC5C,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,WAAW,CAAC;IACzB,wDAAwD;IACxD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,kEAAkE;IAClE,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,OAAO,EACP,WAAW,EACX,MAAM,EACN,gBAAgB,GACjB,EAAE,6BAA6B,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"useBackgroundSessionSync.d.ts","sourceRoot":"","sources":["../../../../../src/ui/session/useBackgroundSessionSync.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAO/C,MAAM,WAAW,6BAA6B;IAC5C,4CAA4C;IAC5C,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,WAAW,CAAC;IACzB,wDAAwD;IACxD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,kEAAkE;IAClE,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,OAAO,EACP,WAAW,EACX,MAAM,EACN,gBAAgB,GACjB,EAAE,6BAA6B,GAAG,IAAI,CAgDtC"}
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* User display name and date formatting utilities.
|
|
3
3
|
*
|
|
4
4
|
* Display-name helpers follow the API contract: prefer `name.displayName` when
|
|
5
|
-
* present, otherwise fall back to the normalized handle
|
|
6
|
-
* `
|
|
7
|
-
* account-switcher surfaces without a handle.
|
|
5
|
+
* present, otherwise fall back to the normalized handle via
|
|
6
|
+
* `getNormalizedUserHandle`.
|
|
8
7
|
*/
|
|
9
8
|
import { type DisplayNameUserShape } from '@oxyhq/core';
|
|
10
9
|
/**
|
|
@@ -14,12 +13,11 @@ export declare const formatDate: (dateString: string | undefined | null | Date)
|
|
|
14
13
|
/**
|
|
15
14
|
* Gets a display name from user data.
|
|
16
15
|
*
|
|
17
|
-
* Prefers API `name.displayName`, then the normalized handle
|
|
18
|
-
* account-switcher fallback chain.
|
|
16
|
+
* Prefers API `name.displayName`, then the normalized handle.
|
|
19
17
|
*/
|
|
20
|
-
export declare const getDisplayName: (user: DisplayNameUserShape | null | undefined
|
|
18
|
+
export declare const getDisplayName: (user: DisplayNameUserShape | null | undefined) => string;
|
|
21
19
|
/**
|
|
22
20
|
* Gets a short display name (first token) for compact UI.
|
|
23
21
|
*/
|
|
24
|
-
export declare const getShortDisplayName: (user: DisplayNameUserShape | null | undefined
|
|
22
|
+
export declare const getShortDisplayName: (user: DisplayNameUserShape | null | undefined) => string;
|
|
25
23
|
//# sourceMappingURL=userUtils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"userUtils.d.ts","sourceRoot":"","sources":["../../../../../src/ui/utils/userUtils.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"userUtils.d.ts","sourceRoot":"","sources":["../../../../../src/ui/utils/userUtils.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAA2B,KAAK,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEjF;;GAEG;AACH,eAAO,MAAM,UAAU,GAAI,YAAY,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,IAAI,KAAG,MAezE,CAAC;AAQF;;;;GAIG;AACH,eAAO,MAAM,cAAc,GACzB,MAAM,oBAAoB,GAAG,IAAI,GAAG,SAAS,KAC5C,MAIF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,mBAAmB,GAC9B,MAAM,oBAAoB,GAAG,IAAI,GAAG,SAAS,KAC5C,MAcF,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProfileScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/ProfileScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAqB,CAAC;AAe3D,UAAU,kBAAmB,SAAQ,eAAe;IAChD,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAOD,QAAA,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,
|
|
1
|
+
{"version":3,"file":"ProfileScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/ProfileScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAqB,CAAC;AAe3D,UAAU,kBAAmB,SAAQ,eAAe;IAChD,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAOD,QAAA,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAkT/C,CAAC;AAuCF,eAAe,aAAa,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backgroundSession.d.ts","sourceRoot":"","sources":["../../../../../src/ui/session/backgroundSession.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"backgroundSession.d.ts","sourceRoot":"","sources":["../../../../../src/ui/session/backgroundSession.ts"],"names":[],"mappings":"AAqCA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAyG/C,yEAAyE;AACzE,wBAAgB,4BAA4B,IAAI,OAAO,CAEtD;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC,CAa5D;AAED,MAAM,WAAW,0BAA0B;IACzC,WAAW,EAAE,WAAW,CAAC;IACzB,wDAAwD;IACxD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,qEAAqE;IACrE,gBAAgB,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,SAAS,EAAE,MAAM,OAAO,CAAC;CAC1B;AA8BD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,qBAAqB,CAAC,KAAK,EAAE,0BAA0B,GAAG,OAAO,CAAC,IAAI,CAAC,CAqF5F"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useBackgroundSessionSync.d.ts","sourceRoot":"","sources":["../../../../../src/ui/session/useBackgroundSessionSync.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAO/C,MAAM,WAAW,6BAA6B;IAC5C,4CAA4C;IAC5C,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,WAAW,CAAC;IACzB,wDAAwD;IACxD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,kEAAkE;IAClE,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,OAAO,EACP,WAAW,EACX,MAAM,EACN,gBAAgB,GACjB,EAAE,6BAA6B,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"useBackgroundSessionSync.d.ts","sourceRoot":"","sources":["../../../../../src/ui/session/useBackgroundSessionSync.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAO/C,MAAM,WAAW,6BAA6B;IAC5C,4CAA4C;IAC5C,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,WAAW,CAAC;IACzB,wDAAwD;IACxD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,kEAAkE;IAClE,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,OAAO,EACP,WAAW,EACX,MAAM,EACN,gBAAgB,GACjB,EAAE,6BAA6B,GAAG,IAAI,CAgDtC"}
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* User display name and date formatting utilities.
|
|
3
3
|
*
|
|
4
4
|
* Display-name helpers follow the API contract: prefer `name.displayName` when
|
|
5
|
-
* present, otherwise fall back to the normalized handle
|
|
6
|
-
* `
|
|
7
|
-
* account-switcher surfaces without a handle.
|
|
5
|
+
* present, otherwise fall back to the normalized handle via
|
|
6
|
+
* `getNormalizedUserHandle`.
|
|
8
7
|
*/
|
|
9
8
|
import { type DisplayNameUserShape } from '@oxyhq/core';
|
|
10
9
|
/**
|
|
@@ -14,12 +13,11 @@ export declare const formatDate: (dateString: string | undefined | null | Date)
|
|
|
14
13
|
/**
|
|
15
14
|
* Gets a display name from user data.
|
|
16
15
|
*
|
|
17
|
-
* Prefers API `name.displayName`, then the normalized handle
|
|
18
|
-
* account-switcher fallback chain.
|
|
16
|
+
* Prefers API `name.displayName`, then the normalized handle.
|
|
19
17
|
*/
|
|
20
|
-
export declare const getDisplayName: (user: DisplayNameUserShape | null | undefined
|
|
18
|
+
export declare const getDisplayName: (user: DisplayNameUserShape | null | undefined) => string;
|
|
21
19
|
/**
|
|
22
20
|
* Gets a short display name (first token) for compact UI.
|
|
23
21
|
*/
|
|
24
|
-
export declare const getShortDisplayName: (user: DisplayNameUserShape | null | undefined
|
|
22
|
+
export declare const getShortDisplayName: (user: DisplayNameUserShape | null | undefined) => string;
|
|
25
23
|
//# sourceMappingURL=userUtils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"userUtils.d.ts","sourceRoot":"","sources":["../../../../../src/ui/utils/userUtils.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"userUtils.d.ts","sourceRoot":"","sources":["../../../../../src/ui/utils/userUtils.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAA2B,KAAK,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEjF;;GAEG;AACH,eAAO,MAAM,UAAU,GAAI,YAAY,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,IAAI,KAAG,MAezE,CAAC;AAQF;;;;GAIG;AACH,eAAO,MAAM,cAAc,GACzB,MAAM,oBAAoB,GAAG,IAAI,GAAG,SAAS,KAC5C,MAIF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,mBAAmB,GAC9B,MAAM,oBAAoB,GAAG,IAAI,GAAG,SAAS,KAC5C,MAcF,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/services",
|
|
3
|
-
"version": "24.0.
|
|
3
|
+
"version": "24.0.2",
|
|
4
4
|
"description": "OxyHQ Expo/React Native SDK — UI components, screens, and native features",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
"@commitlint/cli": "^21.2.1",
|
|
149
149
|
"@commitlint/config-conventional": "^21.2.0",
|
|
150
150
|
"@expo/vector-icons": "^15.0.3",
|
|
151
|
-
"@oxyhq/core": "15.0.
|
|
151
|
+
"@oxyhq/core": "15.0.1",
|
|
152
152
|
"@react-native-async-storage/async-storage": "^2.0.0",
|
|
153
153
|
"@react-native-community/netinfo": "^11.4.1",
|
|
154
154
|
"@react-native/babel-preset": "^0.86.0",
|
|
@@ -135,8 +135,13 @@ const ProfileScreen: React.FC<ProfileScreenProps> = ({ userId, username, theme,
|
|
|
135
135
|
// title (a stable "Profile" fallback before it resolves), so the banner +
|
|
136
136
|
// overlapping avatar scroll as content UNDER the shared gradient nav bar.
|
|
137
137
|
// `onImage` tone keeps the title + close legible over the colored banner.
|
|
138
|
+
// Ends in a string, deliberately: `getNormalizedUserHandle` answers `null` for a
|
|
139
|
+
// user it cannot normalise, and letting that null travel makes this `string | null`
|
|
140
|
+
// — which does not satisfy `Avatar.name` and does not typecheck. Empty is the honest
|
|
141
|
+
// bottom of the chain and every reader below already treats it as falsy
|
|
142
|
+
// (`displayName || t('profile.title')`), so nothing renders a literal "null".
|
|
138
143
|
const displayName = profile
|
|
139
|
-
? (profile.name?.displayName ?? getNormalizedUserHandle(profile))
|
|
144
|
+
? (profile.name?.displayName ?? getNormalizedUserHandle(profile) ?? '')
|
|
140
145
|
: username || '';
|
|
141
146
|
useSurfaceHeader({
|
|
142
147
|
title: displayName || t('profile.title'),
|
|
@@ -191,8 +196,17 @@ const ProfileScreen: React.FC<ProfileScreenProps> = ({ userId, username, theme,
|
|
|
191
196
|
<View style={styles.avatarRow} className="px-screen-margin">
|
|
192
197
|
<View style={styles.avatarWrapper} className="border-bg bg-bg rounded-radius-max">
|
|
193
198
|
<Avatar
|
|
194
|
-
|
|
195
|
-
|
|
199
|
+
// `getFileDownloadUrl` answers `null` for a reference it cannot
|
|
200
|
+
// resolve, and `Avatar.source` takes `undefined` for "no picture" —
|
|
201
|
+
// the two spellings of absent do not meet, so the coalesce is the
|
|
202
|
+
// whole fix. Without it this file does not typecheck, which blocks
|
|
203
|
+
// `bun run build` and therefore every publish of this package.
|
|
204
|
+
source={
|
|
205
|
+
profile?.avatar
|
|
206
|
+
? (oxyServices.getFileDownloadUrl(profile.avatar, 'thumb') ?? undefined)
|
|
207
|
+
: undefined
|
|
208
|
+
}
|
|
209
|
+
name={displayName}
|
|
196
210
|
size={AVATAR_SIZE}
|
|
197
211
|
/>
|
|
198
212
|
</View>
|
|
@@ -56,12 +56,20 @@ function fakeOxy(
|
|
|
56
56
|
* Re-import the module per test: it memoises the resolved native module, which is
|
|
57
57
|
* correct in production (one resolution per process) and must not leak between
|
|
58
58
|
* cases here.
|
|
59
|
+
*
|
|
60
|
+
* `platform` is explicit because the react-native stub defaults `Platform.OS` to
|
|
61
|
+
* `'web'`, where this feature is gated OFF by design. Every case therefore has to
|
|
62
|
+
* say which platform it is describing — defaulting to `'android'` here, since that
|
|
63
|
+
* is the only platform the feature ships on.
|
|
59
64
|
*/
|
|
60
|
-
async function loadModule(native: NativeCalls | null) {
|
|
65
|
+
async function loadModule(native: NativeCalls | null, platform: 'android' | 'web' = 'android') {
|
|
61
66
|
jest.resetModules();
|
|
67
|
+
requireOptionalNativeModule.mockClear();
|
|
62
68
|
requireOptionalNativeModule.mockReturnValue(
|
|
63
69
|
native ? { put: native.put, clear: native.clear, peek: native.peek } : null,
|
|
64
70
|
);
|
|
71
|
+
const { Platform } = await import('react-native');
|
|
72
|
+
Platform.OS = platform;
|
|
65
73
|
return import('../backgroundSession');
|
|
66
74
|
}
|
|
67
75
|
|
|
@@ -145,13 +153,18 @@ describe('syncBackgroundSession', () => {
|
|
|
145
153
|
});
|
|
146
154
|
|
|
147
155
|
expect(provision).toHaveBeenCalledTimes(1);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
156
|
+
// Positional scalars, and the ORDER is the contract: the native signature is
|
|
157
|
+
// five parameters, not one object, because an Expo Modules `Record` does not
|
|
158
|
+
// convert in a consumer build of this package and every call was rejected. A
|
|
159
|
+
// silently reordered argument would store a credential that no mint can use, so
|
|
160
|
+
// this assertion is what pins the order — see `OxyBackgroundSessionModule.kt`.
|
|
161
|
+
expect(native.put).toHaveBeenCalledWith(
|
|
162
|
+
'https://api.oxy.so',
|
|
163
|
+
'device-1',
|
|
164
|
+
'bg-secret',
|
|
165
|
+
'user-1',
|
|
166
|
+
Date.parse(expiresAt),
|
|
167
|
+
);
|
|
155
168
|
// Same account, so nothing was revoked on the way.
|
|
156
169
|
expect(native.clear).not.toHaveBeenCalled();
|
|
157
170
|
});
|
|
@@ -316,6 +329,48 @@ describe('a @oxyhq/core too old to provision', () => {
|
|
|
316
329
|
});
|
|
317
330
|
});
|
|
318
331
|
|
|
332
|
+
describe('on web', () => {
|
|
333
|
+
/**
|
|
334
|
+
* Web must never provision. The credential is non-rotating and long-lived, which
|
|
335
|
+
* is only safe because native background code is its sole consumer; a browser
|
|
336
|
+
* origin already holds the ROTATING deviceSecret and has no background worker, so
|
|
337
|
+
* a successful provision there is a security DOWNGRADE. The harm lands at the
|
|
338
|
+
* network call, which mints a live server-side credential, so "storage failed
|
|
339
|
+
* anyway" is not a defence.
|
|
340
|
+
*
|
|
341
|
+
* These tests present a native module that IS available, which is the whole
|
|
342
|
+
* point: they fail if the gate ever relies on `requireOptionalNativeModule`
|
|
343
|
+
* returning null on web rather than on the platform check.
|
|
344
|
+
*/
|
|
345
|
+
test('does not provision even when a native module IS present', async () => {
|
|
346
|
+
const native = fakeNative(null);
|
|
347
|
+
const { syncBackgroundSession, isBackgroundSessionSupported } = await loadModule(native, 'web');
|
|
348
|
+
const provision = jest.fn(async () => provisioned());
|
|
349
|
+
|
|
350
|
+
expect(isBackgroundSessionSupported()).toBe(false);
|
|
351
|
+
await syncBackgroundSession({
|
|
352
|
+
oxyServices: fakeOxy(provision),
|
|
353
|
+
userId: 'user-1',
|
|
354
|
+
canUsePrivateApi: true,
|
|
355
|
+
isCurrent: () => true,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
expect(provision).not.toHaveBeenCalled();
|
|
359
|
+
expect(native.put).not.toHaveBeenCalled();
|
|
360
|
+
// Not even a read, so nothing in a browser origin is touched at all.
|
|
361
|
+
expect(native.peek).not.toHaveBeenCalled();
|
|
362
|
+
expect(native.clear).not.toHaveBeenCalled();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test('never asks the native registry on web', async () => {
|
|
366
|
+
const { isBackgroundSessionSupported } = await loadModule(fakeNative(null), 'web');
|
|
367
|
+
expect(isBackgroundSessionSupported()).toBe(false);
|
|
368
|
+
// The platform decides BEFORE the registry is consulted, so a web-build
|
|
369
|
+
// native-module mock or a future web shim cannot reopen the gate.
|
|
370
|
+
expect(requireOptionalNativeModule).not.toHaveBeenCalled();
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
319
374
|
describe('without the native module', () => {
|
|
320
375
|
test('every entry point is an inert no-op', async () => {
|
|
321
376
|
const { syncBackgroundSession, clearBackgroundSession, isBackgroundSessionSupported } =
|
|
@@ -25,12 +25,16 @@
|
|
|
25
25
|
* live in different stores with one writer each, so there is nothing to keep in
|
|
26
26
|
* lockstep and no state that can disagree.
|
|
27
27
|
*
|
|
28
|
-
* Android only
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* ## Android only, and web is refused rather than merely unsupported
|
|
29
|
+
*
|
|
30
|
+
* Web is gated off by an explicit platform check, not by the native module being
|
|
31
|
+
* absent — provisioning in a browser origin would be a security downgrade, so it
|
|
32
|
+
* must not depend on an incidental fact. See {@link loadNativeModule}. iOS has no
|
|
33
|
+
* implementation yet and resolves to no module, so every function here degrades to
|
|
34
|
+
* a no-op: an app can enable this and lose nothing on other platforms.
|
|
32
35
|
*/
|
|
33
36
|
import { requireOptionalNativeModule } from 'expo-modules-core';
|
|
37
|
+
import { Platform } from 'react-native';
|
|
34
38
|
import type { OxyServices } from '@oxyhq/core';
|
|
35
39
|
import { logger } from '@oxyhq/core';
|
|
36
40
|
|
|
@@ -40,15 +44,29 @@ import { logger } from '@oxyhq/core';
|
|
|
40
44
|
* secret's only reader is one less place it can leak from.
|
|
41
45
|
*/
|
|
42
46
|
interface OxyBackgroundSessionNativeModule {
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Store (replacing) the credential. Resolves `false` when it was rejected or
|
|
49
|
+
* storage failed.
|
|
50
|
+
*
|
|
51
|
+
* Positional scalars rather than one object, matching the native signature — and
|
|
52
|
+
* that shape is load-bearing, not a preference. Passing an object made this an
|
|
53
|
+
* Expo Modules `Record`, and a `Record` does not convert in a consumer build of
|
|
54
|
+
* this package: every call was rejected with `The 1st argument cannot be cast to
|
|
55
|
+
* type CredentialInput (received class ReadableNativeMap)`, so the credential was
|
|
56
|
+
* never stored and background refreshes stayed signed out. See the comment on
|
|
57
|
+
* `AsyncFunction("put")` in `OxyBackgroundSessionModule.kt` for why, and do not
|
|
58
|
+
* restore the object form without proving on a device that a `Record` converts.
|
|
59
|
+
*
|
|
60
|
+
* `expiresAtMs` is epoch millis, parsed from the server's ISO-8601 string here
|
|
61
|
+
* rather than natively.
|
|
62
|
+
*/
|
|
63
|
+
put(
|
|
64
|
+
baseUrl: string,
|
|
65
|
+
deviceId: string,
|
|
66
|
+
secret: string,
|
|
67
|
+
accountId: string,
|
|
68
|
+
expiresAtMs: number,
|
|
69
|
+
): Promise<boolean>;
|
|
52
70
|
/** Drop the credential and any cached access token. */
|
|
53
71
|
clear(): Promise<boolean>;
|
|
54
72
|
/** What is stored: the owning account and its expiry (epoch millis), or `null`. */
|
|
@@ -67,7 +85,30 @@ const RENEW_WHEN_REMAINING_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
67
85
|
let nativeModule: OxyBackgroundSessionNativeModule | null | undefined;
|
|
68
86
|
|
|
69
87
|
/**
|
|
70
|
-
* Resolve the native module once.
|
|
88
|
+
* Resolve the native module once — and refuse outright on web.
|
|
89
|
+
*
|
|
90
|
+
* ## Web is excluded on purpose, and not merely because the module is absent
|
|
91
|
+
*
|
|
92
|
+
* The credential is deliberately NON-ROTATING and long-lived, which is safe only
|
|
93
|
+
* because native background code is its sole consumer and nobody rotates it. A
|
|
94
|
+
* browser origin already holds the ROTATING `deviceSecret`, and it has no
|
|
95
|
+
* background worker to serve — so provisioning there would add a strictly weaker
|
|
96
|
+
* long-lived secret alongside a stronger short-lived one. That is a downgrade, not
|
|
97
|
+
* a harmless no-op, and the harm lands at the PROVISION CALL (which mints a live
|
|
98
|
+
* server-side credential) regardless of whether anything manages to store it.
|
|
99
|
+
*
|
|
100
|
+
* That property must not rest on `requireOptionalNativeModule` happening to return
|
|
101
|
+
* `null` on web. It does today — the module is registered for android only — but
|
|
102
|
+
* that is an incidental fact a future web shim or a web-build native-module mock
|
|
103
|
+
* would quietly reverse. So the platform is checked FIRST, before we even ask the
|
|
104
|
+
* registry, and every entry point in this module inherits the gate.
|
|
105
|
+
*
|
|
106
|
+
* (A pre-deploy server makes this visible too: oxy-api's `sessionDevice` router
|
|
107
|
+
* applies `requireSameSiteOrigin` path-agnostically, so an unmatched path answers
|
|
108
|
+
* `404` to a native caller but `403 BAD_ORIGIN` to a browser one. Core's degrade is
|
|
109
|
+
* deliberately narrow — `404 → null` only — because a real origin misconfiguration
|
|
110
|
+
* on a CURRENT server also returns 403, and swallowing that would hide a genuine
|
|
111
|
+
* bug. Platform gating belongs here, where the platform is actually known.)
|
|
71
112
|
*
|
|
72
113
|
* `requireOptionalNativeModule` (a static import Metro always resolves) rather
|
|
73
114
|
* than a dynamic `import(moduleName)`: the latter compiles to `require(variable)`
|
|
@@ -77,6 +118,10 @@ let nativeModule: OxyBackgroundSessionNativeModule | null | undefined;
|
|
|
77
118
|
*/
|
|
78
119
|
function loadNativeModule(): OxyBackgroundSessionNativeModule | null {
|
|
79
120
|
if (nativeModule === undefined) {
|
|
121
|
+
if (Platform.OS === 'web') {
|
|
122
|
+
nativeModule = null;
|
|
123
|
+
return nativeModule;
|
|
124
|
+
}
|
|
80
125
|
const native = requireOptionalNativeModule<Partial<OxyBackgroundSessionNativeModule>>(
|
|
81
126
|
'OxyBackgroundSession',
|
|
82
127
|
);
|
|
@@ -246,13 +291,13 @@ export async function syncBackgroundSession(input: BackgroundSessionSyncInput):
|
|
|
246
291
|
return;
|
|
247
292
|
}
|
|
248
293
|
|
|
249
|
-
const persisted = await native.put(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
294
|
+
const persisted = await native.put(
|
|
295
|
+
oxyServices.getBaseURL(),
|
|
296
|
+
provisioned.deviceId,
|
|
297
|
+
provisioned.secret,
|
|
298
|
+
provisioned.accountId,
|
|
254
299
|
expiresAt,
|
|
255
|
-
|
|
300
|
+
);
|
|
256
301
|
if (!persisted) {
|
|
257
302
|
logger.warn(
|
|
258
303
|
'[backgroundSession] the native store did not retain the background credential; background refreshes stay signed out until the next attempt',
|
|
@@ -48,11 +48,10 @@ export function useBackgroundSessionSync({
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
if (!enabled) {
|
|
51
|
-
// An app that
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
// memoized keystore open.
|
|
51
|
+
// An app that leaves the feature off must not keep a credential THIS app
|
|
52
|
+
// provisioned earlier. The store is package-scoped (see
|
|
53
|
+
// OxyBackgroundSessionStore), so this cannot revoke a sibling Oxy app's
|
|
54
|
+
// credential under the shared UID.
|
|
56
55
|
void clearBackgroundSession();
|
|
57
56
|
return;
|
|
58
57
|
}
|