pulse-updates 1.1.0 → 1.2.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/README.md +50 -10
- package/SECURITY.md +42 -0
- package/android/src/main/java/app/pulse/updates/PulseController.kt +6 -2
- package/android/src/main/java/app/pulse/updates/PulseUpdatesModule.kt +25 -0
- package/app.plugin.js +47 -0
- package/ios/PulseUpdates/PulseController.swift +7 -2
- package/ios/PulseUpdates/PulseUpdates.m +7 -0
- package/ios/PulseUpdates/PulseUpdates.swift +22 -0
- package/lib/commonjs/NativePulseUpdates.js.map +1 -1
- package/lib/commonjs/PulseUpdates.js +36 -0
- package/lib/commonjs/PulseUpdates.js.map +1 -1
- package/lib/commonjs/config.js +175 -21
- package/lib/commonjs/config.js.map +1 -1
- package/lib/commonjs/decisions.js +128 -0
- package/lib/commonjs/decisions.js.map +1 -0
- package/lib/commonjs/index.js +12 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/init.js +164 -12
- package/lib/commonjs/init.js.map +1 -1
- package/lib/commonjs/track.js +154 -12
- package/lib/commonjs/track.js.map +1 -1
- package/lib/commonjs/usePulseUpdates.js +21 -16
- package/lib/commonjs/usePulseUpdates.js.map +1 -1
- package/lib/module/NativePulseUpdates.js.map +1 -1
- package/lib/module/PulseUpdates.js +33 -0
- package/lib/module/PulseUpdates.js.map +1 -1
- package/lib/module/config.js +174 -21
- package/lib/module/config.js.map +1 -1
- package/lib/module/decisions.js +122 -0
- package/lib/module/decisions.js.map +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/init.js +165 -14
- package/lib/module/init.js.map +1 -1
- package/lib/module/track.js +152 -12
- package/lib/module/track.js.map +1 -1
- package/lib/module/usePulseUpdates.js +21 -16
- package/lib/module/usePulseUpdates.js.map +1 -1
- package/lib/typescript/NativePulseUpdates.d.ts +1 -0
- package/lib/typescript/NativePulseUpdates.d.ts.map +1 -1
- package/lib/typescript/PulseUpdates.d.ts +20 -0
- package/lib/typescript/PulseUpdates.d.ts.map +1 -1
- package/lib/typescript/config.d.ts +22 -0
- package/lib/typescript/config.d.ts.map +1 -1
- package/lib/typescript/decisions.d.ts +55 -0
- package/lib/typescript/decisions.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +1 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/init.d.ts +51 -4
- package/lib/typescript/init.d.ts.map +1 -1
- package/lib/typescript/track.d.ts +29 -1
- package/lib/typescript/track.d.ts.map +1 -1
- package/lib/typescript/usePulseUpdates.d.ts.map +1 -1
- package/logo.png +0 -0
- package/package.json +14 -3
- package/scripts/publish.mjs +68 -3
- package/src/NativePulseUpdates.ts +1 -0
- package/src/PulseUpdates.ts +54 -0
- package/src/config.ts +234 -21
- package/src/decisions.ts +179 -0
- package/src/index.ts +1 -0
- package/src/init.ts +240 -11
- package/src/track.ts +191 -13
- package/src/usePulseUpdates.ts +21 -16
package/lib/typescript/init.d.ts
CHANGED
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
* reliably on both platforms, and a version guessed wrong aims a rollout at the wrong
|
|
23
23
|
* installs — a targeting bug that looks like a rollout that "did not work".
|
|
24
24
|
*/
|
|
25
|
-
import { type ConfigStorage, type ConfigValue } from './config';
|
|
25
|
+
import { fetchConfig, type ConfigStorage, type ConfigValue, type ConfigSignature, getConfigInfo, getConfigBoolean, getConfigNumber, getConfigString, getConfigJson } from './config';
|
|
26
|
+
import { getTrackingInfo, type TrackedEventInput } from './track';
|
|
26
27
|
export interface InitPulseOptions {
|
|
27
28
|
/** Where Pulse lives, e.g. https://pulse.example.com — no path. */
|
|
28
29
|
apiUrl: string;
|
|
@@ -38,6 +39,8 @@ export interface InitPulseOptions {
|
|
|
38
39
|
storage?: ConfigStorage;
|
|
39
40
|
/** The store build, e.g. "5.2.0". Needed to target a rollout at a version. */
|
|
40
41
|
appVersion?: string;
|
|
42
|
+
/** Locale override. When omitted the installed RN locale adapter is detected. */
|
|
43
|
+
language?: string;
|
|
41
44
|
/**
|
|
42
45
|
* A stable id for this install. Left out, one is minted and persisted.
|
|
43
46
|
*
|
|
@@ -50,6 +53,16 @@ export interface InitPulseOptions {
|
|
|
50
53
|
getUserId?: () => string | undefined;
|
|
51
54
|
/** Attributes a rule can target — a plan, a storefront. Keep them few and stable. */
|
|
52
55
|
getUserAttributes?: () => Record<string, string> | undefined;
|
|
56
|
+
/** Initial analytics consent or a live consent reader. Defaults to granted for compatibility. */
|
|
57
|
+
analyticsConsent?: boolean | (() => boolean);
|
|
58
|
+
/** Privacy guardrails applied to every track() call. */
|
|
59
|
+
eventPropertyAllowlist?: readonly string[];
|
|
60
|
+
redactEventProperties?: readonly string[];
|
|
61
|
+
/** Signed config uses the same Ed25519 public key as OTA updates. */
|
|
62
|
+
signingPublicKey?: string;
|
|
63
|
+
signingKeyId?: string;
|
|
64
|
+
requireConfigSignature?: boolean;
|
|
65
|
+
verifyConfigSignature?: (canonicalPayload: string, signature: ConfigSignature) => boolean | Promise<boolean>;
|
|
53
66
|
/** Foreground poll interval for the config. 0 disables it; launch and resume still fetch. */
|
|
54
67
|
pollIntervalMs?: number;
|
|
55
68
|
/** How often queued events are sent. Default 10s. */
|
|
@@ -64,10 +77,44 @@ export interface InitPulseOptions {
|
|
|
64
77
|
onIgnoredEvents?: (events: string[]) => void;
|
|
65
78
|
onError?: (error: unknown) => void;
|
|
66
79
|
}
|
|
67
|
-
|
|
68
|
-
|
|
80
|
+
export interface PulseHealth {
|
|
81
|
+
appSlug: string;
|
|
82
|
+
deviceId: string;
|
|
83
|
+
config: ReturnType<typeof getConfigInfo>;
|
|
84
|
+
events: ReturnType<typeof getTrackingInfo>;
|
|
85
|
+
analyticsConsent: boolean;
|
|
86
|
+
}
|
|
87
|
+
export interface PulseClient {
|
|
69
88
|
deviceId: string;
|
|
70
89
|
configUrl: string;
|
|
71
90
|
trackUrl: string;
|
|
72
|
-
|
|
91
|
+
track: (event: string, props?: TrackedEventInput['props'], time?: Date) => void;
|
|
92
|
+
flush: () => Promise<number>;
|
|
93
|
+
setUser: (userId?: string, attributes?: Record<string, string>) => void;
|
|
94
|
+
clearUser: () => void;
|
|
95
|
+
setAnalyticsConsent: (granted: boolean) => void;
|
|
96
|
+
resetInstallId: () => string;
|
|
97
|
+
health: () => PulseHealth;
|
|
98
|
+
config: {
|
|
99
|
+
boolean: typeof getConfigBoolean;
|
|
100
|
+
number: typeof getConfigNumber;
|
|
101
|
+
string: typeof getConfigString;
|
|
102
|
+
json: typeof getConfigJson;
|
|
103
|
+
refresh: typeof fetchConfig;
|
|
104
|
+
};
|
|
105
|
+
dispose: () => Promise<void>;
|
|
106
|
+
}
|
|
107
|
+
export interface AsyncPulseStorage {
|
|
108
|
+
getItem(key: string): Promise<string | null>;
|
|
109
|
+
setItem(key: string, value: string): Promise<void>;
|
|
110
|
+
}
|
|
111
|
+
/** Wires config and events, and returns the context the two share. */
|
|
112
|
+
export declare function initPulse(opts: InitPulseOptions): PulseClient;
|
|
113
|
+
/**
|
|
114
|
+
* AsyncStorage-friendly one-call setup. It preloads only Pulse's three app-scoped
|
|
115
|
+
* records, then exposes the same synchronous hot-path API as initPulse.
|
|
116
|
+
*/
|
|
117
|
+
export declare function initPulseAsync(opts: Omit<InitPulseOptions, 'storage'> & {
|
|
118
|
+
storage: AsyncPulseStorage;
|
|
119
|
+
}): Promise<PulseClient>;
|
|
73
120
|
//# sourceMappingURL=init.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/init.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/init.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAEL,WAAW,EAGX,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,aAAa,EACd,MAAM,UAAU,CAAC;AAClB,OAAO,EAIL,eAAe,EAEf,KAAK,iBAAiB,EACvB,MAAM,SAAS,CAAC;AAEjB,MAAM,WAAW,gBAAgB;IAC/B,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IAEf,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAEhB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAEvC;;;;OAIG;IACH,OAAO,CAAC,EAAE,aAAa,CAAC;IAExB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,8FAA8F;IAC9F,SAAS,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAErC,qFAAqF;IACrF,iBAAiB,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IAE7D,iGAAiG;IACjG,gBAAgB,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,CAAC;IAE7C,wDAAwD;IACxD,sBAAsB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,qBAAqB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAE1C,qEAAqE;IACrE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,qBAAqB,CAAC,EAAE,CAAC,gBAAgB,EAAE,MAAM,EAAE,SAAS,EAAE,eAAe,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE7G,6FAA6F;IAC7F,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,qDAAqD;IACrD,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,yEAAyE;IACzE,QAAQ,CAAC,EAAE;QAAE,gBAAgB,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,KAAK;YAAE,MAAM,EAAE,MAAM,IAAI,CAAA;SAAE,CAAA;KAAE,CAAC;IAE9G,qFAAqF;IACrF,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IAE7C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AAID,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;IACzC,MAAM,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;IAC3C,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,EAAE,IAAI,KAAK,IAAI,CAAC;IAChF,KAAK,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7B,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,CAAC;IACxE,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,mBAAmB,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAChD,cAAc,EAAE,MAAM,MAAM,CAAC;IAC7B,MAAM,EAAE,MAAM,WAAW,CAAC;IAC1B,MAAM,EAAE;QACN,OAAO,EAAE,OAAO,gBAAgB,CAAC;QACjC,MAAM,EAAE,OAAO,eAAe,CAAC;QAC/B,MAAM,EAAE,OAAO,eAAe,CAAC;QAC/B,IAAI,EAAE,OAAO,aAAa,CAAC;QAC3B,OAAO,EAAE,OAAO,WAAW,CAAC;KAC7B,CAAC;IACF,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpD;AAKD,sEAAsE;AACtE,wBAAgB,SAAS,CAAC,IAAI,EAAE,gBAAgB,GAAG,WAAW,CAmI7D;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,IAAI,CAAC,gBAAgB,EAAE,SAAS,CAAC,GAAG;IAAE,OAAO,EAAE,iBAAiB,CAAA;CAAE,GACvE,OAAO,CAAC,WAAW,CAAC,CAkBtB"}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* lost is a row missing from an average, and a request that blocks the app is a bug
|
|
20
20
|
* everybody sees.
|
|
21
21
|
*/
|
|
22
|
-
import type { ConfigContext } from './config';
|
|
22
|
+
import type { ConfigContext, ConfigStorage } from './config';
|
|
23
23
|
export interface TrackOptions {
|
|
24
24
|
/**
|
|
25
25
|
* The config url this app already uses (…/pulse/config/{slug}) or the track url
|
|
@@ -36,8 +36,29 @@ export interface TrackOptions {
|
|
|
36
36
|
maxBatch?: number;
|
|
37
37
|
/** Events held before the oldest are dropped. Default 500. */
|
|
38
38
|
maxQueue?: number;
|
|
39
|
+
/** Persists the outbox across process death. Use the same storage passed to initPulse. */
|
|
40
|
+
storage?: ConfigStorage;
|
|
41
|
+
/** App-scoped persistence key. initPulse supplies one automatically. */
|
|
42
|
+
storageKey?: string;
|
|
43
|
+
/** Per-request timeout. Default 10s. */
|
|
44
|
+
timeoutMs?: number;
|
|
45
|
+
/** Maximum delivery attempts before an event is dropped. Default 10. */
|
|
46
|
+
maxAttempts?: number;
|
|
47
|
+
/** Return false until analytics consent exists. No event is retained before consent. */
|
|
48
|
+
hasConsent?: () => boolean;
|
|
49
|
+
/** Only these property keys may leave the process. */
|
|
50
|
+
propertyAllowlist?: readonly string[];
|
|
51
|
+
/** These allowed properties are replaced with "[REDACTED]". */
|
|
52
|
+
redactProperties?: readonly string[];
|
|
53
|
+
/** Foreground/background lifecycle used for a best-effort final flush. */
|
|
54
|
+
appState?: {
|
|
55
|
+
addEventListener: (type: 'change', handler: (state: string) => void) => {
|
|
56
|
+
remove: () => void;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
39
59
|
/** Called with the event keys the server did not recognise. */
|
|
40
60
|
onIgnored?: (events: string[]) => void;
|
|
61
|
+
onDropped?: (count: number, reason: 'queue-full' | 'max-attempts' | 'no-consent') => void;
|
|
41
62
|
onError?: (error: unknown) => void;
|
|
42
63
|
}
|
|
43
64
|
export interface TrackedEventInput {
|
|
@@ -62,5 +83,12 @@ export declare function track(event: string, props?: TrackedEventInput['props'],
|
|
|
62
83
|
export declare function flushEvents(): Promise<number>;
|
|
63
84
|
/** How many events are waiting — for a caller that wants to flush before backgrounding. */
|
|
64
85
|
export declare function pendingEventCount(): number;
|
|
86
|
+
export declare function getTrackingInfo(): {
|
|
87
|
+
queueDepth: number;
|
|
88
|
+
sending: boolean;
|
|
89
|
+
nextAttemptAt: number;
|
|
90
|
+
};
|
|
91
|
+
/** Stop timers and listeners while preserving the durable outbox. */
|
|
92
|
+
export declare function disposeTracking(): void;
|
|
65
93
|
export declare function stopTracking(): void;
|
|
66
94
|
//# sourceMappingURL=track.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"track.d.ts","sourceRoot":"","sources":["../../src/track.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"track.d.ts","sourceRoot":"","sources":["../../src/track.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE7D,MAAM,WAAW,YAAY;IAC3B;;;;;OAKG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,aAAa,CAAC;IAEjC,wDAAwD;IACxD,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,0FAA0F;IAC1F,OAAO,CAAC,EAAE,aAAa,CAAC;IAExB,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,wFAAwF;IACxF,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC;IAE3B,sDAAsD;IACtD,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAEtC,+DAA+D;IAC/D,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAErC,0EAA0E;IAC1E,QAAQ,CAAC,EAAE;QAAE,gBAAgB,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,KAAK;YAAE,MAAM,EAAE,MAAM,IAAI,CAAA;SAAE,CAAA;KAAE,CAAC;IAE9G,+DAA+D;IAC/D,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IAEvC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,cAAc,GAAG,YAAY,KAAK,IAAI,CAAC;IAE1F,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACrE,IAAI,CAAC,EAAE,IAAI,CAAC;CACb;AAiCD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAS1D;AAED;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CA2B1F;AAED;;;;;;GAMG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC,CA8DnD;AAED,2FAA2F;AAC3F,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAgB,eAAe,IAAI;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACvB,CAEA;AAED,qEAAqE;AACrE,wBAAgB,eAAe,IAAI,IAAI,CAStC;AAED,wBAAgB,YAAY,IAAI,IAAI,CAMnC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usePulseUpdates.d.ts","sourceRoot":"","sources":["../../src/usePulseUpdates.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEnF,MAAM,WAAW,qBAAqB;IAEpC,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAC;IAC/B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAGrB,eAAe,EAAE,aAAa,GAAG,IAAI,CAAC;IACtC,gBAAgB,EAAE,aAAa,GAAG,IAAI,CAAC;IACvC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAGpB,cAAc,EAAE,MAAM,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACjD,cAAc,EAAE,MAAM,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACjD,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B;AAED,wBAAgB,eAAe,IAAI,qBAAqB,
|
|
1
|
+
{"version":3,"file":"usePulseUpdates.d.ts","sourceRoot":"","sources":["../../src/usePulseUpdates.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEnF,MAAM,WAAW,qBAAqB;IAEpC,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAC;IAC/B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAGrB,eAAe,EAAE,aAAa,GAAG,IAAI,CAAC;IACtC,gBAAgB,EAAE,aAAa,GAAG,IAAI,CAAC;IACvC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAGpB,cAAc,EAAE,MAAM,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACjD,cAAc,EAAE,MAAM,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACjD,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B;AAED,wBAAgB,eAAe,IAAI,qBAAqB,CA+GvD"}
|
package/logo.png
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pulse-updates",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "OTA updates for React Native - lightweight alternative to expo-updates",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
"android/src",
|
|
18
18
|
"android/build.gradle",
|
|
19
19
|
"scripts",
|
|
20
|
+
"app.plugin.js",
|
|
21
|
+
"SECURITY.md",
|
|
22
|
+
"logo.png",
|
|
20
23
|
"pulse-updates.podspec",
|
|
21
24
|
"react-native.config.js",
|
|
22
25
|
"!**/__tests__"
|
|
@@ -44,13 +47,21 @@
|
|
|
44
47
|
"author": "Pulse Updates",
|
|
45
48
|
"license": "MIT",
|
|
46
49
|
"peerDependencies": {
|
|
50
|
+
"@expo/config-plugins": ">=8.0.0",
|
|
47
51
|
"react": ">=18.2.0",
|
|
48
52
|
"react-native": ">=0.76.0"
|
|
49
53
|
},
|
|
54
|
+
"peerDependenciesMeta": {
|
|
55
|
+
"@expo/config-plugins": { "optional": true }
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"tweetnacl": "1.0.3"
|
|
59
|
+
},
|
|
50
60
|
"devDependencies": {
|
|
51
61
|
"@types/react": "^18.2.0",
|
|
62
|
+
"@types/react-native": "^0.72.0",
|
|
52
63
|
"react": "^18.2.0",
|
|
53
|
-
"react-native": "^0.
|
|
64
|
+
"react-native": "^0.73.0",
|
|
54
65
|
"react-native-builder-bob": "^0.23.0",
|
|
55
66
|
"typescript": "^5.0.0"
|
|
56
67
|
},
|
|
@@ -68,4 +79,4 @@
|
|
|
68
79
|
]
|
|
69
80
|
]
|
|
70
81
|
}
|
|
71
|
-
}
|
|
82
|
+
}
|
package/scripts/publish.mjs
CHANGED
|
@@ -1086,10 +1086,19 @@ async function publish(options) {
|
|
|
1086
1086
|
*/
|
|
1087
1087
|
async function registerCapabilities(options) {
|
|
1088
1088
|
const config = loadConfig(options);
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
throw new Error('Provide --bundle <path to the embedded JS bundle> (the bundle built into the app binary).');
|
|
1089
|
+
if (!config.platform || !['ios', 'android'].includes(config.platform)) {
|
|
1090
|
+
throw new Error('Platform is required. Pass --platform ios or --platform android.');
|
|
1092
1091
|
}
|
|
1092
|
+
if (!config.runtimeVersion) {
|
|
1093
|
+
throw new Error('Runtime version is required. Pass --runtime-version or configure it in the native project.');
|
|
1094
|
+
}
|
|
1095
|
+
if (!config.apiUrl) {
|
|
1096
|
+
throw new Error('API URL is required. Set --api-url, PULSE_API_URL, or the native PulseUpdatesURL.');
|
|
1097
|
+
}
|
|
1098
|
+
if (!config.apiKey) {
|
|
1099
|
+
throw new Error('API key is required. Set --api-key, PULSE_API_KEY, or pulse.config.json.');
|
|
1100
|
+
}
|
|
1101
|
+
const bundlePath = resolveCapabilitiesBundle(options, config);
|
|
1093
1102
|
const nativeModules = extractNativeModules(bundlePath);
|
|
1094
1103
|
log(`Registering ${nativeModules.length} native module(s) for ${config.runtimeVersion}/${config.platform}`);
|
|
1095
1104
|
const res = await fetch(`${config.apiUrl}/api/capabilities`, {
|
|
@@ -1107,6 +1116,27 @@ async function registerCapabilities(options) {
|
|
|
1107
1116
|
logSuccess(`Capabilities registered (${nativeModules.length} native modules)`);
|
|
1108
1117
|
}
|
|
1109
1118
|
|
|
1119
|
+
/**
|
|
1120
|
+
* Resolve the embedded bundle used for the capability snapshot. A release pipeline
|
|
1121
|
+
* can pass the exact bundle it just put in the binary; local and CI adoption can use
|
|
1122
|
+
* the zero-config path, which builds the same production Metro graph first.
|
|
1123
|
+
*/
|
|
1124
|
+
export function resolveCapabilitiesBundle(options, config, bundleFactory = createBundle) {
|
|
1125
|
+
const explicit = options.bundle || options['embedded-bundle'];
|
|
1126
|
+
if (explicit) {
|
|
1127
|
+
const resolved = path.resolve(explicit);
|
|
1128
|
+
if (!fs.existsSync(resolved)) throw new Error(`Capability bundle not found: ${resolved}`);
|
|
1129
|
+
return resolved;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
logStep('1/2', `Building the ${config.platform} production bundle for capability discovery...`);
|
|
1133
|
+
return bundleFactory(
|
|
1134
|
+
config.platform,
|
|
1135
|
+
path.resolve(config.bundleDir || './dist'),
|
|
1136
|
+
config.entryFile || 'index.ts'
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1110
1140
|
|
|
1111
1141
|
/**
|
|
1112
1142
|
* The experiments an app is running, and how their split is holding.
|
|
@@ -1293,6 +1323,34 @@ ${colors.cyan}App${colors.reset} (PUBLIC — set in Info.plist / AndroidManifest
|
|
|
1293
1323
|
`);
|
|
1294
1324
|
}
|
|
1295
1325
|
|
|
1326
|
+
/** Generate small app-local contracts from a reviewable schema file. */
|
|
1327
|
+
export function generateContracts(options) {
|
|
1328
|
+
const schemaPath = path.resolve(options.schema || 'pulse.schema.json');
|
|
1329
|
+
const outputPath = path.resolve(options.output || 'src/pulse.generated.ts');
|
|
1330
|
+
if (!fs.existsSync(schemaPath)) {
|
|
1331
|
+
throw new Error(`Schema not found: ${schemaPath}. Create pulse.schema.json or pass --schema.`);
|
|
1332
|
+
}
|
|
1333
|
+
const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
|
|
1334
|
+
const scalar = (value) => ({ boolean: 'boolean', number: 'number', string: 'string', json: 'unknown' }[value] || 'unknown');
|
|
1335
|
+
const objectType = (shape = {}) => `{ ${Object.entries(shape).map(([key, value]) => `${JSON.stringify(key)}?: ${scalar(value)}`).join('; ')} }`;
|
|
1336
|
+
const configEntries = Object.entries(schema.config || {});
|
|
1337
|
+
const eventEntries = Object.entries(schema.events || {});
|
|
1338
|
+
const actionEntries = Object.entries(schema.actions || {});
|
|
1339
|
+
const output = `/* Generated by pulse-updates generate. Do not edit by hand. */\n` +
|
|
1340
|
+
`import type { TrackedEventInput, DecisionAction } from 'pulse-updates';\n\n` +
|
|
1341
|
+
`export interface PulseConfigSchema {\n${configEntries.map(([key, value]) => ` ${JSON.stringify(key)}: ${scalar(value)};`).join('\n')}\n}\n\n` +
|
|
1342
|
+
`export interface PulseEventSchema {\n${eventEntries.map(([key, value]) => ` ${JSON.stringify(key)}: ${objectType(value)};`).join('\n')}\n}\n\n` +
|
|
1343
|
+
`export type PulseEventKey = keyof PulseEventSchema;\n` +
|
|
1344
|
+
`export type PulseTypedEvent<K extends PulseEventKey> = { event: K; props: PulseEventSchema[K]; time?: Date };\n\n` +
|
|
1345
|
+
`export type PulseAction = ${actionEntries.length > 0
|
|
1346
|
+
? actionEntries.map(([key, value]) => `(DecisionAction & { kind: ${JSON.stringify(key)} } & ${objectType(value)})`).join(' | ')
|
|
1347
|
+
: 'DecisionAction'};\n` +
|
|
1348
|
+
`export type PulseTrack = <K extends PulseEventKey>(event: K, props?: PulseEventSchema[K], time?: TrackedEventInput['time']) => void;\n`;
|
|
1349
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
1350
|
+
fs.writeFileSync(outputPath, output);
|
|
1351
|
+
logSuccess(`Generated ${path.relative(process.cwd(), outputPath)} from ${path.relative(process.cwd(), schemaPath)}`);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1296
1354
|
/**
|
|
1297
1355
|
* Show help
|
|
1298
1356
|
*/
|
|
@@ -1307,6 +1365,7 @@ ${colors.cyan}Commands:${colors.reset}
|
|
|
1307
1365
|
publish Create bundle and publish update to server
|
|
1308
1366
|
keygen Generate an Ed25519 signing keypair (server + app config)
|
|
1309
1367
|
register-capabilities Record the embedded bundle's native capability set (crash-prediction)
|
|
1368
|
+
generate Generate typed config/event/action contracts from pulse.schema.json
|
|
1310
1369
|
|
|
1311
1370
|
${colors.cyan}Options:${colors.reset}
|
|
1312
1371
|
--api-url <url> API server URL (auto-detected from native config)
|
|
@@ -1315,6 +1374,7 @@ ${colors.cyan}Options:${colors.reset}
|
|
|
1315
1374
|
--runtime-version <v> Runtime version (auto-detected from native config)
|
|
1316
1375
|
--channel <channel> Release channel (default: production)
|
|
1317
1376
|
--bundle-dir <dir> Output directory (default: ./dist)
|
|
1377
|
+
--bundle <file> Embedded bundle for register-capabilities (otherwise built automatically)
|
|
1318
1378
|
--entry-file <file> Entry file (default: index.ts)
|
|
1319
1379
|
--skip-bundle Skip bundle creation (use existing)
|
|
1320
1380
|
--dry-run Build + validate locally without creating a release on the server
|
|
@@ -1326,6 +1386,8 @@ ${colors.cyan}Options:${colors.reset}
|
|
|
1326
1386
|
--build <number> Build number (shown in version, e.g., 1.0.0.42)
|
|
1327
1387
|
--message <msg> Release message/notes
|
|
1328
1388
|
--key-id <id> Key id for keygen (default: auto-generated)
|
|
1389
|
+
--schema <file> Contract schema for generate (default: pulse.schema.json)
|
|
1390
|
+
--output <file> Generated TypeScript file (default: src/pulse.generated.ts)
|
|
1329
1391
|
|
|
1330
1392
|
${colors.cyan}Experiments:${colors.reset}
|
|
1331
1393
|
pulse-updates experiments List them, with the split each one got
|
|
@@ -1388,6 +1450,9 @@ async function main() {
|
|
|
1388
1450
|
case 'keygen':
|
|
1389
1451
|
keygen(options);
|
|
1390
1452
|
break;
|
|
1453
|
+
case 'generate':
|
|
1454
|
+
generateContracts(options);
|
|
1455
|
+
break;
|
|
1391
1456
|
case 'help':
|
|
1392
1457
|
case '--help':
|
|
1393
1458
|
case '-h':
|
|
@@ -43,6 +43,7 @@ export interface Spec extends TurboModule {
|
|
|
43
43
|
|
|
44
44
|
markAppReady(): Promise<void>;
|
|
45
45
|
reportLaunchFailure(reason: string): Promise<void>;
|
|
46
|
+
verifySignatureAsync(canonicalPayload: string, alg: string, keyId: string, signature: string): Promise<boolean>;
|
|
46
47
|
|
|
47
48
|
// Event emitter
|
|
48
49
|
addListener(eventName: string): void;
|
package/src/PulseUpdates.ts
CHANGED
|
@@ -16,6 +16,42 @@ let _isEmbeddedLaunch = true;
|
|
|
16
16
|
let _manifest: PulseManifest | null = null;
|
|
17
17
|
let _createdAt: Date | null = null;
|
|
18
18
|
let _localAssets: Record<string, string> | null = null;
|
|
19
|
+
const stateListeners = new Set<() => void>();
|
|
20
|
+
|
|
21
|
+
export interface PulseNativeState {
|
|
22
|
+
isEnabled: boolean;
|
|
23
|
+
updateId: string | null;
|
|
24
|
+
runtimeVersion: string | null;
|
|
25
|
+
channel: string | null;
|
|
26
|
+
isEmbeddedLaunch: boolean;
|
|
27
|
+
manifest: PulseManifest | null;
|
|
28
|
+
createdAt: Date | null;
|
|
29
|
+
localAssets: Record<string, string> | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A stable read surface for React and for diagnostics. */
|
|
33
|
+
export function getUpdatesState(): PulseNativeState {
|
|
34
|
+
return {
|
|
35
|
+
isEnabled: _isEnabled,
|
|
36
|
+
updateId: _updateId,
|
|
37
|
+
runtimeVersion: _runtimeVersion,
|
|
38
|
+
channel: _channel,
|
|
39
|
+
isEmbeddedLaunch: _isEmbeddedLaunch,
|
|
40
|
+
manifest: _manifest,
|
|
41
|
+
createdAt: _createdAt,
|
|
42
|
+
localAssets: _localAssets,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Subscribe to native-state refreshes without depending on mutable ES exports. */
|
|
47
|
+
export function onUpdatesStateChange(listener: () => void): () => void {
|
|
48
|
+
stateListeners.add(listener);
|
|
49
|
+
return () => stateListeners.delete(listener);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function notifyStateListeners(): void {
|
|
53
|
+
for (const listener of stateListeners) listener();
|
|
54
|
+
}
|
|
19
55
|
|
|
20
56
|
/**
|
|
21
57
|
* Configure pulse-updates with the given options.
|
|
@@ -51,6 +87,9 @@ export function configure(config: PulseUpdatesConfig): void {
|
|
|
51
87
|
_isConfigured = true;
|
|
52
88
|
_isEnabled = config.enabled;
|
|
53
89
|
_runtimeVersion = config.runtimeVersion;
|
|
90
|
+
isEnabled = _isEnabled;
|
|
91
|
+
runtimeVersion = _runtimeVersion;
|
|
92
|
+
notifyStateListeners();
|
|
54
93
|
}
|
|
55
94
|
|
|
56
95
|
/**
|
|
@@ -133,6 +172,7 @@ export async function refreshStateAsync(): Promise<void> {
|
|
|
133
172
|
// Read localAssets map for asset resolution
|
|
134
173
|
_localAssets = state.localAssets ?? null;
|
|
135
174
|
localAssets = _localAssets;
|
|
175
|
+
notifyStateListeners();
|
|
136
176
|
} catch (error) {
|
|
137
177
|
console.warn('[PulseUpdates] Failed to refresh state:', error);
|
|
138
178
|
// Keep current values on error
|
|
@@ -241,6 +281,20 @@ export async function reportLaunchFailure(reason: string): Promise<void> {
|
|
|
241
281
|
await PulseUpdatesModule.reportLaunchFailure(reason);
|
|
242
282
|
}
|
|
243
283
|
|
|
284
|
+
/** Verify config/decision payloads with the public key already pinned in native config. */
|
|
285
|
+
export async function verifyConfigSignatureAsync(
|
|
286
|
+
canonicalPayload: string,
|
|
287
|
+
signature: { alg: string; keyId: string; sig: string },
|
|
288
|
+
): Promise<boolean> {
|
|
289
|
+
if (!PulseUpdatesModule?.verifySignatureAsync) return false;
|
|
290
|
+
return PulseUpdatesModule.verifySignatureAsync(
|
|
291
|
+
canonicalPayload,
|
|
292
|
+
signature.alg,
|
|
293
|
+
signature.keyId,
|
|
294
|
+
signature.sig,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
244
298
|
/**
|
|
245
299
|
* Get the build number from the current update's metadata.
|
|
246
300
|
*/
|