@rakomi/react-native 0.0.0 → 0.1.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.
@@ -0,0 +1,377 @@
1
+ // src/native/bare-rn-example.ts
2
+ function createBareRnAdapter(options) {
3
+ const servicePrefix = options.keychainServicePrefix ?? "rakomi";
4
+ const crypto = {
5
+ getRandomBytes: async (length) => new Uint8Array(options.quickCrypto.randomBytes(length)),
6
+ digestSha256: async (input) => new Uint8Array(options.quickCrypto.createHash("sha256").update(input).digest())
7
+ };
8
+ const storage = {
9
+ getItem: async (key, opts) => {
10
+ const result = await options.keychain.getGenericPassword({
11
+ service: `${servicePrefix}.${key}`,
12
+ ...opts?.requireAuthentication ? { authenticationPrompt: { title: opts.authenticationPrompt ?? "Authenticate" } } : {}
13
+ });
14
+ return result ? result.password : null;
15
+ },
16
+ setItem: async (key, value, opts) => {
17
+ await options.keychain.setGenericPassword("rakomi", value, {
18
+ service: `${servicePrefix}.${key}`,
19
+ accessible: opts?.keychainAccessible ?? "AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY"
20
+ });
21
+ },
22
+ removeItem: async (key) => {
23
+ await options.keychain.resetGenericPassword({ service: `${servicePrefix}.${key}` });
24
+ }
25
+ };
26
+ const browser = {
27
+ openAuthSession: async (authUrl, redirectUri, opts) => {
28
+ const available = await options.inAppBrowser.isAvailable();
29
+ if (!available) return { type: "locked" };
30
+ const result = await options.inAppBrowser.openAuth(authUrl, redirectUri, {
31
+ ephemeralWebSession: opts?.preferEphemeralSession ?? true
32
+ });
33
+ if (result.type === "success" && result.url) return { type: "success", url: result.url };
34
+ if (result.type === "cancel") return { type: "cancel" };
35
+ return { type: "dismiss" };
36
+ }
37
+ };
38
+ const deepLink = {
39
+ getInitialUrl: () => options.linking.getInitialURL(),
40
+ addListener: (listener) => options.linking.addEventListener("url", (e) => listener(e.url))
41
+ };
42
+ const biometric = {
43
+ isAvailable: async () => {
44
+ const sensor = await options.biometrics.isSensorAvailable();
45
+ return sensor.available;
46
+ },
47
+ authenticate: async ({ promptMessage }) => {
48
+ const result = await options.biometrics.simplePrompt({ promptMessage });
49
+ if (result.success) return { success: true };
50
+ return { success: false, reason: "cancelled" };
51
+ }
52
+ };
53
+ const appLifecycle = {
54
+ addStateChangeListener: (listener) => options.appState.addEventListener("change", listener),
55
+ getCurrent: () => options.appState.currentState
56
+ };
57
+ const connectivity = {
58
+ isConnected: async () => {
59
+ const state = await options.netInfo.fetch();
60
+ return state.isConnected === true;
61
+ },
62
+ addListener: (listener) => {
63
+ const unsub = options.netInfo.addEventListener((state) => listener(state.isConnected === true));
64
+ return { remove: unsub };
65
+ }
66
+ };
67
+ return {
68
+ crypto,
69
+ storage,
70
+ browser,
71
+ deepLink,
72
+ biometric,
73
+ appLifecycle,
74
+ connectivity
75
+ };
76
+ }
77
+
78
+ // src/native/dpop-prover.ts
79
+ function createNativeDpopProver(options) {
80
+ const mod = options.module;
81
+ return {
82
+ async createProof(input) {
83
+ const proof = await mod.createDpopProof(input.htm, input.htu, input.nonce ?? null);
84
+ if (typeof proof !== "string" || proof.length === 0) {
85
+ throw new Error("native DPoP signer returned an empty proof");
86
+ }
87
+ return proof;
88
+ },
89
+ async jktHint() {
90
+ return mod.dpopJktThumbprint();
91
+ }
92
+ };
93
+ }
94
+
95
+ // src/native/expo-adapter.ts
96
+ function createDefaultExpoAdapter(options = {}) {
97
+ const crypto = createExpoCrypto();
98
+ const storage = createExpoSecureStore({ tokenCache: options.tokenCache });
99
+ const browser = createExpoBrowser();
100
+ const deepLink = createExpoDeepLink();
101
+ const biometric = createExpoBiometricGate();
102
+ const appLifecycle = createRnAppLifecycle();
103
+ const connectivity = createNetInfoConnectivity();
104
+ return {
105
+ crypto,
106
+ storage,
107
+ browser,
108
+ deepLink,
109
+ biometric,
110
+ appLifecycle,
111
+ connectivity
112
+ };
113
+ }
114
+ function createExpoCrypto() {
115
+ let mod = null;
116
+ async function load() {
117
+ if (!mod) {
118
+ mod = await import('expo-crypto').catch(() => null);
119
+ if (!mod) {
120
+ throw new Error(
121
+ "@rakomi/react-native: expo-crypto is required for the default adapter. Install it (Expo-managed) or pass a custom nativeAdapter (bare RN)."
122
+ );
123
+ }
124
+ }
125
+ return mod;
126
+ }
127
+ return {
128
+ getRandomBytes: async (n) => {
129
+ const m = await load();
130
+ const result = await m.getRandomBytesAsync(n);
131
+ return new Uint8Array(result);
132
+ },
133
+ digestSha256: async (input) => {
134
+ const m = await load();
135
+ let str = "";
136
+ for (let i = 0; i < input.length; i++) str += String.fromCharCode(input[i]);
137
+ const hex = await m.digestStringAsync(
138
+ m.CryptoDigestAlgorithm.SHA256,
139
+ str,
140
+ { encoding: m.CryptoEncoding.HEX }
141
+ );
142
+ const bytes = new Uint8Array(hex.length / 2);
143
+ for (let i = 0; i < bytes.length; i++) {
144
+ bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
145
+ }
146
+ return bytes;
147
+ }
148
+ };
149
+ }
150
+ function createExpoSecureStore(opts) {
151
+ if (opts.tokenCache) {
152
+ const tc = opts.tokenCache;
153
+ return {
154
+ getItem: (key) => tc.getToken(key),
155
+ setItem: async (key, value) => {
156
+ await tc.saveToken(key, value);
157
+ },
158
+ removeItem: (key) => tc.clearToken(key)
159
+ };
160
+ }
161
+ let mod = null;
162
+ async function load() {
163
+ if (!mod) {
164
+ mod = await import('expo-secure-store').catch(() => null);
165
+ if (!mod) {
166
+ throw new Error(
167
+ "@rakomi/react-native: expo-secure-store is required for the default adapter. Install it (Expo-managed) or pass `tokenCache` / a custom nativeAdapter."
168
+ );
169
+ }
170
+ }
171
+ return mod;
172
+ }
173
+ return {
174
+ getItem: async (key, options) => {
175
+ const m = await load();
176
+ return m.getItemAsync(key, {
177
+ requireAuthentication: options?.requireAuthentication,
178
+ authenticationPrompt: options?.authenticationPrompt
179
+ });
180
+ },
181
+ setItem: async (key, value, options) => {
182
+ const m = await load();
183
+ const accessibility = mapKeychainAccessible(
184
+ m,
185
+ options?.keychainAccessible ?? "AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY"
186
+ );
187
+ await m.setItemAsync(key, value, {
188
+ keychainAccessible: accessibility,
189
+ requireAuthentication: options?.requireAuthentication,
190
+ authenticationPrompt: options?.authenticationPrompt
191
+ });
192
+ },
193
+ removeItem: async (key) => {
194
+ const m = await load();
195
+ await m.deleteItemAsync(key);
196
+ }
197
+ };
198
+ }
199
+ function mapKeychainAccessible(m, level) {
200
+ switch (level) {
201
+ case "WHEN_UNLOCKED":
202
+ return m.WHEN_UNLOCKED;
203
+ case "AFTER_FIRST_UNLOCK":
204
+ return m.AFTER_FIRST_UNLOCK;
205
+ case "WHEN_UNLOCKED_THIS_DEVICE_ONLY":
206
+ return m.WHEN_UNLOCKED_THIS_DEVICE_ONLY;
207
+ case "AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY":
208
+ return m.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY;
209
+ case "WHEN_PASSCODE_SET_THIS_DEVICE_ONLY":
210
+ return m.WHEN_PASSCODE_SET_THIS_DEVICE_ONLY;
211
+ }
212
+ }
213
+ function createExpoBrowser() {
214
+ let mod = null;
215
+ async function load() {
216
+ if (!mod) {
217
+ mod = await import('expo-web-browser').catch(() => null);
218
+ if (!mod) {
219
+ throw new Error("@rakomi/react-native: expo-web-browser is required for OAuth flows.");
220
+ }
221
+ }
222
+ return mod;
223
+ }
224
+ return {
225
+ openAuthSession: async (authUrl, redirectUri, options) => {
226
+ const m = await load();
227
+ const result = await m.openAuthSessionAsync(authUrl, redirectUri, {
228
+ preferEphemeralSession: options?.preferEphemeralSession ?? true
229
+ });
230
+ if (result.type === "success" && result.url) {
231
+ return { type: "success", url: result.url };
232
+ }
233
+ if (result.type === "cancel") return { type: "cancel" };
234
+ if (result.type === "dismiss") return { type: "dismiss" };
235
+ if (result.type === "locked") return { type: "locked" };
236
+ return { type: "cancel" };
237
+ }
238
+ };
239
+ }
240
+ function createExpoDeepLink() {
241
+ let mod = null;
242
+ async function load() {
243
+ if (!mod) {
244
+ mod = await import('expo-linking').catch(() => null);
245
+ if (!mod) {
246
+ throw new Error("@rakomi/react-native: expo-linking is required for OAuth deep-link callback.");
247
+ }
248
+ }
249
+ return mod;
250
+ }
251
+ return {
252
+ getInitialUrl: async () => {
253
+ const m = await load();
254
+ return m.getInitialURL();
255
+ },
256
+ addListener: (listener) => {
257
+ let sub = null;
258
+ void load().then((m) => {
259
+ sub = m.addEventListener("url", (event) => listener(event.url));
260
+ });
261
+ return {
262
+ remove: () => {
263
+ sub?.remove();
264
+ }
265
+ };
266
+ }
267
+ };
268
+ }
269
+ function createExpoBiometricGate() {
270
+ let mod = null;
271
+ async function load() {
272
+ if (mod !== null) return mod;
273
+ try {
274
+ mod = await import('expo-local-authentication');
275
+ } catch {
276
+ mod = null;
277
+ }
278
+ return mod;
279
+ }
280
+ return {
281
+ isAvailable: async () => {
282
+ const m = await load();
283
+ if (!m) return false;
284
+ const has = await m.hasHardwareAsync();
285
+ if (!has) return false;
286
+ const enrolled = await m.isEnrolledAsync();
287
+ return enrolled;
288
+ },
289
+ authenticate: async ({ promptMessage, strict = false }) => {
290
+ const m = await load();
291
+ if (!m) return { success: false, reason: "unavailable" };
292
+ const result = await m.authenticateAsync({
293
+ promptMessage,
294
+ disableDeviceFallback: strict,
295
+ fallbackLabel: "Use passcode"
296
+ });
297
+ if (result.success) return { success: true };
298
+ switch (result.error) {
299
+ case "user_cancel":
300
+ case "system_cancel":
301
+ case "app_cancel":
302
+ return { success: false, reason: "cancelled" };
303
+ case "lockout":
304
+ case "lockout_permanent":
305
+ return { success: false, reason: "lockout" };
306
+ case "not_enrolled":
307
+ return { success: false, reason: "not_enrolled" };
308
+ case "not_available":
309
+ case "no_space":
310
+ return { success: false, reason: "unavailable" };
311
+ default:
312
+ return { success: false, reason: "unknown" };
313
+ }
314
+ }
315
+ };
316
+ }
317
+ function createRnAppLifecycle() {
318
+ let rn = null;
319
+ let pending = null;
320
+ async function ensure() {
321
+ if (rn) return rn;
322
+ if (!pending) {
323
+ pending = import('react-native').then((m) => {
324
+ rn = m.default ?? m;
325
+ return rn;
326
+ });
327
+ }
328
+ return pending;
329
+ }
330
+ void ensure().catch(() => void 0);
331
+ return {
332
+ addStateChangeListener: (listener) => {
333
+ let sub = null;
334
+ void ensure().then((r) => {
335
+ sub = r.AppState.addEventListener("change", (next) => {
336
+ listener(coerceAppState(next));
337
+ });
338
+ });
339
+ return { remove: () => sub?.remove() };
340
+ },
341
+ getCurrent: () => rn ? coerceAppState(rn.AppState.currentState) : "unknown"
342
+ };
343
+ }
344
+ function coerceAppState(value) {
345
+ if (value === "active" || value === "background" || value === "inactive") return value;
346
+ return "unknown";
347
+ }
348
+ function createNetInfoConnectivity() {
349
+ let mod = null;
350
+ async function load() {
351
+ if (mod !== null) return mod;
352
+ try {
353
+ mod = (await import('@react-native-community/netinfo')).default ?? null;
354
+ } catch {
355
+ mod = null;
356
+ }
357
+ return mod;
358
+ }
359
+ return {
360
+ isConnected: async () => {
361
+ const m = await load();
362
+ if (!m) return true;
363
+ const state = await m.fetch();
364
+ return state.isConnected ?? true;
365
+ },
366
+ addListener: (listener) => {
367
+ let unsubscribe = null;
368
+ void load().then((m) => {
369
+ if (!m) return;
370
+ unsubscribe = m.addEventListener((state) => listener(state.isConnected ?? true));
371
+ });
372
+ return { remove: () => unsubscribe?.() };
373
+ }
374
+ };
375
+ }
376
+
377
+ export { createBareRnAdapter, createDefaultExpoAdapter, createNativeDpopProver };
package/package.json CHANGED
@@ -1,7 +1,98 @@
1
1
  {
2
2
  "name": "@rakomi/react-native",
3
- "version": "0.0.0",
4
- "description": "Reserved placeholder — first stable release published via OIDC provenance. Do not install.",
5
- "license": "Apache-2.0",
6
- "private": false
7
- }
3
+ "version": "0.1.0",
4
+ "description": "React Native / Expo SDK for Rakomi authentication.",
5
+ "keywords": [
6
+ "rakomi",
7
+ "authentication",
8
+ "auth",
9
+ "oauth",
10
+ "oidc",
11
+ "react-native",
12
+ "expo",
13
+ "sdk"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/rakomidev/rakomi-js.git"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/rakomidev/rakomi-js/issues"
21
+ },
22
+ "homepage": "https://github.com/rakomidev/rakomi-js#readme",
23
+ "rakomi": {
24
+ "apiVersion": "2026-03-01"
25
+ },
26
+ "publishConfig": {
27
+ "provenance": true
28
+ },
29
+ "type": "module",
30
+ "license": "MIT",
31
+ "sideEffects": false,
32
+ "exports": {
33
+ ".": {
34
+ "import": {
35
+ "types": "./dist/index.d.ts",
36
+ "default": "./dist/index.js"
37
+ },
38
+ "require": {
39
+ "types": "./dist/index.d.cts",
40
+ "default": "./dist/index.cjs"
41
+ }
42
+ },
43
+ "./native": {
44
+ "import": {
45
+ "types": "./dist/native/index.d.ts",
46
+ "default": "./dist/native/index.js"
47
+ },
48
+ "require": {
49
+ "types": "./dist/native/index.d.cts",
50
+ "default": "./dist/native/index.cjs"
51
+ }
52
+ },
53
+ "./package.json": "./package.json"
54
+ },
55
+ "main": "./dist/index.cjs",
56
+ "module": "./dist/index.js",
57
+ "types": "./dist/index.d.ts",
58
+ "author": "CRE8EVE Sp. z o.o.",
59
+ "files": [
60
+ "dist",
61
+ "!dist/metafile-*.json",
62
+ "README.md",
63
+ "LICENSE",
64
+ "SECURITY.md",
65
+ "COMPLIANCE.md",
66
+ "THIRD-PARTY-NOTICES",
67
+ "sbom.cdx.json"
68
+ ],
69
+ "peerDependencies": {
70
+ "expo": ">=54",
71
+ "react": ">=18",
72
+ "react-native": ">=0.81"
73
+ },
74
+ "peerDependenciesMeta": {
75
+ "expo": {
76
+ "optional": true
77
+ },
78
+ "react-native": {
79
+ "optional": false
80
+ },
81
+ "react": {
82
+ "optional": false
83
+ }
84
+ },
85
+ "devDependencies": {
86
+ "@rakomi/react": "0.1.0"
87
+ },
88
+ "dependencies": {
89
+ "jose": "^6.2.3",
90
+ "@rakomi/sdk-core": "^0.1.0"
91
+ },
92
+ "scripts": {
93
+ "build": "NODE_OPTIONS=--max-old-space-size=4096 tsup",
94
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.typetest.json",
95
+ "lint": "eslint src/ test/ --max-warnings=0",
96
+ "test": "vitest run"
97
+ }
98
+ }
package/sbom.cdx.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "bomFormat": "CycloneDX",
3
+ "specVersion": "1.6",
4
+ "version": 1,
5
+ "metadata": {
6
+ "authors": [
7
+ {
8
+ "name": "CRE8EVE"
9
+ }
10
+ ],
11
+ "tools": {
12
+ "components": [
13
+ {
14
+ "type": "application",
15
+ "group": "rakomi",
16
+ "name": "generate-sbom",
17
+ "version": "sha256:6612f58a07b8"
18
+ }
19
+ ]
20
+ },
21
+ "component": {
22
+ "type": "library",
23
+ "bom-ref": "pkg:npm/%40rakomi%2Freact-native@0.1.0",
24
+ "name": "@rakomi/react-native",
25
+ "version": "0.1.0",
26
+ "purl": "pkg:npm/%40rakomi%2Freact-native@0.1.0",
27
+ "licenses": [
28
+ {
29
+ "license": {
30
+ "id": "MIT"
31
+ }
32
+ }
33
+ ]
34
+ }
35
+ },
36
+ "components": [
37
+ {
38
+ "type": "library",
39
+ "bom-ref": "pkg:npm/@rakomi/sdk-core@0.1.0",
40
+ "name": "@rakomi/sdk-core",
41
+ "version": "0.1.0",
42
+ "purl": "pkg:npm/@rakomi/sdk-core@0.1.0",
43
+ "licenses": [
44
+ {
45
+ "license": {
46
+ "id": "MIT"
47
+ }
48
+ }
49
+ ]
50
+ },
51
+ {
52
+ "type": "library",
53
+ "bom-ref": "pkg:npm/jose@6.2.3",
54
+ "name": "jose",
55
+ "version": "6.2.3",
56
+ "purl": "pkg:npm/jose@6.2.3",
57
+ "licenses": [
58
+ {
59
+ "license": {
60
+ "id": "MIT"
61
+ }
62
+ }
63
+ ]
64
+ }
65
+ ]
66
+ }