pushwave-client 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.
Files changed (45) hide show
  1. package/android/build.gradle +55 -0
  2. package/android/consumer-rules.pro +1 -0
  3. package/android/src/main/AndroidManifest.xml +7 -0
  4. package/android/src/main/java/com/pushwaveclient/PushwaveAttestationModule.kt +38 -0
  5. package/android/src/main/java/com/pushwaveclient/PushwaveAttestationPackage.kt +16 -0
  6. package/dist/attestation/getAndroidSignature.d.ts +0 -0
  7. package/dist/attestation/getAndroidSignature.js +1 -0
  8. package/dist/attestation/getApplicationAttestation.d.ts +16 -0
  9. package/dist/attestation/getApplicationAttestation.js +56 -0
  10. package/dist/attestation/getApplicationSignature.d.ts +1 -0
  11. package/dist/attestation/getApplicationSignature.js +9 -0
  12. package/dist/attestation/getIosDeviceCheck.d.ts +1 -0
  13. package/dist/attestation/getIosDeviceCheck.js +2 -0
  14. package/dist/attestation/native.d.ts +2 -0
  15. package/dist/attestation/native.js +24 -0
  16. package/dist/index.d.ts +6 -0
  17. package/dist/index.js +12 -0
  18. package/dist/registerPushWave.d.ts +14 -0
  19. package/dist/registerPushWave.js +58 -0
  20. package/dist/utils/apiKeyCheck.d.ts +1 -0
  21. package/dist/utils/apiKeyCheck.js +6 -0
  22. package/dist/utils/expoToken.d.ts +1 -0
  23. package/dist/utils/expoToken.js +59 -0
  24. package/dist/utils/fetch.d.ts +1 -0
  25. package/dist/utils/fetch.js +28 -0
  26. package/dist/utils/pwLogger.d.ts +6 -0
  27. package/dist/utils/pwLogger.js +35 -0
  28. package/dist/utils/validation.d.ts +0 -0
  29. package/dist/utils/validation.js +1 -0
  30. package/ios/PushwaveAttestation.m +11 -0
  31. package/ios/PushwaveAttestation.swift +37 -0
  32. package/package.json +39 -0
  33. package/plugin/index.js +46 -0
  34. package/pushwave-client.podspec +21 -0
  35. package/react-native.config.js +14 -0
  36. package/src/attestation/getApplicationAttestation.ts +92 -0
  37. package/src/attestation/native.ts +22 -0
  38. package/src/index.ts +16 -0
  39. package/src/registerPushWave.ts +87 -0
  40. package/src/utils/apiKeyCheck.ts +3 -0
  41. package/src/utils/expoToken.ts +26 -0
  42. package/src/utils/fetch.ts +36 -0
  43. package/src/utils/pwLogger.ts +49 -0
  44. package/src/utils/validation.ts +0 -0
  45. package/tsconfig.json +16 -0
@@ -0,0 +1,55 @@
1
+ buildscript {
2
+ ext.safeExtGet = { prop, fallback ->
3
+ return rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
4
+ }
5
+ ext.kotlin_version = safeExtGet("kotlinVersion", "1.9.22")
6
+ repositories {
7
+ google()
8
+ mavenCentral()
9
+ }
10
+ dependencies {
11
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
12
+ }
13
+ }
14
+
15
+ apply plugin: "com.android.library"
16
+ apply plugin: "org.jetbrains.kotlin.android"
17
+
18
+ def getExtOrDefault(name, defaultValue) {
19
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : defaultValue
20
+ }
21
+
22
+ android {
23
+ namespace "com.pushwaveclient"
24
+ compileSdkVersion getExtOrDefault("compileSdkVersion", 34)
25
+
26
+ defaultConfig {
27
+ minSdkVersion getExtOrDefault("minSdkVersion", 23)
28
+ targetSdkVersion getExtOrDefault("targetSdkVersion", 34)
29
+ consumerProguardFiles "consumer-rules.pro"
30
+ }
31
+
32
+ compileOptions {
33
+ sourceCompatibility JavaVersion.VERSION_17
34
+ targetCompatibility JavaVersion.VERSION_17
35
+ }
36
+
37
+ kotlinOptions {
38
+ jvmTarget = "17"
39
+ }
40
+
41
+ lintOptions {
42
+ abortOnError false
43
+ }
44
+ }
45
+
46
+ repositories {
47
+ google()
48
+ mavenCentral()
49
+ }
50
+
51
+ dependencies {
52
+ implementation "com.facebook.react:react-native:+"
53
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
54
+ implementation "com.google.android.play:integrity:1.3.0"
55
+ }
@@ -0,0 +1 @@
1
+ # Keep file intentionally empty; no consumer rules required for this library.
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
3
+ package="com.pushwaveclient">
4
+
5
+ <application />
6
+
7
+ </manifest>
@@ -0,0 +1,38 @@
1
+ package com.pushwaveclient
2
+
3
+ import com.facebook.react.bridge.Promise
4
+ import com.facebook.react.bridge.ReactApplicationContext
5
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
6
+ import com.facebook.react.bridge.ReactMethod
7
+ import com.google.android.gms.tasks.OnFailureListener
8
+ import com.google.android.gms.tasks.OnSuccessListener
9
+ import com.google.android.play.core.integrity.IntegrityManagerFactory
10
+ import com.google.android.play.core.integrity.StandardIntegrityManager
11
+ import com.google.android.play.core.integrity.StandardIntegrityTokenRequest
12
+
13
+ class PushwaveAttestationModule(private val context: ReactApplicationContext) :
14
+ ReactContextBaseJavaModule(context) {
15
+
16
+ override fun getName(): String = "PushwaveAttestation"
17
+
18
+ @ReactMethod
19
+ fun getIntegrityToken(nonce: String, promise: Promise) {
20
+ try {
21
+ val integrityManager = IntegrityManagerFactory.create(context)
22
+ val standard: StandardIntegrityManager = integrityManager.standardIntegrityManager()
23
+ val request = StandardIntegrityTokenRequest.builder()
24
+ .setNonce(nonce)
25
+ .build()
26
+
27
+ standard.requestIntegrityToken(request)
28
+ .addOnSuccessListener(OnSuccessListener { response ->
29
+ promise.resolve(response.token())
30
+ })
31
+ .addOnFailureListener(OnFailureListener { error ->
32
+ promise.reject("PLAY_INTEGRITY_ERROR", error.localizedMessage, error)
33
+ })
34
+ } catch (e: Exception) {
35
+ promise.reject("PLAY_INTEGRITY_ERROR", e.localizedMessage, e)
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,16 @@
1
+ package com.pushwaveclient
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+ class PushwaveAttestationPackage : ReactPackage {
9
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
10
+ return listOf(PushwaveAttestationModule(reactContext))
11
+ }
12
+
13
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
14
+ return emptyList()
15
+ }
16
+ }
File without changes
@@ -0,0 +1 @@
1
+ "use strict";
@@ -0,0 +1,16 @@
1
+ export interface AndroidAttestationPayload {
2
+ nonce: string;
3
+ timestamp: number;
4
+ integrityToken: string;
5
+ }
6
+ export interface IosAttestationPayload {
7
+ nonce: string;
8
+ timestamp: number;
9
+ deviceCheckToken: string;
10
+ }
11
+ export interface DisabledAttestation {
12
+ attestationDisabled: true;
13
+ reason: string;
14
+ }
15
+ export type ApplicationAttestation = AndroidAttestationPayload | IosAttestationPayload | DisabledAttestation | true;
16
+ export default function getApplicationAttestation(apiKey: string): Promise<ApplicationAttestation>;
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = getApplicationAttestation;
4
+ const buffer_1 = require("buffer");
5
+ const react_native_1 = require("react-native");
6
+ const native_1 = require("./native");
7
+ async function getApplicationAttestation(apiKey) {
8
+ if (!requiresAttestation(apiKey))
9
+ return true;
10
+ const { nonce, timestamp } = createNonce();
11
+ try {
12
+ if (react_native_1.Platform.OS === "android")
13
+ return await getAndroidSignature(nonce, timestamp);
14
+ if (react_native_1.Platform.OS === "ios")
15
+ return await getIosDeviceCheck(nonce, timestamp);
16
+ }
17
+ catch (err) {
18
+ const reason = err?.message ?? "attestation-error";
19
+ return { attestationDisabled: true, reason };
20
+ }
21
+ return { attestationDisabled: true, reason: "platform-unsupported" };
22
+ }
23
+ function requiresAttestation(apiKey) {
24
+ return apiKey.startsWith("pw_pub_");
25
+ }
26
+ function createNonce() {
27
+ const timestamp = Date.now();
28
+ const random = Math.random().toString(36).slice(2);
29
+ const raw = `${timestamp}:${random}`;
30
+ const nonce = base64UrlEncode(raw);
31
+ return { nonce, timestamp };
32
+ }
33
+ function base64UrlEncode(input) {
34
+ if (typeof globalThis.btoa === "function") {
35
+ return globalThis.btoa(input).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
36
+ }
37
+ return buffer_1.Buffer.from(input, "utf8")
38
+ .toString("base64")
39
+ .replace(/\+/g, "-")
40
+ .replace(/\//g, "_")
41
+ .replace(/=+$/, "");
42
+ }
43
+ async function getAndroidSignature(nonce, timestamp) {
44
+ const integrityToken = await (0, native_1.getAndroidIntegrityToken)(nonce);
45
+ if (!integrityToken) {
46
+ return { attestationDisabled: true, reason: "play-integrity-unavailable" };
47
+ }
48
+ return { nonce, timestamp, integrityToken };
49
+ }
50
+ async function getIosDeviceCheck(nonce, timestamp) {
51
+ const deviceCheckToken = await (0, native_1.getDeviceCheckToken)(nonce);
52
+ if (!deviceCheckToken) {
53
+ return { attestationDisabled: true, reason: "devicecheck-unavailable" };
54
+ }
55
+ return { nonce, timestamp, deviceCheckToken };
56
+ }
@@ -0,0 +1 @@
1
+ export default function getApplicationSignature(): void;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = getApplicationSignature;
4
+ function getApplicationSignature() {
5
+ }
6
+ function getAndroidSignature() {
7
+ }
8
+ function getIosDeviceCheck() {
9
+ }
@@ -0,0 +1 @@
1
+ export default function (): any;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,2 @@
1
+ export declare function getAndroidIntegrityToken(nonce: string): Promise<string | undefined>;
2
+ export declare function getDeviceCheckToken(nonce: string): Promise<string | undefined>;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getAndroidIntegrityToken = getAndroidIntegrityToken;
4
+ exports.getDeviceCheckToken = getDeviceCheckToken;
5
+ const react_native_1 = require("react-native");
6
+ const MODULE_NAME = "PushwaveAttestation";
7
+ const NativeAttestation = react_native_1.NativeModules?.[MODULE_NAME];
8
+ function assertLinked() {
9
+ if (!NativeAttestation) {
10
+ throw new Error("native-module-unavailable");
11
+ }
12
+ }
13
+ async function getAndroidIntegrityToken(nonce) {
14
+ if (react_native_1.Platform.OS !== "android")
15
+ return;
16
+ assertLinked();
17
+ return NativeAttestation.getIntegrityToken(nonce);
18
+ }
19
+ async function getDeviceCheckToken(nonce) {
20
+ if (react_native_1.Platform.OS !== "ios")
21
+ return;
22
+ assertLinked();
23
+ return NativeAttestation.getDeviceCheckToken(nonce);
24
+ }
@@ -0,0 +1,6 @@
1
+ import { RegisterPushWaveClient, RegisterPushWaveResponse } from "./registerPushWave";
2
+ export interface PushWaveClientType {
3
+ init(options: RegisterPushWaveClient): Promise<RegisterPushWaveResponse>;
4
+ }
5
+ declare const PushWaveClient: PushWaveClientType;
6
+ export default PushWaveClient;
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const registerPushWave_1 = __importDefault(require("./registerPushWave"));
7
+ const PushWaveClient = {
8
+ init(options) {
9
+ return (0, registerPushWave_1.default)(options);
10
+ },
11
+ };
12
+ exports.default = PushWaveClient;
@@ -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 RegisterPushWaveOptions {
9
+ apiKey: string;
10
+ expoToken: string;
11
+ platform: string;
12
+ appAttestation?: any;
13
+ }
14
+ export default function registerPushWave({ apiKey }: RegisterPushWaveClient): Promise<RegisterPushWaveResponse>;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = registerPushWave;
7
+ const fetch_1 = require("./utils/fetch");
8
+ const expoToken_1 = require("./utils/expoToken");
9
+ const getApplicationAttestation_1 = __importDefault(require("./attestation/getApplicationAttestation"));
10
+ const react_native_1 = require("react-native");
11
+ const pwLogger_1 = require("./utils/pwLogger");
12
+ const apiKeyCheck_1 = require("./utils/apiKeyCheck");
13
+ async function registerPushWave({ apiKey }) {
14
+ if ((0, apiKeyCheck_1.isSecretKey)(apiKey)) {
15
+ 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.`;
16
+ pwLogger_1.PWLogger.warn(warn);
17
+ }
18
+ const expoToken = await (0, expoToken_1.getExpoToken)();
19
+ if (!expoToken) {
20
+ const message = "could not get ExpoToken";
21
+ pwLogger_1.PWLogger.error(message);
22
+ return {
23
+ success: false,
24
+ message: "[PushWaveClient] Error: " + message
25
+ };
26
+ }
27
+ const appAttestation = await (0, getApplicationAttestation_1.default)(apiKey);
28
+ if (!appAttestation) {
29
+ const message = `could not get ${react_native_1.Platform.OS} attestation.`;
30
+ pwLogger_1.PWLogger.error(message);
31
+ return {
32
+ success: false,
33
+ message: `[PushWaveClient] Error: ` + message
34
+ };
35
+ }
36
+ const path = "/v1/expo-tokens";
37
+ const options = {
38
+ apiKey: apiKey,
39
+ expoToken: expoToken,
40
+ platform: react_native_1.Platform.OS,
41
+ appAttestation: appAttestation
42
+ };
43
+ try {
44
+ const res = await (0, fetch_1.fetchApi)(path, options);
45
+ return {
46
+ success: true,
47
+ message: res.message,
48
+ };
49
+ }
50
+ catch (err) {
51
+ const e = err;
52
+ pwLogger_1.PWLogger.error(e.message);
53
+ return {
54
+ success: false,
55
+ message: e.message,
56
+ };
57
+ }
58
+ }
@@ -0,0 +1 @@
1
+ export declare function isSecretKey(apiKey: string): boolean;
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isSecretKey = isSecretKey;
4
+ function isSecretKey(apiKey) {
5
+ return apiKey.startsWith("pw_sec_");
6
+ }
@@ -0,0 +1 @@
1
+ export declare function getExpoToken(): Promise<string | undefined>;
@@ -0,0 +1,59 @@
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 __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.getExpoToken = getExpoToken;
40
+ const Notifications = __importStar(require("expo-notifications"));
41
+ const expo_constants_1 = __importDefault(require("expo-constants"));
42
+ async function getExpoToken() {
43
+ try {
44
+ const existing = await Notifications.getPermissionsAsync();
45
+ const finalStatus = existing.status === "granted"
46
+ ? existing.status
47
+ : (await Notifications.requestPermissionsAsync()).status;
48
+ if (finalStatus !== "granted")
49
+ return;
50
+ const projectId = expo_constants_1.default.expoConfig?.extra?.eas?.projectId ?? expo_constants_1.default.easConfig?.projectId;
51
+ const token = (projectId
52
+ ? await Notifications.getExpoPushTokenAsync({ projectId })
53
+ : await Notifications.getExpoPushTokenAsync()).data;
54
+ return token;
55
+ }
56
+ catch (e) {
57
+ throw new Error(`[expo-notifications] Error: ` + e);
58
+ }
59
+ }
@@ -0,0 +1 @@
1
+ export declare function fetchApi<TResponse>(path: string, data: Record<string, any>): Promise<TResponse>;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchApi = fetchApi;
4
+ const BASE_URL = "https://pushwave.luruk-hai.fr";
5
+ async function fetchApi(path, data) {
6
+ const url = BASE_URL + path;
7
+ const res = await fetch(url, {
8
+ method: "POST",
9
+ headers: {
10
+ "Content-Type": "application/json",
11
+ },
12
+ body: JSON.stringify(data),
13
+ });
14
+ const text = await res.text();
15
+ let json = null;
16
+ try {
17
+ json = text ? JSON.parse(text) : null;
18
+ }
19
+ catch (err) {
20
+ // JSON empty/invalid
21
+ }
22
+ if (!res.ok) {
23
+ const apiError = (json?.error ?? json?.message ?? "");
24
+ const message = `(${res.status}) ${apiError}`.trim();
25
+ throw new Error(message);
26
+ }
27
+ return json;
28
+ }
@@ -0,0 +1,6 @@
1
+ export declare const PWLogger: {
2
+ info: (...args: any[]) => void;
3
+ warn: (...args: any[]) => void;
4
+ error: (...args: any[]) => void;
5
+ success: (...args: any[]) => void;
6
+ };
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ // utils/pwLogger.ts
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.PWLogger = void 0;
5
+ const COLORS = {
6
+ reset: "\x1b[0m",
7
+ bold: "\x1b[1m",
8
+ dim: "\x1b[2m",
9
+ red: "\x1b[31m",
10
+ yellow: "\x1b[33m",
11
+ cyan: "\x1b[36m",
12
+ green: "\x1b[32m"
13
+ };
14
+ exports.PWLogger = {
15
+ info: (...args) => {
16
+ if (__DEV__) {
17
+ console.log(`${COLORS.cyan}${COLORS.bold}[PushWave INFO]${COLORS.reset}`, ...args);
18
+ }
19
+ },
20
+ warn: (...args) => {
21
+ if (__DEV__) {
22
+ console.warn(`${COLORS.yellow}${COLORS.bold}⚠️ [PushWave WARNING]${COLORS.reset}`, ...args);
23
+ }
24
+ },
25
+ error: (...args) => {
26
+ if (__DEV__) {
27
+ console.error(`${COLORS.red}${COLORS.bold}❌ [PushWave ERROR]${COLORS.reset}`, ...args);
28
+ }
29
+ },
30
+ success: (...args) => {
31
+ if (__DEV__) {
32
+ console.log(`${COLORS.green}${COLORS.bold}✔️ [PushWave]${COLORS.reset}`, ...args);
33
+ }
34
+ }
35
+ };
File without changes
@@ -0,0 +1 @@
1
+ "use strict";
@@ -0,0 +1,11 @@
1
+ #import <React/RCTBridgeModule.h>
2
+
3
+ @interface RCT_EXTERN_MODULE(PushwaveAttestation, NSObject)
4
+
5
+ RCT_EXTERN_METHOD(
6
+ getDeviceCheckToken:(NSString *)nonce
7
+ resolver:(RCTPromiseResolveBlock)resolve
8
+ rejecter:(RCTPromiseRejectBlock)reject
9
+ )
10
+
11
+ @end
@@ -0,0 +1,37 @@
1
+ import DeviceCheck
2
+ import Foundation
3
+ import React
4
+
5
+ @objc(PushwaveAttestation)
6
+ class PushwaveAttestation: NSObject {
7
+ @objc
8
+ static func requiresMainQueueSetup() -> Bool {
9
+ return false
10
+ }
11
+
12
+ @objc
13
+ func getDeviceCheckToken(
14
+ _ nonce: NSString,
15
+ resolver resolve: @escaping RCTPromiseResolveBlock,
16
+ rejecter reject: @escaping RCTPromiseRejectBlock
17
+ ) {
18
+ guard DCDevice.current.isSupported else {
19
+ resolve(NSNull())
20
+ return
21
+ }
22
+
23
+ DCDevice.current.generateToken { data, error in
24
+ if let error = error {
25
+ reject("DEVICE_CHECK_ERROR", error.localizedDescription, error)
26
+ return
27
+ }
28
+
29
+ guard let tokenData = data else {
30
+ resolve(NSNull())
31
+ return
32
+ }
33
+
34
+ resolve(tokenData.base64EncodedString())
35
+ }
36
+ }
37
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "pushwave-client",
3
+ "version": "0.1.0",
4
+ "description": "PushWave Client, Expo Push Notifications SaaS SDK",
5
+ "homepage": "https://github.com/luruk-hai/pushwave-client#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/luruk-hai/pushwave-client/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/luruk-hai/pushwave-client.git"
12
+ },
13
+ "license": "MIT",
14
+ "author": "",
15
+ "main": "dist/index.js",
16
+ "types": "dist/index.d.ts",
17
+ "expo": {
18
+ "plugin": "./plugin/index.js"
19
+ },
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "clean": "rm -rf dist"
23
+ },
24
+ "peerDependencies": {
25
+ "expo-constants": "*",
26
+ "expo-notifications": "*",
27
+ "react-native": "*",
28
+ "expo": "*"
29
+ },
30
+ "dependencies": {
31
+ "cross-fetch": "^4.1.0",
32
+ "@expo/config-plugins": "^8.0.6"
33
+ },
34
+ "devDependencies": {
35
+ "typescript": "^5.9.3",
36
+ "ts-node": "^10.9.2",
37
+ "@types/node": "^24.10.1"
38
+ }
39
+ }
@@ -0,0 +1,46 @@
1
+ const {
2
+ createRunOncePlugin,
3
+ withAppBuildGradle,
4
+ } = require("@expo/config-plugins");
5
+
6
+ const pkg = require("../package.json");
7
+
8
+ const PLAY_INTEGRITY_DEP = 'implementation("com.google.android.play:integrity:1.3.0")';
9
+
10
+ function withPushwaveAndroid(config) {
11
+ return withAppBuildGradle(config, (config) => {
12
+ if (config.modResults.language !== "groovy") {
13
+ return config;
14
+ }
15
+
16
+ const content = config.modResults.contents;
17
+
18
+ if (content.includes(PLAY_INTEGRITY_DEP)) {
19
+ return config;
20
+ }
21
+
22
+ config.modResults.contents = content.replace(
23
+ /dependencies\s?{[^}]*}/,
24
+ (match) => {
25
+ if (match.includes(PLAY_INTEGRITY_DEP)) return match;
26
+ return match.replace(
27
+ /dependencies\s?{/,
28
+ `dependencies {\n ${PLAY_INTEGRITY_DEP}\n`
29
+ );
30
+ }
31
+ );
32
+
33
+ return config;
34
+ });
35
+ }
36
+
37
+ const withPushwaveClient = (config) => {
38
+ config = withPushwaveAndroid(config);
39
+ return config;
40
+ };
41
+
42
+ module.exports = createRunOncePlugin(
43
+ withPushwaveClient,
44
+ pkg.name,
45
+ pkg.version
46
+ );
@@ -0,0 +1,21 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "pushwave-client"
7
+ s.version = package["version"]
8
+ s.summary = package["description"] || "PushWave Client SDK"
9
+ s.homepage = package["homepage"] || "https://github.com/luruk-hai/pushwave-client"
10
+ s.license = package["license"] || "MIT"
11
+ s.author = package["author"] || { "PushWave" => "support@pushwave.dev" }
12
+ s.platforms = { :ios => "13.0" }
13
+ s.source = { :git => package["repository"] ? package["repository"]["url"] : "https://github.com/luruk-hai/pushwave-client.git",
14
+ :tag => "v#{s.version}" }
15
+
16
+ s.source_files = "ios/**/*.{h,m,mm,swift}"
17
+ s.requires_arc = true
18
+ s.swift_version = "5.0"
19
+ s.frameworks = "DeviceCheck"
20
+ s.dependency "React-Core"
21
+ end
@@ -0,0 +1,14 @@
1
+ module.exports = {
2
+ dependency: {
3
+ platforms: {
4
+ ios: {
5
+ podspecPath: "pushwave-client.podspec",
6
+ },
7
+ android: {
8
+ sourceDir: "android",
9
+ packageImportPath: "import com.pushwaveclient.PushwaveAttestationPackage",
10
+ packageInstance: "new PushwaveAttestationPackage()",
11
+ },
12
+ },
13
+ },
14
+ };
@@ -0,0 +1,92 @@
1
+ import { Buffer } from "buffer";
2
+ import { Platform } from "react-native";
3
+ import { getAndroidIntegrityToken, getDeviceCheckToken } from "./native";
4
+
5
+ export interface AndroidAttestationPayload {
6
+ nonce: string;
7
+ timestamp: number;
8
+ integrityToken: string;
9
+ }
10
+
11
+ export interface IosAttestationPayload {
12
+ nonce: string;
13
+ timestamp: number;
14
+ deviceCheckToken: string;
15
+ }
16
+
17
+ export interface DisabledAttestation {
18
+ attestationDisabled: true;
19
+ reason: string;
20
+ }
21
+
22
+ export type ApplicationAttestation =
23
+ | AndroidAttestationPayload
24
+ | IosAttestationPayload
25
+ | DisabledAttestation
26
+ | true;
27
+
28
+ export default async function getApplicationAttestation(apiKey: string): Promise<ApplicationAttestation> {
29
+ if (!requiresAttestation(apiKey)) return true;
30
+
31
+ const { nonce, timestamp } = createNonce();
32
+
33
+ try {
34
+ if (Platform.OS === "android") return await getAndroidSignature(nonce, timestamp);
35
+ if (Platform.OS === "ios") return await getIosDeviceCheck(nonce, timestamp);
36
+ } catch (err) {
37
+ const reason = (err as Error)?.message ?? "attestation-error";
38
+ return { attestationDisabled: true, reason };
39
+ }
40
+
41
+ return { attestationDisabled: true, reason: "platform-unsupported" };
42
+ }
43
+
44
+ function requiresAttestation(apiKey: string) {
45
+ return apiKey.startsWith("pw_pub_");
46
+ }
47
+
48
+ function createNonce() {
49
+ const timestamp = Date.now();
50
+ const random = Math.random().toString(36).slice(2);
51
+ const raw = `${timestamp}:${random}`;
52
+ const nonce = base64UrlEncode(raw);
53
+ return { nonce, timestamp };
54
+ }
55
+
56
+ function base64UrlEncode(input: string): string {
57
+ if (typeof globalThis.btoa === "function") {
58
+ return globalThis.btoa(input).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
59
+ }
60
+
61
+ return Buffer.from(input, "utf8")
62
+ .toString("base64")
63
+ .replace(/\+/g, "-")
64
+ .replace(/\//g, "_")
65
+ .replace(/=+$/, "");
66
+ }
67
+
68
+ async function getAndroidSignature(
69
+ nonce: string,
70
+ timestamp: number
71
+ ): Promise<AndroidAttestationPayload | DisabledAttestation> {
72
+ const integrityToken = await getAndroidIntegrityToken(nonce);
73
+
74
+ if (!integrityToken) {
75
+ return { attestationDisabled: true, reason: "play-integrity-unavailable" };
76
+ }
77
+
78
+ return { nonce, timestamp, integrityToken };
79
+ }
80
+
81
+ async function getIosDeviceCheck(
82
+ nonce: string,
83
+ timestamp: number
84
+ ): Promise<IosAttestationPayload | DisabledAttestation> {
85
+ const deviceCheckToken = await getDeviceCheckToken(nonce);
86
+
87
+ if (!deviceCheckToken) {
88
+ return { attestationDisabled: true, reason: "devicecheck-unavailable" };
89
+ }
90
+
91
+ return { nonce, timestamp, deviceCheckToken };
92
+ }
@@ -0,0 +1,22 @@
1
+ import { NativeModules, Platform } from "react-native";
2
+
3
+ const MODULE_NAME = "PushwaveAttestation";
4
+ const NativeAttestation = NativeModules?.[MODULE_NAME];
5
+
6
+ function assertLinked() {
7
+ if (!NativeAttestation) {
8
+ throw new Error("native-module-unavailable");
9
+ }
10
+ }
11
+
12
+ export async function getAndroidIntegrityToken(nonce: string): Promise<string | undefined> {
13
+ if (Platform.OS !== "android") return;
14
+ assertLinked();
15
+ return NativeAttestation.getIntegrityToken(nonce);
16
+ }
17
+
18
+ export async function getDeviceCheckToken(nonce: string): Promise<string | undefined> {
19
+ if (Platform.OS !== "ios") return;
20
+ assertLinked();
21
+ return NativeAttestation.getDeviceCheckToken(nonce);
22
+ }
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ import registerPushWave, {
2
+ RegisterPushWaveClient,
3
+ RegisterPushWaveResponse,
4
+ } from "./registerPushWave";
5
+
6
+ export interface PushWaveClientType {
7
+ init(options: RegisterPushWaveClient): Promise<RegisterPushWaveResponse>;
8
+ }
9
+
10
+ const PushWaveClient: PushWaveClientType = {
11
+ init(options) {
12
+ return registerPushWave(options);
13
+ },
14
+ };
15
+
16
+ export default PushWaveClient;
@@ -0,0 +1,87 @@
1
+ import { fetchApi } from "./utils/fetch";
2
+ import { getExpoToken } from "./utils/expoToken";
3
+ import getApplicationAttestation from "./attestation/getApplicationAttestation";
4
+ import { Platform } from "react-native";
5
+ import { PWLogger } from "./utils/pwLogger";
6
+ import { isSecretKey } from "./utils/apiKeyCheck";
7
+
8
+ export interface RegisterPushWaveClient {
9
+ apiKey: string;
10
+ }
11
+
12
+ export interface RegisterPushWaveResponse {
13
+ success: boolean;
14
+ message?: string;
15
+ }
16
+
17
+ export interface RegisterPushWaveOptions {
18
+ apiKey: string;
19
+ expoToken: string;
20
+ platform: string;
21
+ appAttestation?: any;
22
+ }
23
+
24
+ export default async function registerPushWave(
25
+ { apiKey }: RegisterPushWaveClient
26
+ ): Promise<RegisterPushWaveResponse> {
27
+
28
+ if (isSecretKey(apiKey)) {
29
+ 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.`;
30
+ PWLogger.warn(warn);
31
+ }
32
+
33
+ const expoToken = await getExpoToken();
34
+
35
+ if (!expoToken) {
36
+
37
+ const message = "could not get ExpoToken";
38
+
39
+ PWLogger.error(message)
40
+
41
+ return {
42
+ success: false,
43
+ message: "[PushWaveClient] Error: " + message
44
+ }
45
+ }
46
+
47
+ const appAttestation = await getApplicationAttestation(apiKey);
48
+
49
+ if (!appAttestation) {
50
+
51
+ const message = `could not get ${Platform.OS} attestation.`;
52
+
53
+ PWLogger.error(message);
54
+
55
+ return {
56
+ success: false,
57
+ message: `[PushWaveClient] Error: ` + message
58
+ }
59
+ }
60
+
61
+ const path = "/v1/expo-tokens"
62
+
63
+ const options: RegisterPushWaveOptions = {
64
+ apiKey: apiKey,
65
+ expoToken: expoToken,
66
+ platform: Platform.OS,
67
+ appAttestation: appAttestation
68
+ }
69
+
70
+ try {
71
+ const res: RegisterPushWaveResponse = await fetchApi(path, options)
72
+
73
+ return {
74
+ success: true,
75
+ message: res.message,
76
+ };
77
+ } catch (err) {
78
+ const e = err as Error;
79
+
80
+ PWLogger.error(e.message);
81
+
82
+ return {
83
+ success: false,
84
+ message: e.message,
85
+ };
86
+ }
87
+ }
@@ -0,0 +1,3 @@
1
+ export function isSecretKey(apiKey: string) {
2
+ return apiKey.startsWith("pw_sec_");
3
+ }
@@ -0,0 +1,26 @@
1
+ import * as Notifications from "expo-notifications";
2
+ import Constants from "expo-constants";
3
+
4
+ export async function getExpoToken() {
5
+ try {
6
+ const existing = await Notifications.getPermissionsAsync();
7
+ const finalStatus =
8
+ existing.status === "granted"
9
+ ? existing.status
10
+ : (await Notifications.requestPermissionsAsync()).status;
11
+ if (finalStatus !== "granted") return;
12
+
13
+ const projectId =
14
+ Constants.expoConfig?.extra?.eas?.projectId ?? Constants.easConfig?.projectId;
15
+
16
+ const token = (
17
+ projectId
18
+ ? await Notifications.getExpoPushTokenAsync({ projectId })
19
+ : await Notifications.getExpoPushTokenAsync()
20
+ ).data;
21
+
22
+ return token;
23
+ } catch (e) {
24
+ throw new Error(`[expo-notifications] Error: ` + e);
25
+ }
26
+ }
@@ -0,0 +1,36 @@
1
+ const BASE_URL = "https://pushwave.luruk-hai.fr";
2
+
3
+ export async function fetchApi<TResponse>(
4
+ path: string,
5
+ data: Record<string, any>
6
+ ): Promise<TResponse> {
7
+
8
+ const url = BASE_URL + path;
9
+
10
+ const res = await fetch(url, {
11
+ method: "POST",
12
+ headers: {
13
+ "Content-Type": "application/json",
14
+ },
15
+ body: JSON.stringify(data),
16
+ });
17
+
18
+ const text = await res.text();
19
+ let json: any = null;
20
+
21
+ try {
22
+ json = text ? JSON.parse(text) : null;
23
+ } catch (err) {
24
+ // JSON empty/invalid
25
+ }
26
+
27
+ if (!res.ok) {
28
+ const apiError = (json?.error ?? json?.message ?? "") as string;
29
+
30
+ const message = `(${res.status}) ${apiError}`.trim();
31
+
32
+ throw new Error(message);
33
+ }
34
+
35
+ return json as TResponse;
36
+ }
@@ -0,0 +1,49 @@
1
+ // utils/pwLogger.ts
2
+
3
+ const COLORS = {
4
+ reset: "\x1b[0m",
5
+ bold: "\x1b[1m",
6
+ dim: "\x1b[2m",
7
+ red: "\x1b[31m",
8
+ yellow: "\x1b[33m",
9
+ cyan: "\x1b[36m",
10
+ green: "\x1b[32m"
11
+ };
12
+
13
+ export const PWLogger = {
14
+ info: (...args: any[]) => {
15
+ if (__DEV__) {
16
+ console.log(
17
+ `${COLORS.cyan}${COLORS.bold}[PushWave INFO]${COLORS.reset}`,
18
+ ...args
19
+ );
20
+ }
21
+ },
22
+
23
+ warn: (...args: any[]) => {
24
+ if (__DEV__) {
25
+ console.warn(
26
+ `${COLORS.yellow}${COLORS.bold}⚠️ [PushWave WARNING]${COLORS.reset}`,
27
+ ...args
28
+ );
29
+ }
30
+ },
31
+
32
+ error: (...args: any[]) => {
33
+ if (__DEV__) {
34
+ console.error(
35
+ `${COLORS.red}${COLORS.bold}❌ [PushWave ERROR]${COLORS.reset}`,
36
+ ...args
37
+ );
38
+ }
39
+ },
40
+
41
+ success: (...args: any[]) => {
42
+ if (__DEV__) {
43
+ console.log(
44
+ `${COLORS.green}${COLORS.bold}✔️ [PushWave]${COLORS.reset}`,
45
+ ...args
46
+ );
47
+ }
48
+ }
49
+ };
File without changes
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "CommonJS",
5
+ "outDir": "dist",
6
+ "rootDir": "src",
7
+ "declaration": true,
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "moduleResolution": "node",
12
+ "jsx": "react-native"
13
+ },
14
+ "include": ["src"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }