@appattest/react-native 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.
- package/LICENSE +21 -0
- package/README.md +150 -0
- package/ios/AppAttestModule.mm +52 -0
- package/ios/AppAttestModule.swift +173 -0
- package/ios/AppAttestReactNative.podspec +34 -0
- package/lib/commonjs/NativeAppAttest.js +20 -0
- package/lib/commonjs/NativeAppAttest.js.map +1 -0
- package/lib/commonjs/index.js +245 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/module/NativeAppAttest.js +17 -0
- package/lib/module/NativeAppAttest.js.map +1 -0
- package/lib/module/index.js +237 -0
- package/lib/module/index.js.map +1 -0
- package/lib/typescript/NativeAppAttest.d.ts +54 -0
- package/lib/typescript/NativeAppAttest.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +115 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/package.json +76 -0
- package/src/NativeAppAttest.ts +70 -0
- package/src/index.ts +282 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @appattest/react-native — public API.
|
|
3
|
+
*
|
|
4
|
+
* Surface mirrors the Swift SDK's bridge translation:
|
|
5
|
+
* - AppAttest.start() — fire-and-forget setup (zero-arg)
|
|
6
|
+
* - AppAttest.getSecret(name) — Promise<string | null>
|
|
7
|
+
* - AppAttest.getAllSecrets() — Promise<Record<string, string>>
|
|
8
|
+
* - AppAttest.getState() — Promise<AppAttestState>
|
|
9
|
+
* - AppAttest.waitForReady() — Promise<void>
|
|
10
|
+
* - AppAttest.retry() — Promise<void>
|
|
11
|
+
* - AppAttest.addStateListener(fn) — (state) => void; returns unsubscribe
|
|
12
|
+
* - AppAttest.setDebugMode
|
|
13
|
+
*
|
|
14
|
+
* React hooks (idiomatic for RN consumers):
|
|
15
|
+
* - useSecret(name) — string | null, re-renders on rotation
|
|
16
|
+
* - useAllSecrets() — Record<string, string>, re-renders on rotation
|
|
17
|
+
* - useAppAttestState() — AppAttestState, re-renders on transition
|
|
18
|
+
*
|
|
19
|
+
* There is no sandbox-mode modal logic. Developers handle their
|
|
20
|
+
* own UX for `.subscriptionRequired` / `.creditsRequired` / `.unavailable`.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Lifecycle state. Mirrors `AppAttestClient.State` on the native side.
|
|
24
|
+
*/
|
|
25
|
+
export type AppAttestStateName = 'initializing' | 'attesting' | 'syncing' | 'ready' | 'subscription_required' | 'credits_required' | 'unavailable';
|
|
26
|
+
export interface AppAttestState {
|
|
27
|
+
name: AppAttestStateName;
|
|
28
|
+
error?: AppAttestError;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Typed error thrown by every method. `code` matches the Swift SDK's
|
|
32
|
+
* `AppAttestError.code` one-for-one.
|
|
33
|
+
*/
|
|
34
|
+
export declare class AppAttestError extends Error {
|
|
35
|
+
readonly code: string;
|
|
36
|
+
/** For `subscription_required`: URL to (re)start the project subscription. */
|
|
37
|
+
readonly subscribeUrl?: string;
|
|
38
|
+
/** For `credits_required`: URL to top up the project balance. */
|
|
39
|
+
readonly topupUrl?: string;
|
|
40
|
+
readonly nativeError?: unknown;
|
|
41
|
+
constructor(code: string, message: string, extras?: {
|
|
42
|
+
subscribeUrl?: string;
|
|
43
|
+
topupUrl?: string;
|
|
44
|
+
nativeError?: unknown;
|
|
45
|
+
});
|
|
46
|
+
/**
|
|
47
|
+
* Single accessor for the dashboard URL regardless of code. Returns
|
|
48
|
+
* `subscribeUrl` for `subscription_required`, `topupUrl` for
|
|
49
|
+
* `credits_required`, else `undefined`.
|
|
50
|
+
*/
|
|
51
|
+
get actionUrl(): string | undefined;
|
|
52
|
+
}
|
|
53
|
+
/** Stable string codes. Match Swift `AppAttestError.code` one-for-one. */
|
|
54
|
+
export declare const ErrorCode: {
|
|
55
|
+
readonly SubscriptionRequired: "subscription_required";
|
|
56
|
+
readonly CreditsRequired: "credits_required";
|
|
57
|
+
readonly AttestationRejected: "attestation_rejected";
|
|
58
|
+
readonly ServiceUnavailable: "service_unavailable";
|
|
59
|
+
readonly Network: "network";
|
|
60
|
+
readonly DebugModeReleaseBlocked: "debug_mode_release_blocked";
|
|
61
|
+
readonly InvalidArgument: "invalid_argument";
|
|
62
|
+
};
|
|
63
|
+
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
64
|
+
export type DebugMode = 'production' | 'local';
|
|
65
|
+
export declare const AppAttest: {
|
|
66
|
+
/**
|
|
67
|
+
* Synchronous, idempotent setup. Zero-argument. Apple's AAGUID
|
|
68
|
+
* determines the bucket (sandbox vs production) server-side; the SDK
|
|
69
|
+
* is bucket-blind.
|
|
70
|
+
*/
|
|
71
|
+
start(): Promise<void>;
|
|
72
|
+
/** Awaits a terminal state. Resolves on `ready`; rejects with
|
|
73
|
+
* AppAttestError on subscriptionRequired / creditsRequired / unavailable. */
|
|
74
|
+
waitForReady(): Promise<void>;
|
|
75
|
+
/** Re-runs the background sync. */
|
|
76
|
+
retry(): Promise<void>;
|
|
77
|
+
/** Wipes stored credentials and secrets. */
|
|
78
|
+
reset(): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Invalidate the cached secrets bundle and immediately sync. Keeps
|
|
81
|
+
* attestation credentials. Forces a 200 on the next sync, which
|
|
82
|
+
* consumes one credit on the production bucket.
|
|
83
|
+
*
|
|
84
|
+
* Use this for host-app "force refresh" / "sync now" UX. For wiping
|
|
85
|
+
* everything (including attestation), see ``reset()``.
|
|
86
|
+
*/
|
|
87
|
+
invalidateBundle(): Promise<void>;
|
|
88
|
+
/** Returns the secret for `name`, or `null`. */
|
|
89
|
+
getSecret(name: string): Promise<string | null>;
|
|
90
|
+
/** Snapshot of every synced secret. */
|
|
91
|
+
getAllSecrets(): Promise<Record<string, string>>;
|
|
92
|
+
/** Current state snapshot. */
|
|
93
|
+
getState(): Promise<AppAttestState>;
|
|
94
|
+
/** Subscribe to state transitions. Returns an unsubscribe fn. */
|
|
95
|
+
addStateListener(listener: (state: AppAttestState) => void): () => void;
|
|
96
|
+
/** Set runtime mode. `'production'` (or `null`) is default. */
|
|
97
|
+
setDebugMode(mode: DebugMode | null, stubs?: Record<string, string>): Promise<void>;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* React hook returning the current value of `secrets[name]`. Re-renders
|
|
101
|
+
* the host component whenever the underlying value changes (e.g. after
|
|
102
|
+
* a foreground rotation pulled new data).
|
|
103
|
+
*/
|
|
104
|
+
export declare function useSecret(name: string): string | null;
|
|
105
|
+
/**
|
|
106
|
+
* React hook returning every synced secret. Re-renders on rotation.
|
|
107
|
+
*/
|
|
108
|
+
export declare function useAllSecrets(): Record<string, string>;
|
|
109
|
+
/**
|
|
110
|
+
* React hook returning the current `AppAttestState`. Re-renders on
|
|
111
|
+
* every transition.
|
|
112
|
+
*/
|
|
113
|
+
export declare function useAppAttestState(): AppAttestState;
|
|
114
|
+
export default AppAttest;
|
|
115
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AASH;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAC1B,cAAc,GACd,WAAW,GACX,SAAS,GACT,OAAO,GACP,uBAAuB,GACvB,kBAAkB,GAClB,aAAa,CAAC;AAElB,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,kBAAkB,CAAC;IACzB,KAAK,CAAC,EAAE,cAAc,CAAC;CACxB;AAED;;;GAGG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,iEAAiE;IACjE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;gBAG7B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE;QACP,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,WAAW,CAAC,EAAE,OAAO,CAAC;KACvB;IAUH;;;;OAIG;IACH,IAAI,SAAS,IAAI,MAAM,GAAG,SAAS,CAElC;CACF;AAED,0EAA0E;AAC1E,eAAO,MAAM,SAAS;;;;;;;;CAQZ,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,OAAO,SAAS,CAAC,CAAC;AAEnE,MAAM,MAAM,SAAS,GAAG,YAAY,GAAG,OAAO,CAAC;AAwC/C,eAAO,MAAM,SAAS;IACpB;;;;OAIG;aACM,OAAO,CAAC,IAAI,CAAC;IAItB;kFAC8E;oBAC9D,OAAO,CAAC,IAAI,CAAC;IAI7B,mCAAmC;aAC1B,OAAO,CAAC,IAAI,CAAC;IAItB,4CAA4C;aACnC,OAAO,CAAC,IAAI,CAAC;IAItB;;;;;;;OAOG;wBACiB,OAAO,CAAC,IAAI,CAAC;IAIjC,gDAAgD;oBAChC,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAI/C,uCAAuC;qBACtB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAIhD,8BAA8B;gBACZ,OAAO,CAAC,cAAc,CAAC;IAKzC,iEAAiE;+BACtC,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,MAAM,IAAI;IAOvE,+DAA+D;uBAC5C,SAAS,GAAG,IAAI,UAAU,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;CAMpF,CAAC;AAIF;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAmBrD;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAmBtD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,cAAc,CAclD;AAED,eAAe,SAAS,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@appattest/react-native",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React Native bridge for AppAttest — App-Attest-gated secret delivery for iOS.",
|
|
5
|
+
"main": "lib/commonjs/index.js",
|
|
6
|
+
"module": "lib/module/index.js",
|
|
7
|
+
"types": "lib/typescript/index.d.ts",
|
|
8
|
+
"react-native": "src/index.ts",
|
|
9
|
+
"source": "src/index.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"lib",
|
|
13
|
+
"ios",
|
|
14
|
+
"!ios/build",
|
|
15
|
+
"*.podspec",
|
|
16
|
+
"!**/.*"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"test": "jest",
|
|
21
|
+
"prepare": "bob build"
|
|
22
|
+
},
|
|
23
|
+
"jest": {
|
|
24
|
+
"preset": "ts-jest",
|
|
25
|
+
"testEnvironment": "node",
|
|
26
|
+
"testMatch": [
|
|
27
|
+
"<rootDir>/__tests__/**/*.test.ts"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"react-native",
|
|
32
|
+
"ios",
|
|
33
|
+
"app-attest",
|
|
34
|
+
"secrets",
|
|
35
|
+
"keychain"
|
|
36
|
+
],
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/AppAttest/appAttest-sdk.git",
|
|
40
|
+
"directory": "bridges/react-native"
|
|
41
|
+
},
|
|
42
|
+
"author": "Bault LLC",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/AppAttest/appAttest-sdk/issues"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/AppAttest/appAttest-sdk/tree/main/bridges/react-native#readme",
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"react": "*",
|
|
50
|
+
"react-native": ">=0.71.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/jest": "^29.5.0",
|
|
54
|
+
"@types/react": "^18.0.0",
|
|
55
|
+
"jest": "^29.7.0",
|
|
56
|
+
"react-native": "^0.77.0",
|
|
57
|
+
"react-native-builder-bob": "^0.42.1",
|
|
58
|
+
"ts-jest": "^29.1.0",
|
|
59
|
+
"typescript": "^5.4.0"
|
|
60
|
+
},
|
|
61
|
+
"codegenConfig": {
|
|
62
|
+
"name": "RNAppAttestSpec",
|
|
63
|
+
"type": "modules",
|
|
64
|
+
"jsSrcsDir": "src",
|
|
65
|
+
"android": {}
|
|
66
|
+
},
|
|
67
|
+
"react-native-builder-bob": {
|
|
68
|
+
"source": "src",
|
|
69
|
+
"output": "lib",
|
|
70
|
+
"targets": [
|
|
71
|
+
["commonjs"],
|
|
72
|
+
["module"],
|
|
73
|
+
["typescript", { "project": "tsconfig.json" }]
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TurboModule spec for the AppAttest iOS native module.
|
|
3
|
+
*
|
|
4
|
+
* Codegen reads this file. The shape is restricted to what TurboModule
|
|
5
|
+
* codegen can serialize (primitives, Object, arrays of primitives,
|
|
6
|
+
* Promises, null).
|
|
7
|
+
*
|
|
8
|
+
* Errors: rejection objects carry `{ code, message,
|
|
9
|
+
* subscribeUrl?, topupUrl? }`. The TS wrapper in `index.ts` translates
|
|
10
|
+
* these into `AppAttestError`.
|
|
11
|
+
*/
|
|
12
|
+
import type { TurboModule } from 'react-native';
|
|
13
|
+
import { TurboModuleRegistry } from 'react-native';
|
|
14
|
+
|
|
15
|
+
export interface Spec extends TurboModule {
|
|
16
|
+
// Lifecycle
|
|
17
|
+
|
|
18
|
+
/** Synchronous, idempotent setup. Zero-argument (bucket is
|
|
19
|
+
* AAGUID-derived server-side). */
|
|
20
|
+
start(): Promise<void>;
|
|
21
|
+
|
|
22
|
+
/** Awaits the next terminal state. Resolves on `ready`; rejects on
|
|
23
|
+
* subscriptionRequired / creditsRequired / unavailable. */
|
|
24
|
+
waitForReady(): Promise<void>;
|
|
25
|
+
|
|
26
|
+
/** Re-runs the background sync. */
|
|
27
|
+
retry(): Promise<void>;
|
|
28
|
+
|
|
29
|
+
/** Wipes stored credentials and secrets. */
|
|
30
|
+
reset(): Promise<void>;
|
|
31
|
+
|
|
32
|
+
/** Invalidate the cached secrets bundle and immediately sync. Keeps
|
|
33
|
+
* attestation; forces a 200 (1 credit on production). */
|
|
34
|
+
invalidateBundle(): Promise<void>;
|
|
35
|
+
|
|
36
|
+
// Reads
|
|
37
|
+
|
|
38
|
+
/** Synchronous-feeling secret lookup. Returns `null` if not yet
|
|
39
|
+
* synced or absent. */
|
|
40
|
+
getSecret(name: string): Promise<string | null>;
|
|
41
|
+
|
|
42
|
+
/** Snapshot of every synced secret as `{ [name]: value }`. */
|
|
43
|
+
getAllSecrets(): Promise<{ [key: string]: string }>;
|
|
44
|
+
|
|
45
|
+
/** Current state as `{ name, error? }`. */
|
|
46
|
+
getState(): Promise<{
|
|
47
|
+
name: string;
|
|
48
|
+
error?: {
|
|
49
|
+
code: string;
|
|
50
|
+
message: string;
|
|
51
|
+
subscribeUrl?: string;
|
|
52
|
+
topupUrl?: string;
|
|
53
|
+
};
|
|
54
|
+
}>;
|
|
55
|
+
|
|
56
|
+
// Configuration
|
|
57
|
+
|
|
58
|
+
/** `null` / `'production'` for production; `'local'` for the DEBUG-only
|
|
59
|
+
* stub mode. `'local'` requires `stubs`. */
|
|
60
|
+
setDebugMode(name: string | null, stubs: { [key: string]: string } | null): Promise<void>;
|
|
61
|
+
|
|
62
|
+
// setApiBaseUrl removed — base URL is hardcoded in the Swift SDK.
|
|
63
|
+
|
|
64
|
+
// Event emitter plumbing
|
|
65
|
+
|
|
66
|
+
addListener(eventName: string): void;
|
|
67
|
+
removeListeners(count: number): void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default TurboModuleRegistry.getEnforcing<Spec>('RNAppAttest');
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @appattest/react-native — public API.
|
|
3
|
+
*
|
|
4
|
+
* Surface mirrors the Swift SDK's bridge translation:
|
|
5
|
+
* - AppAttest.start() — fire-and-forget setup (zero-arg)
|
|
6
|
+
* - AppAttest.getSecret(name) — Promise<string | null>
|
|
7
|
+
* - AppAttest.getAllSecrets() — Promise<Record<string, string>>
|
|
8
|
+
* - AppAttest.getState() — Promise<AppAttestState>
|
|
9
|
+
* - AppAttest.waitForReady() — Promise<void>
|
|
10
|
+
* - AppAttest.retry() — Promise<void>
|
|
11
|
+
* - AppAttest.addStateListener(fn) — (state) => void; returns unsubscribe
|
|
12
|
+
* - AppAttest.setDebugMode
|
|
13
|
+
*
|
|
14
|
+
* React hooks (idiomatic for RN consumers):
|
|
15
|
+
* - useSecret(name) — string | null, re-renders on rotation
|
|
16
|
+
* - useAllSecrets() — Record<string, string>, re-renders on rotation
|
|
17
|
+
* - useAppAttestState() — AppAttestState, re-renders on transition
|
|
18
|
+
*
|
|
19
|
+
* There is no sandbox-mode modal logic. Developers handle their
|
|
20
|
+
* own UX for `.subscriptionRequired` / `.creditsRequired` / `.unavailable`.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { useEffect, useState } from 'react';
|
|
24
|
+
import { NativeEventEmitter, NativeModules } from 'react-native';
|
|
25
|
+
import NativeAppAttest from './NativeAppAttest';
|
|
26
|
+
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
const eventEmitter = new NativeEventEmitter(NativeModules.RNAppAttest as any);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Lifecycle state. Mirrors `AppAttestClient.State` on the native side.
|
|
32
|
+
*/
|
|
33
|
+
export type AppAttestStateName =
|
|
34
|
+
| 'initializing'
|
|
35
|
+
| 'attesting'
|
|
36
|
+
| 'syncing'
|
|
37
|
+
| 'ready'
|
|
38
|
+
| 'subscription_required'
|
|
39
|
+
| 'credits_required'
|
|
40
|
+
| 'unavailable';
|
|
41
|
+
|
|
42
|
+
export interface AppAttestState {
|
|
43
|
+
name: AppAttestStateName;
|
|
44
|
+
error?: AppAttestError;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Typed error thrown by every method. `code` matches the Swift SDK's
|
|
49
|
+
* `AppAttestError.code` one-for-one.
|
|
50
|
+
*/
|
|
51
|
+
export class AppAttestError extends Error {
|
|
52
|
+
readonly code: string;
|
|
53
|
+
/** For `subscription_required`: URL to (re)start the project subscription. */
|
|
54
|
+
readonly subscribeUrl?: string;
|
|
55
|
+
/** For `credits_required`: URL to top up the project balance. */
|
|
56
|
+
readonly topupUrl?: string;
|
|
57
|
+
readonly nativeError?: unknown;
|
|
58
|
+
|
|
59
|
+
constructor(
|
|
60
|
+
code: string,
|
|
61
|
+
message: string,
|
|
62
|
+
extras?: {
|
|
63
|
+
subscribeUrl?: string;
|
|
64
|
+
topupUrl?: string;
|
|
65
|
+
nativeError?: unknown;
|
|
66
|
+
},
|
|
67
|
+
) {
|
|
68
|
+
super(message);
|
|
69
|
+
this.name = 'AppAttestError';
|
|
70
|
+
this.code = code;
|
|
71
|
+
this.subscribeUrl = extras?.subscribeUrl;
|
|
72
|
+
this.topupUrl = extras?.topupUrl;
|
|
73
|
+
this.nativeError = extras?.nativeError;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Single accessor for the dashboard URL regardless of code. Returns
|
|
78
|
+
* `subscribeUrl` for `subscription_required`, `topupUrl` for
|
|
79
|
+
* `credits_required`, else `undefined`.
|
|
80
|
+
*/
|
|
81
|
+
get actionUrl(): string | undefined {
|
|
82
|
+
return this.subscribeUrl ?? this.topupUrl;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Stable string codes. Match Swift `AppAttestError.code` one-for-one. */
|
|
87
|
+
export const ErrorCode = {
|
|
88
|
+
SubscriptionRequired: 'subscription_required',
|
|
89
|
+
CreditsRequired: 'credits_required',
|
|
90
|
+
AttestationRejected: 'attestation_rejected',
|
|
91
|
+
ServiceUnavailable: 'service_unavailable',
|
|
92
|
+
Network: 'network',
|
|
93
|
+
DebugModeReleaseBlocked: 'debug_mode_release_blocked',
|
|
94
|
+
InvalidArgument: 'invalid_argument',
|
|
95
|
+
} as const;
|
|
96
|
+
|
|
97
|
+
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
98
|
+
|
|
99
|
+
export type DebugMode = 'production' | 'local';
|
|
100
|
+
|
|
101
|
+
interface ErrorEnvelope {
|
|
102
|
+
code?: string;
|
|
103
|
+
message?: string;
|
|
104
|
+
userInfo?: {
|
|
105
|
+
subscribeUrl?: string;
|
|
106
|
+
topupUrl?: string;
|
|
107
|
+
};
|
|
108
|
+
// RN flattens NSError userInfo onto the rejection too; cover both shapes.
|
|
109
|
+
subscribeUrl?: string;
|
|
110
|
+
topupUrl?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function buildError(e: ErrorEnvelope, native: unknown): AppAttestError {
|
|
114
|
+
return new AppAttestError(e.code ?? 'internal_error', e.message ?? String(native), {
|
|
115
|
+
subscribeUrl: e.userInfo?.subscribeUrl ?? e.subscribeUrl,
|
|
116
|
+
topupUrl: e.userInfo?.topupUrl ?? e.topupUrl,
|
|
117
|
+
nativeError: native,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function wrap<T>(p: Promise<T>): Promise<T> {
|
|
122
|
+
try {
|
|
123
|
+
return await p;
|
|
124
|
+
} catch (err) {
|
|
125
|
+
throw buildError(err as ErrorEnvelope, err);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface NativeStateRaw {
|
|
130
|
+
name: string;
|
|
131
|
+
error?: ErrorEnvelope;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function decodeState(raw: NativeStateRaw): AppAttestState {
|
|
135
|
+
const error = raw.error ? buildError(raw.error, raw.error) : undefined;
|
|
136
|
+
return { name: raw.name as AppAttestStateName, error };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const AppAttest = {
|
|
140
|
+
/**
|
|
141
|
+
* Synchronous, idempotent setup. Zero-argument. Apple's AAGUID
|
|
142
|
+
* determines the bucket (sandbox vs production) server-side; the SDK
|
|
143
|
+
* is bucket-blind.
|
|
144
|
+
*/
|
|
145
|
+
start(): Promise<void> {
|
|
146
|
+
return wrap(NativeAppAttest.start());
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
/** Awaits a terminal state. Resolves on `ready`; rejects with
|
|
150
|
+
* AppAttestError on subscriptionRequired / creditsRequired / unavailable. */
|
|
151
|
+
waitForReady(): Promise<void> {
|
|
152
|
+
return wrap(NativeAppAttest.waitForReady());
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
/** Re-runs the background sync. */
|
|
156
|
+
retry(): Promise<void> {
|
|
157
|
+
return wrap(NativeAppAttest.retry());
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
/** Wipes stored credentials and secrets. */
|
|
161
|
+
reset(): Promise<void> {
|
|
162
|
+
return wrap(NativeAppAttest.reset());
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Invalidate the cached secrets bundle and immediately sync. Keeps
|
|
167
|
+
* attestation credentials. Forces a 200 on the next sync, which
|
|
168
|
+
* consumes one credit on the production bucket.
|
|
169
|
+
*
|
|
170
|
+
* Use this for host-app "force refresh" / "sync now" UX. For wiping
|
|
171
|
+
* everything (including attestation), see ``reset()``.
|
|
172
|
+
*/
|
|
173
|
+
invalidateBundle(): Promise<void> {
|
|
174
|
+
return wrap(NativeAppAttest.invalidateBundle());
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
/** Returns the secret for `name`, or `null`. */
|
|
178
|
+
getSecret(name: string): Promise<string | null> {
|
|
179
|
+
return wrap(NativeAppAttest.getSecret(name));
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
/** Snapshot of every synced secret. */
|
|
183
|
+
getAllSecrets(): Promise<Record<string, string>> {
|
|
184
|
+
return wrap(NativeAppAttest.getAllSecrets());
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
/** Current state snapshot. */
|
|
188
|
+
async getState(): Promise<AppAttestState> {
|
|
189
|
+
const raw = await wrap(NativeAppAttest.getState());
|
|
190
|
+
return decodeState(raw as NativeStateRaw);
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
/** Subscribe to state transitions. Returns an unsubscribe fn. */
|
|
194
|
+
addStateListener(listener: (state: AppAttestState) => void): () => void {
|
|
195
|
+
const subscription = eventEmitter.addListener('stateChanged', (raw) => {
|
|
196
|
+
listener(decodeState(raw as NativeStateRaw));
|
|
197
|
+
});
|
|
198
|
+
return () => subscription.remove();
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
/** Set runtime mode. `'production'` (or `null`) is default. */
|
|
202
|
+
setDebugMode(mode: DebugMode | null, stubs?: Record<string, string>): Promise<void> {
|
|
203
|
+
return wrap(NativeAppAttest.setDebugMode(mode, stubs ?? null));
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
// setApiBaseUrl is not exposed — the base URL is hardcoded in the Swift
|
|
207
|
+
// SDK. There is no runtime override path from JavaScript.
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// MARK: - React hooks
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* React hook returning the current value of `secrets[name]`. Re-renders
|
|
214
|
+
* the host component whenever the underlying value changes (e.g. after
|
|
215
|
+
* a foreground rotation pulled new data).
|
|
216
|
+
*/
|
|
217
|
+
export function useSecret(name: string): string | null {
|
|
218
|
+
const [value, setValue] = useState<string | null>(null);
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
let active = true;
|
|
221
|
+
AppAttest.getSecret(name).then((v) => { if (active) setValue(v); }).catch(() => {});
|
|
222
|
+
const unsubscribe = AppAttest.addStateListener(async (s) => {
|
|
223
|
+
if (s.name === 'ready') {
|
|
224
|
+
try {
|
|
225
|
+
const v = await AppAttest.getSecret(name);
|
|
226
|
+
if (active) setValue(v);
|
|
227
|
+
} catch { /* ignore */ }
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
return () => {
|
|
231
|
+
active = false;
|
|
232
|
+
unsubscribe();
|
|
233
|
+
};
|
|
234
|
+
}, [name]);
|
|
235
|
+
return value;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* React hook returning every synced secret. Re-renders on rotation.
|
|
240
|
+
*/
|
|
241
|
+
export function useAllSecrets(): Record<string, string> {
|
|
242
|
+
const [all, setAll] = useState<Record<string, string>>({});
|
|
243
|
+
useEffect(() => {
|
|
244
|
+
let active = true;
|
|
245
|
+
AppAttest.getAllSecrets().then((v) => { if (active) setAll(v); }).catch(() => {});
|
|
246
|
+
const unsubscribe = AppAttest.addStateListener(async (s) => {
|
|
247
|
+
if (s.name === 'ready') {
|
|
248
|
+
try {
|
|
249
|
+
const v = await AppAttest.getAllSecrets();
|
|
250
|
+
if (active) setAll(v);
|
|
251
|
+
} catch { /* ignore */ }
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
return () => {
|
|
255
|
+
active = false;
|
|
256
|
+
unsubscribe();
|
|
257
|
+
};
|
|
258
|
+
}, []);
|
|
259
|
+
return all;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* React hook returning the current `AppAttestState`. Re-renders on
|
|
264
|
+
* every transition.
|
|
265
|
+
*/
|
|
266
|
+
export function useAppAttestState(): AppAttestState {
|
|
267
|
+
const [state, setState] = useState<AppAttestState>({ name: 'initializing' });
|
|
268
|
+
useEffect(() => {
|
|
269
|
+
let active = true;
|
|
270
|
+
AppAttest.getState().then((s) => { if (active) setState(s); }).catch(() => {});
|
|
271
|
+
const unsubscribe = AppAttest.addStateListener((s) => {
|
|
272
|
+
if (active) setState(s);
|
|
273
|
+
});
|
|
274
|
+
return () => {
|
|
275
|
+
active = false;
|
|
276
|
+
unsubscribe();
|
|
277
|
+
};
|
|
278
|
+
}, []);
|
|
279
|
+
return state;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export default AppAttest;
|