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.
Files changed (64) hide show
  1. package/README.md +50 -10
  2. package/SECURITY.md +42 -0
  3. package/android/src/main/java/app/pulse/updates/PulseController.kt +6 -2
  4. package/android/src/main/java/app/pulse/updates/PulseUpdatesModule.kt +25 -0
  5. package/app.plugin.js +47 -0
  6. package/ios/PulseUpdates/PulseController.swift +7 -2
  7. package/ios/PulseUpdates/PulseUpdates.m +7 -0
  8. package/ios/PulseUpdates/PulseUpdates.swift +22 -0
  9. package/lib/commonjs/NativePulseUpdates.js.map +1 -1
  10. package/lib/commonjs/PulseUpdates.js +36 -0
  11. package/lib/commonjs/PulseUpdates.js.map +1 -1
  12. package/lib/commonjs/config.js +175 -21
  13. package/lib/commonjs/config.js.map +1 -1
  14. package/lib/commonjs/decisions.js +128 -0
  15. package/lib/commonjs/decisions.js.map +1 -0
  16. package/lib/commonjs/index.js +12 -0
  17. package/lib/commonjs/index.js.map +1 -1
  18. package/lib/commonjs/init.js +164 -12
  19. package/lib/commonjs/init.js.map +1 -1
  20. package/lib/commonjs/track.js +154 -12
  21. package/lib/commonjs/track.js.map +1 -1
  22. package/lib/commonjs/usePulseUpdates.js +21 -16
  23. package/lib/commonjs/usePulseUpdates.js.map +1 -1
  24. package/lib/module/NativePulseUpdates.js.map +1 -1
  25. package/lib/module/PulseUpdates.js +33 -0
  26. package/lib/module/PulseUpdates.js.map +1 -1
  27. package/lib/module/config.js +174 -21
  28. package/lib/module/config.js.map +1 -1
  29. package/lib/module/decisions.js +122 -0
  30. package/lib/module/decisions.js.map +1 -0
  31. package/lib/module/index.js +1 -0
  32. package/lib/module/index.js.map +1 -1
  33. package/lib/module/init.js +165 -14
  34. package/lib/module/init.js.map +1 -1
  35. package/lib/module/track.js +152 -12
  36. package/lib/module/track.js.map +1 -1
  37. package/lib/module/usePulseUpdates.js +21 -16
  38. package/lib/module/usePulseUpdates.js.map +1 -1
  39. package/lib/typescript/NativePulseUpdates.d.ts +1 -0
  40. package/lib/typescript/NativePulseUpdates.d.ts.map +1 -1
  41. package/lib/typescript/PulseUpdates.d.ts +20 -0
  42. package/lib/typescript/PulseUpdates.d.ts.map +1 -1
  43. package/lib/typescript/config.d.ts +22 -0
  44. package/lib/typescript/config.d.ts.map +1 -1
  45. package/lib/typescript/decisions.d.ts +55 -0
  46. package/lib/typescript/decisions.d.ts.map +1 -0
  47. package/lib/typescript/index.d.ts +1 -0
  48. package/lib/typescript/index.d.ts.map +1 -1
  49. package/lib/typescript/init.d.ts +51 -4
  50. package/lib/typescript/init.d.ts.map +1 -1
  51. package/lib/typescript/track.d.ts +29 -1
  52. package/lib/typescript/track.d.ts.map +1 -1
  53. package/lib/typescript/usePulseUpdates.d.ts.map +1 -1
  54. package/logo.png +0 -0
  55. package/package.json +14 -3
  56. package/scripts/publish.mjs +68 -3
  57. package/src/NativePulseUpdates.ts +1 -0
  58. package/src/PulseUpdates.ts +54 -0
  59. package/src/config.ts +234 -21
  60. package/src/decisions.ts +179 -0
  61. package/src/index.ts +1 -0
  62. package/src/init.ts +240 -11
  63. package/src/track.ts +191 -13
  64. package/src/usePulseUpdates.ts +21 -16
@@ -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
- /** Wires config and events, and returns the context the two share. */
68
- export declare function initPulse(opts: InitPulseOptions): {
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,EAKL,KAAK,aAAa,EAClB,KAAK,WAAW,EACjB,MAAM,UAAU,CAAC;AAGlB,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;;;;;;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,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,sEAAsE;AACtE,wBAAgB,SAAS,CAAC,IAAI,EAAE,gBAAgB,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CA2C3G"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/init.ts"],"names":[],"mappings":"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"}
@@ -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;AAE9C,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,+DAA+D;IAC/D,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IAEvC,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;AAyBD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAG1D;AAED;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAe1F;AAED;;;;;;GAMG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC,CA6CnD;AAED,2FAA2F;AAC3F,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAgB,YAAY,IAAI,IAAI,CAQnC"}
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,CA0GvD"}
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.1.0",
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.83.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
+ }
@@ -1086,10 +1086,19 @@ async function publish(options) {
1086
1086
  */
1087
1087
  async function registerCapabilities(options) {
1088
1088
  const config = loadConfig(options);
1089
- const bundlePath = options['bundle'] || options['embedded-bundle'];
1090
- if (!bundlePath) {
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;
@@ -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
  */