pushwave-client 0.2.5 → 0.2.7
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/README.md +14 -1
- package/android/src/main/java/com/pushwaveclient/PushwaveAttestationModule.kt +7 -1
- package/dist/attestation/attestation.types.d.ts +22 -0
- package/dist/attestation/attestation.types.js +2 -0
- package/dist/attestation/getApplicationAttestation.d.ts +2 -23
- package/dist/attestation/getApplicationAttestation.js +1 -6
- package/dist/attestation/index.d.ts +2 -0
- package/dist/attestation/index.js +18 -0
- package/dist/attestation/native.js +13 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -5
- package/dist/register/index.d.ts +2 -0
- package/dist/register/index.js +18 -0
- package/dist/register/registerPushWave.d.ts +2 -0
- package/dist/register/registerPushWave.dto.d.ts +14 -0
- package/dist/register/registerPushWave.dto.js +2 -0
- package/dist/register/registerPushWave.js +51 -0
- package/dist/utils/fetch.d.ts +2 -1
- package/dist/utils/fetch.js +30 -4
- package/dist/utils/pushwaveSettings.d.ts +5 -0
- package/dist/utils/pushwaveSettings.js +19 -0
- package/package.json +1 -1
- package/src/attestation/getApplicationAttestation.ts +0 -6
- package/src/attestation/native.ts +20 -1
- package/src/register/registerPushWave.ts +6 -4
- package/src/utils/fetch.ts +42 -6
- package/src/utils/pushwaveSettings.ts +22 -0
package/README.md
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
# pushwave-client (alpha)
|
|
2
2
|
|
|
3
|
-
Expo
|
|
3
|
+
PushWave is a lightweight Expo-first SDK to get push notifications running without building your own backend or wrestling with native setup. It fetches the Expo push token and prepares app attestation (Android Play Integrity / iOS DeviceCheck) so you can secure delivery. The SaaS dashboard (scheduling, targeting, templates, cron-like sends) is coming soon. **Consider this an early-stage project**.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Why PushWave?
|
|
8
|
+
|
|
9
|
+
- **No backend needed**: token storage, targeting logic, scheduling, and sending are all handled by PushWave’s cloud. Forget cron jobs and custom endpoints.
|
|
10
|
+
- **Expo-first design**: auto-linking, config plugin, no manual Gradle/Pod edits. Works seamlessly with EAS, dev clients, and Expo Router.
|
|
11
|
+
- **Native attestation (roadmap)**: Play Integrity on Android + DeviceCheck on iOS to reduce spoofed APKs, fake tokens, and leaked API keys.
|
|
12
|
+
- **One-line setup**: `PushWaveClient.init({ apiKey })` retrieves the Expo token, performs attestation when required, and logs enhanced debug info under `__DEV__`.
|
|
13
|
+
- **Dashboard-first workflow (roadmap)**: audiences, groups, segments, templates, one-off or recurring pushes without touching Firebase or APNs directly.
|
|
14
|
+
- **Minimal external config**: for Android you still upload your FCM credentials to Expo (required by the platform), but PushWave handles the rest.
|
|
4
15
|
|
|
5
16
|
---
|
|
6
17
|
|
|
@@ -61,6 +72,7 @@ export default function App() {
|
|
|
61
72
|
- The SDK retrieves the user’s Expo push token. If the user denies notification permission, no push will be delivered and the token may not be available depending on platform/permission.
|
|
62
73
|
- On iOS, user permission is required to obtain a push token.
|
|
63
74
|
- On Android, Expo handles permission/channel setup; if the user refuses, no push is delivered.
|
|
75
|
+
- You still need to provide FCM credentials to Expo for Android push (standard Expo requirement).
|
|
64
76
|
|
|
65
77
|
---
|
|
66
78
|
|
|
@@ -91,4 +103,5 @@ export default function App() {
|
|
|
91
103
|
|
|
92
104
|
- Server-side validation of Play Integrity / DeviceCheck tokens.
|
|
93
105
|
- Full attestation docs and Play Store setup (enable Integrity API, internal track).
|
|
106
|
+
- Dashboard for targeting, templates, scheduling (one-off and cron-like).
|
|
94
107
|
- Complete registration flow with the PushWave backend.
|
|
@@ -16,11 +16,17 @@ class PushwaveAttestationModule(private val context: ReactApplicationContext) :
|
|
|
16
16
|
override fun getName(): String = "PushwaveAttestation"
|
|
17
17
|
|
|
18
18
|
@ReactMethod
|
|
19
|
-
fun getIntegrityToken(nonce: String, promise: Promise) {
|
|
19
|
+
fun getIntegrityToken(nonce: String, cloudProjectNumber: Double?, promise: Promise) {
|
|
20
20
|
try {
|
|
21
|
+
if (cloudProjectNumber == null) {
|
|
22
|
+
promise.reject("PLAY_INTEGRITY_ERROR", "cloudProjectNumber is required")
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
val manager: IntegrityManager = IntegrityManagerFactory.create(context)
|
|
22
27
|
val request = IntegrityTokenRequest.builder()
|
|
23
28
|
.setNonce(nonce)
|
|
29
|
+
.setCloudProjectNumber(cloudProjectNumber.toLong())
|
|
24
30
|
.build()
|
|
25
31
|
|
|
26
32
|
manager.requestIntegrityToken(request)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface AndroidAttestationPayload {
|
|
2
|
+
status: "ok";
|
|
3
|
+
platform: "android";
|
|
4
|
+
nonce: string;
|
|
5
|
+
timestamp: number;
|
|
6
|
+
integrityToken: string;
|
|
7
|
+
}
|
|
8
|
+
export interface IosAttestationPayload {
|
|
9
|
+
status: "ok";
|
|
10
|
+
platform: "ios";
|
|
11
|
+
nonce: string;
|
|
12
|
+
timestamp: number;
|
|
13
|
+
deviceCheckToken: string;
|
|
14
|
+
}
|
|
15
|
+
export interface DisabledAttestation {
|
|
16
|
+
status: "disabled";
|
|
17
|
+
reason: string;
|
|
18
|
+
}
|
|
19
|
+
export interface SkippedAttestation {
|
|
20
|
+
status: "skipped";
|
|
21
|
+
}
|
|
22
|
+
export type ApplicationAttestation = AndroidAttestationPayload | IosAttestationPayload | DisabledAttestation | SkippedAttestation;
|
|
@@ -1,23 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
platform: "android";
|
|
4
|
-
nonce: string;
|
|
5
|
-
timestamp: number;
|
|
6
|
-
integrityToken: string;
|
|
7
|
-
}
|
|
8
|
-
export interface IosAttestationPayload {
|
|
9
|
-
status: "ok";
|
|
10
|
-
platform: "ios";
|
|
11
|
-
nonce: string;
|
|
12
|
-
timestamp: number;
|
|
13
|
-
deviceCheckToken: string;
|
|
14
|
-
}
|
|
15
|
-
export interface DisabledAttestation {
|
|
16
|
-
status: "disabled";
|
|
17
|
-
reason: string;
|
|
18
|
-
}
|
|
19
|
-
export interface SkippedAttestation {
|
|
20
|
-
status: "skipped";
|
|
21
|
-
}
|
|
22
|
-
export type ApplicationAttestation = AndroidAttestationPayload | IosAttestationPayload | DisabledAttestation | SkippedAttestation;
|
|
23
|
-
export default function getApplicationAttestation(apiKey: string): Promise<ApplicationAttestation>;
|
|
1
|
+
import { ApplicationAttestation } from "./index";
|
|
2
|
+
export declare function getApplicationAttestation(apiKey: string): Promise<ApplicationAttestation>;
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.getApplicationAttestation = getApplicationAttestation;
|
|
4
4
|
const buffer_1 = require("buffer");
|
|
5
5
|
const react_native_1 = require("react-native");
|
|
6
6
|
const native_1 = require("./native");
|
|
7
7
|
async function getApplicationAttestation(apiKey) {
|
|
8
|
-
if (!requiresAttestation(apiKey))
|
|
9
|
-
return { status: "skipped" };
|
|
10
8
|
const { nonce, timestamp } = createNonce();
|
|
11
9
|
try {
|
|
12
10
|
if (react_native_1.Platform.OS === "android")
|
|
@@ -20,9 +18,6 @@ async function getApplicationAttestation(apiKey) {
|
|
|
20
18
|
}
|
|
21
19
|
return { status: "disabled", reason: "platform-unsupported" };
|
|
22
20
|
}
|
|
23
|
-
function requiresAttestation(apiKey) {
|
|
24
|
-
return apiKey.startsWith("pw_pub_");
|
|
25
|
-
}
|
|
26
21
|
function createNonce() {
|
|
27
22
|
const timestamp = Date.now();
|
|
28
23
|
const random = Math.random().toString(36).slice(2);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./attestation.types"), exports);
|
|
18
|
+
__exportStar(require("./getApplicationAttestation"), exports);
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.getAndroidIntegrityToken = getAndroidIntegrityToken;
|
|
4
4
|
exports.getDeviceCheckToken = getDeviceCheckToken;
|
|
5
5
|
const react_native_1 = require("react-native");
|
|
6
|
+
const pushwaveSettings_1 = require("../utils/pushwaveSettings");
|
|
6
7
|
const MODULE_NAME = "PushwaveAttestation";
|
|
7
8
|
const NativeAttestation = react_native_1.NativeModules?.[MODULE_NAME];
|
|
8
9
|
function assertLinked() {
|
|
@@ -13,8 +14,10 @@ function assertLinked() {
|
|
|
13
14
|
async function getAndroidIntegrityToken(nonce) {
|
|
14
15
|
if (react_native_1.Platform.OS !== "android")
|
|
15
16
|
return;
|
|
17
|
+
const pushwaveSettings = await (0, pushwaveSettings_1.getPushwaveSettings)();
|
|
18
|
+
const cloudProjectNumber = parseCloudProjectNumber(pushwaveSettings);
|
|
16
19
|
assertLinked();
|
|
17
|
-
return NativeAttestation.getIntegrityToken(nonce);
|
|
20
|
+
return NativeAttestation.getIntegrityToken(nonce, cloudProjectNumber);
|
|
18
21
|
}
|
|
19
22
|
async function getDeviceCheckToken(nonce) {
|
|
20
23
|
if (react_native_1.Platform.OS !== "ios")
|
|
@@ -22,3 +25,12 @@ async function getDeviceCheckToken(nonce) {
|
|
|
22
25
|
assertLinked();
|
|
23
26
|
return NativeAttestation.getDeviceCheckToken(nonce);
|
|
24
27
|
}
|
|
28
|
+
function parseCloudProjectNumber(pushwaveSettings) {
|
|
29
|
+
const projectNumber = typeof pushwaveSettings?.cloudProjectNumber === "string"
|
|
30
|
+
? Number(pushwaveSettings.cloudProjectNumber)
|
|
31
|
+
: pushwaveSettings?.cloudProjectNumber;
|
|
32
|
+
if (typeof projectNumber !== "number" || !Number.isFinite(projectNumber)) {
|
|
33
|
+
throw new Error("Google Cloud Project Number missing.");
|
|
34
|
+
}
|
|
35
|
+
return projectNumber;
|
|
36
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RegisterPushWaveClient, RegisterPushWaveResponse } from "./
|
|
1
|
+
import { RegisterPushWaveClient, RegisterPushWaveResponse } from "./register";
|
|
2
2
|
export interface PushWaveClientType {
|
|
3
3
|
init(options: RegisterPushWaveClient): Promise<RegisterPushWaveResponse>;
|
|
4
4
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const
|
|
3
|
+
const register_1 = require("./register");
|
|
7
4
|
const PushWaveClient = {
|
|
8
5
|
init(options) {
|
|
9
|
-
return (0,
|
|
6
|
+
return (0, register_1.registerPushWave)(options);
|
|
10
7
|
},
|
|
11
8
|
};
|
|
12
9
|
exports.default = PushWaveClient;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./registerPushWave"), exports);
|
|
18
|
+
__exportStar(require("./registerPushWave.dto"), exports);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface RegisterPushWaveClient {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
}
|
|
4
|
+
export interface RegisterPushWaveResponse {
|
|
5
|
+
success: boolean;
|
|
6
|
+
message?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface RegisterPushWaveDTO {
|
|
9
|
+
apiKey: string;
|
|
10
|
+
expoToken: string;
|
|
11
|
+
platform: string;
|
|
12
|
+
appAttestation?: any;
|
|
13
|
+
environment: "development" | "production";
|
|
14
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerPushWave = registerPushWave;
|
|
4
|
+
const fetch_1 = require("../utils/fetch");
|
|
5
|
+
const expoToken_1 = require("../utils/expoToken");
|
|
6
|
+
const react_native_1 = require("react-native");
|
|
7
|
+
const pwLogger_1 = require("../utils/pwLogger");
|
|
8
|
+
const apiKeyCheck_1 = require("../utils/apiKeyCheck");
|
|
9
|
+
const index_1 = require("../attestation/index");
|
|
10
|
+
async function registerPushWave({ apiKey }) {
|
|
11
|
+
const OS = react_native_1.Platform.OS;
|
|
12
|
+
if ((0, apiKeyCheck_1.isSecretKey)(apiKey)) {
|
|
13
|
+
const warn = `\x1b[0m You are using your SECRET API key in a client environment. This key must NEVER be embedded in a mobile app.`;
|
|
14
|
+
pwLogger_1.PWLogger.warn(warn);
|
|
15
|
+
}
|
|
16
|
+
const expoToken = await (0, expoToken_1.getExpoToken)();
|
|
17
|
+
if (!expoToken) {
|
|
18
|
+
const message = "could not get ExpoToken";
|
|
19
|
+
pwLogger_1.PWLogger.error(message);
|
|
20
|
+
return {
|
|
21
|
+
success: false,
|
|
22
|
+
message: "[PushWaveClient] Error: " + message
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const appAttestation = await (0, index_1.getApplicationAttestation)(apiKey);
|
|
26
|
+
if (appAttestation.status === "disabled")
|
|
27
|
+
pwLogger_1.PWLogger.warn(`(${react_native_1.Platform.OS}) could not get attestation: ${appAttestation.reason}`);
|
|
28
|
+
const path = "expo-tokens";
|
|
29
|
+
const options = {
|
|
30
|
+
apiKey: apiKey,
|
|
31
|
+
expoToken: expoToken,
|
|
32
|
+
platform: OS,
|
|
33
|
+
appAttestation: appAttestation,
|
|
34
|
+
environment: __DEV__ ? "development" : "production"
|
|
35
|
+
};
|
|
36
|
+
try {
|
|
37
|
+
const res = await (0, fetch_1.fetchApiPost)(path, options);
|
|
38
|
+
return {
|
|
39
|
+
success: true,
|
|
40
|
+
message: res.message,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
const e = err;
|
|
45
|
+
pwLogger_1.PWLogger.error(e.message);
|
|
46
|
+
return {
|
|
47
|
+
success: false,
|
|
48
|
+
message: e.message,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
package/dist/utils/fetch.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export declare function
|
|
1
|
+
export declare function fetchApiPost<TResponse>(path: string, data?: Record<string, any>): Promise<TResponse>;
|
|
2
|
+
export declare function fetchApiGet<TResponse>(path: string, params?: Record<string, string | number | boolean | undefined>): Promise<TResponse>;
|
package/dist/utils/fetch.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
exports.fetchApiPost = fetchApiPost;
|
|
4
|
+
exports.fetchApiGet = fetchApiGet;
|
|
5
|
+
const BASE_URL = "https://pushwave.luruk-hai.fr/v1/";
|
|
6
|
+
async function fetchApiPost(path, data = {}) {
|
|
6
7
|
const url = BASE_URL + path;
|
|
7
|
-
console.log(data);
|
|
8
8
|
const res = await fetch(url, {
|
|
9
9
|
method: "POST",
|
|
10
10
|
headers: {
|
|
@@ -27,3 +27,29 @@ async function fetchApi(path, data) {
|
|
|
27
27
|
}
|
|
28
28
|
return json;
|
|
29
29
|
}
|
|
30
|
+
async function fetchApiGet(path, params) {
|
|
31
|
+
const search = params
|
|
32
|
+
? new URLSearchParams(Object.entries(params).reduce((acc, [key, value]) => {
|
|
33
|
+
if (value === undefined)
|
|
34
|
+
return acc;
|
|
35
|
+
acc[key] = String(value);
|
|
36
|
+
return acc;
|
|
37
|
+
}, {})).toString()
|
|
38
|
+
: "";
|
|
39
|
+
const url = BASE_URL + path + (search ? `?${search}` : "");
|
|
40
|
+
const res = await fetch(url);
|
|
41
|
+
const text = await res.text();
|
|
42
|
+
let json = null;
|
|
43
|
+
try {
|
|
44
|
+
json = text ? JSON.parse(text) : null;
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
// JSON empty/invalid
|
|
48
|
+
}
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
const apiError = (json?.error ?? json?.message ?? "");
|
|
51
|
+
const message = `(${res.status}) ${apiError}`.trim();
|
|
52
|
+
throw new Error(message);
|
|
53
|
+
}
|
|
54
|
+
return json;
|
|
55
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getPushwaveSettings = getPushwaveSettings;
|
|
4
|
+
const react_native_1 = require("react-native");
|
|
5
|
+
const fetch_1 = require("./fetch");
|
|
6
|
+
const pwLogger_1 = require("./pwLogger");
|
|
7
|
+
const pushwaveSettingsPromise = (async () => {
|
|
8
|
+
try {
|
|
9
|
+
return await (0, fetch_1.fetchApiGet)("pushwave-config", { platform: react_native_1.Platform.OS });
|
|
10
|
+
}
|
|
11
|
+
catch (e) {
|
|
12
|
+
// log si besoin
|
|
13
|
+
pwLogger_1.PWLogger.warn(`Unable to load PushWave configuration: ${e}`);
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
})();
|
|
17
|
+
function getPushwaveSettings() {
|
|
18
|
+
return pushwaveSettingsPromise;
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -4,8 +4,6 @@ import { getAndroidIntegrityToken, getDeviceCheckToken } from "./native";
|
|
|
4
4
|
import { ApplicationAttestation, AndroidAttestationPayload, DisabledAttestation, IosAttestationPayload } from "./index";
|
|
5
5
|
|
|
6
6
|
export async function getApplicationAttestation(apiKey: string): Promise<ApplicationAttestation> {
|
|
7
|
-
if (!requiresAttestation(apiKey)) return { status: "skipped" };
|
|
8
|
-
|
|
9
7
|
const { nonce, timestamp } = createNonce();
|
|
10
8
|
|
|
11
9
|
try {
|
|
@@ -19,10 +17,6 @@ export async function getApplicationAttestation(apiKey: string): Promise<Applica
|
|
|
19
17
|
return { status: "disabled", reason: "platform-unsupported" };
|
|
20
18
|
}
|
|
21
19
|
|
|
22
|
-
function requiresAttestation(apiKey: string) {
|
|
23
|
-
return apiKey.startsWith("pw_pub_");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
20
|
function createNonce() {
|
|
27
21
|
const timestamp = Date.now();
|
|
28
22
|
const random = Math.random().toString(36).slice(2);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NativeModules, Platform } from "react-native";
|
|
2
|
+
import { getPushwaveSettings, PushwaveSettings } from "../utils/pushwaveSettings";
|
|
2
3
|
|
|
3
4
|
const MODULE_NAME = "PushwaveAttestation";
|
|
4
5
|
const NativeAttestation = NativeModules?.[MODULE_NAME];
|
|
@@ -11,8 +12,13 @@ function assertLinked() {
|
|
|
11
12
|
|
|
12
13
|
export async function getAndroidIntegrityToken(nonce: string): Promise<string | undefined> {
|
|
13
14
|
if (Platform.OS !== "android") return;
|
|
15
|
+
|
|
16
|
+
const pushwaveSettings = await getPushwaveSettings();
|
|
17
|
+
|
|
18
|
+
const cloudProjectNumber = parseCloudProjectNumber(pushwaveSettings);
|
|
19
|
+
|
|
14
20
|
assertLinked();
|
|
15
|
-
return NativeAttestation.getIntegrityToken(nonce);
|
|
21
|
+
return NativeAttestation.getIntegrityToken(nonce, cloudProjectNumber);
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
export async function getDeviceCheckToken(nonce: string): Promise<string | undefined> {
|
|
@@ -20,3 +26,16 @@ export async function getDeviceCheckToken(nonce: string): Promise<string | undef
|
|
|
20
26
|
assertLinked();
|
|
21
27
|
return NativeAttestation.getDeviceCheckToken(nonce);
|
|
22
28
|
}
|
|
29
|
+
|
|
30
|
+
function parseCloudProjectNumber(pushwaveSettings: PushwaveSettings): number {
|
|
31
|
+
const projectNumber =
|
|
32
|
+
typeof pushwaveSettings?.cloudProjectNumber === "string"
|
|
33
|
+
? Number(pushwaveSettings.cloudProjectNumber)
|
|
34
|
+
: pushwaveSettings?.cloudProjectNumber;
|
|
35
|
+
|
|
36
|
+
if (typeof projectNumber !== "number" || !Number.isFinite(projectNumber)) {
|
|
37
|
+
throw new Error("Google Cloud Project Number missing.");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return projectNumber;
|
|
41
|
+
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fetchApiPost, fetchApiGet } from "../utils/fetch";
|
|
2
2
|
import { getExpoToken } from "../utils/expoToken";
|
|
3
3
|
import { Platform } from "react-native";
|
|
4
4
|
import { PWLogger } from "../utils/pwLogger";
|
|
5
5
|
import { isSecretKey } from "../utils/apiKeyCheck";
|
|
6
6
|
import { RegisterPushWaveClient, RegisterPushWaveDTO, RegisterPushWaveResponse } from "./registerPushWave.dto";
|
|
7
7
|
import { getApplicationAttestation } from "../attestation/index";
|
|
8
|
+
import { platform } from "os";
|
|
8
9
|
|
|
9
10
|
export async function registerPushWave(
|
|
10
11
|
{ apiKey }: RegisterPushWaveClient
|
|
11
12
|
): Promise<RegisterPushWaveResponse> {
|
|
13
|
+
const OS = Platform.OS;
|
|
12
14
|
|
|
13
15
|
if (isSecretKey(apiKey)) {
|
|
14
16
|
const warn = `\x1b[0m You are using your SECRET API key in a client environment. This key must NEVER be embedded in a mobile app.`;
|
|
@@ -34,18 +36,18 @@ export async function registerPushWave(
|
|
|
34
36
|
if (appAttestation.status === "disabled")
|
|
35
37
|
PWLogger.warn(`(${Platform.OS}) could not get attestation: ${appAttestation.reason}`);
|
|
36
38
|
|
|
37
|
-
const path = "
|
|
39
|
+
const path = "expo-tokens"
|
|
38
40
|
|
|
39
41
|
const options: RegisterPushWaveDTO = {
|
|
40
42
|
apiKey: apiKey,
|
|
41
43
|
expoToken: expoToken,
|
|
42
|
-
platform:
|
|
44
|
+
platform: OS,
|
|
43
45
|
appAttestation: appAttestation,
|
|
44
46
|
environment: __DEV__ ? "development" : "production"
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
try {
|
|
48
|
-
const res: RegisterPushWaveResponse = await
|
|
50
|
+
const res: RegisterPushWaveResponse = await fetchApiPost(path, options)
|
|
49
51
|
|
|
50
52
|
return {
|
|
51
53
|
success: true,
|
package/src/utils/fetch.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
const BASE_URL = "https://pushwave.luruk-hai.fr";
|
|
1
|
+
const BASE_URL = "https://pushwave.luruk-hai.fr/v1/";
|
|
2
2
|
|
|
3
|
-
export async function
|
|
3
|
+
export async function fetchApiPost<TResponse>(
|
|
4
4
|
path: string,
|
|
5
|
-
data: Record<string, any>
|
|
5
|
+
data: Record<string, any> = {}
|
|
6
6
|
): Promise<TResponse> {
|
|
7
7
|
|
|
8
8
|
const url = BASE_URL + path;
|
|
9
9
|
|
|
10
|
-
console.log(data);
|
|
11
|
-
|
|
12
10
|
const res = await fetch(url, {
|
|
13
11
|
method: "POST",
|
|
14
12
|
headers: {
|
|
@@ -35,4 +33,42 @@ export async function fetchApi<TResponse>(
|
|
|
35
33
|
}
|
|
36
34
|
|
|
37
35
|
return json as TResponse;
|
|
38
|
-
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function fetchApiGet<TResponse>(
|
|
39
|
+
path: string,
|
|
40
|
+
params?: Record<string, string | number | boolean | undefined>
|
|
41
|
+
): Promise<TResponse> {
|
|
42
|
+
const search = params
|
|
43
|
+
? new URLSearchParams(
|
|
44
|
+
Object.entries(params).reduce<Record<string, string>>((acc, [key, value]) => {
|
|
45
|
+
if (value === undefined) return acc;
|
|
46
|
+
acc[key] = String(value);
|
|
47
|
+
return acc;
|
|
48
|
+
}, {})
|
|
49
|
+
).toString()
|
|
50
|
+
: "";
|
|
51
|
+
|
|
52
|
+
const url = BASE_URL + path + (search ? `?${search}` : "");
|
|
53
|
+
|
|
54
|
+
const res = await fetch(url);
|
|
55
|
+
|
|
56
|
+
const text = await res.text();
|
|
57
|
+
let json: any = null;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
json = text ? JSON.parse(text) : null;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
// JSON empty/invalid
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!res.ok) {
|
|
66
|
+
const apiError = (json?.error ?? json?.message ?? "") as string;
|
|
67
|
+
|
|
68
|
+
const message = `(${res.status}) ${apiError}`.trim();
|
|
69
|
+
|
|
70
|
+
throw new Error(message);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return json as TResponse;
|
|
74
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Platform } from "react-native";
|
|
2
|
+
import { fetchApiGet } from "./fetch";
|
|
3
|
+
import { PWLogger } from "./pwLogger";
|
|
4
|
+
|
|
5
|
+
export type PushwaveSettings = {
|
|
6
|
+
cloudProjectNumber?: string | number;
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const pushwaveSettingsPromise: Promise<PushwaveSettings> = (async () => {
|
|
11
|
+
try {
|
|
12
|
+
return await fetchApiGet("pushwave-config", { platform: Platform.OS });
|
|
13
|
+
} catch (e) {
|
|
14
|
+
// log si besoin
|
|
15
|
+
PWLogger.warn(`Unable to load PushWave configuration: ${e}`)
|
|
16
|
+
return {}
|
|
17
|
+
}
|
|
18
|
+
})();
|
|
19
|
+
|
|
20
|
+
export function getPushwaveSettings() {
|
|
21
|
+
return pushwaveSettingsPromise;
|
|
22
|
+
}
|