pulse-updates 1.3.10 → 1.5.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 (51) hide show
  1. package/README.md +64 -5
  2. package/lib/commonjs/config.js +25 -0
  3. package/lib/commonjs/config.js.map +1 -1
  4. package/lib/commonjs/decisions.js +20 -6
  5. package/lib/commonjs/decisions.js.map +1 -1
  6. package/lib/commonjs/experiences.js +72 -0
  7. package/lib/commonjs/experiences.js.map +1 -0
  8. package/lib/commonjs/index.js +24 -0
  9. package/lib/commonjs/index.js.map +1 -1
  10. package/lib/commonjs/init.js +63 -17
  11. package/lib/commonjs/init.js.map +1 -1
  12. package/lib/commonjs/links.js +7 -1
  13. package/lib/commonjs/links.js.map +1 -1
  14. package/lib/commonjs/subject.js +44 -0
  15. package/lib/commonjs/subject.js.map +1 -0
  16. package/lib/module/config.js +24 -0
  17. package/lib/module/config.js.map +1 -1
  18. package/lib/module/decisions.js +20 -6
  19. package/lib/module/decisions.js.map +1 -1
  20. package/lib/module/experiences.js +66 -0
  21. package/lib/module/experiences.js.map +1 -0
  22. package/lib/module/index.js +2 -0
  23. package/lib/module/index.js.map +1 -1
  24. package/lib/module/init.js +64 -18
  25. package/lib/module/init.js.map +1 -1
  26. package/lib/module/links.js +7 -1
  27. package/lib/module/links.js.map +1 -1
  28. package/lib/module/subject.js +37 -0
  29. package/lib/module/subject.js.map +1 -0
  30. package/lib/typescript/config.d.ts +2 -0
  31. package/lib/typescript/config.d.ts.map +1 -1
  32. package/lib/typescript/decisions.d.ts +12 -0
  33. package/lib/typescript/decisions.d.ts.map +1 -1
  34. package/lib/typescript/experiences.d.ts +28 -0
  35. package/lib/typescript/experiences.d.ts.map +1 -0
  36. package/lib/typescript/index.d.ts +2 -0
  37. package/lib/typescript/index.d.ts.map +1 -1
  38. package/lib/typescript/init.d.ts +11 -0
  39. package/lib/typescript/init.d.ts.map +1 -1
  40. package/lib/typescript/links.d.ts +7 -16
  41. package/lib/typescript/links.d.ts.map +1 -1
  42. package/lib/typescript/subject.d.ts +12 -0
  43. package/lib/typescript/subject.d.ts.map +1 -0
  44. package/package.json +1 -1
  45. package/src/config.ts +19 -0
  46. package/src/decisions.ts +36 -3
  47. package/src/experiences.ts +72 -0
  48. package/src/index.ts +2 -0
  49. package/src/init.ts +63 -19
  50. package/src/links.ts +14 -19
  51. package/src/subject.ts +47 -0
package/src/links.ts CHANGED
@@ -122,14 +122,6 @@ export interface AnonymousFirstOpenContext {
122
122
  /** JavaScript Date#getTimezoneOffset semantics, bounded to real-world UTC offsets. */
123
123
  timezoneOffsetMinutes?: number;
124
124
  isEmulator?: boolean;
125
- /**
126
- * The address the person is signed in with, when the host app has one. An install that arrives
127
- * with an account does not need to be guessed at: the resolver binds it to the message sent to
128
- * that address and skips probabilistic matching entirely. Omitted for signed-out first opens,
129
- * and never read from the clipboard or any other ambient source — the app passes what it
130
- * already knows, or nothing.
131
- */
132
- accountEmail?: string;
133
125
  }
134
126
 
135
127
  export type AnonymousFirstOpenResult =
@@ -270,16 +262,15 @@ export interface DeferredLinkClientOptions<Action extends string = DeferredLinkA
270
262
  /** Phase-one public campaign exposures are 128-bit hex; account tokens use the wider grammar. */
271
263
  isPublicToken?: (token: string) => boolean;
272
264
  /**
273
- * Whether a first open may name the signed-in account.
265
+ * Where to read the signed-in address, when the integrator wants a first open to name it.
274
266
  *
275
- * Off by default, and deliberately a second switch rather than "the host passed one, so send
276
- * it": an address is the only piece of the first-open request that identifies a person, and an
277
- * integrator adopting Pulse must choose to send it rather than discover later that reading their
278
- * own auth state started forwarding addresses. With it on, an install that arrives signed in is
279
- * bound to the message sent to that address instead of being inferred from device shape and
280
- * timing — the only deterministic signal left where no store referrer survives.
267
+ * Provide it and an install that arrives already signed in is bound to the message sent to that
268
+ * address, instead of being inferred from device shape and timing the only deterministic
269
+ * signal left where no store referrer survives. Leave it out and nothing about the account is
270
+ * ever sent: absent is off, so an integrator cannot enable this by accident, and there is no
271
+ * second switch to keep in sync with it. Read at the moment of the request, never stored.
281
272
  */
282
- sendAccountEmailOnFirstOpen?: boolean;
273
+ getAccountEmail?: () => string | null | undefined;
283
274
  }
284
275
 
285
276
  export interface DeferredLinkClient {
@@ -1627,9 +1618,13 @@ export function createPulseLinkClient<Action extends string = DeferredLinkAction
1627
1618
  return 'retry_scheduled';
1628
1619
  }
1629
1620
 
1630
- const accountEmail = options.sendAccountEmailOnFirstOpen === true
1631
- ? normalizeAccountEmail(context.accountEmail)
1632
- : null;
1621
+ let accountEmail: string | null = null;
1622
+ try {
1623
+ accountEmail = normalizeAccountEmail(options.getAccountEmail?.());
1624
+ } catch {
1625
+ // A host that cannot answer right now simply does not name the account; matching carries on.
1626
+ accountEmail = null;
1627
+ }
1633
1628
  const body = {
1634
1629
  appBundleId: context.appBundleId.trim(),
1635
1630
  platform: 'ios' as const,
package/src/subject.ts ADDED
@@ -0,0 +1,47 @@
1
+ /** Scalar signals supplied by the host app. Never send contact details or credentials. */
2
+ export type PulseAttributeValue = string | number | boolean | Date | null | undefined;
3
+ export interface PulseUserProfile {
4
+ id: string;
5
+ /** Backend account creation time, never installation/update time. */
6
+ registrationDate?: string | Date | null;
7
+ attributes?: Record<string, PulseAttributeValue>;
8
+ }
9
+
10
+ /** Normalize the wire format shared by config and decisions. pulse.* belongs to the server. */
11
+ export function normalizePulseAttributes(
12
+ attributes?: Record<string, PulseAttributeValue>,
13
+ ): Record<string, string> {
14
+ const result: Record<string, string> = {};
15
+ for (const [rawKey, value] of Object.entries(attributes ?? {})) {
16
+ const key = rawKey.trim();
17
+ if (!key || key.length > 128 || key.toLowerCase().startsWith('pulse.') || value == null) continue;
18
+ let scalar: string;
19
+ if (value instanceof Date) {
20
+ if (!Number.isFinite(value.getTime())) continue;
21
+ scalar = value.toISOString();
22
+ } else if (typeof value === 'number') {
23
+ if (!Number.isFinite(value)) continue;
24
+ scalar = String(value);
25
+ } else if (typeof value === 'string' || typeof value === 'boolean') scalar = String(value);
26
+ else continue;
27
+ if (scalar.length > 1024) continue;
28
+ result[key] = scalar;
29
+ if (Object.keys(result).length >= 64) break;
30
+ }
31
+ return result;
32
+ }
33
+
34
+ export function pulseProfileAttributes(profile?: PulseUserProfile): Record<string, string> {
35
+ const attributes = normalizePulseAttributes(profile?.attributes);
36
+ if (profile?.registrationDate != null) {
37
+ // An explicit invalid backend date must not fall back to a conflicting custom value.
38
+ for (const key of Object.keys(attributes)) if (key.toLowerCase() === 'registeredat') delete attributes[key];
39
+ const raw = profile.registrationDate;
40
+ const date = raw instanceof Date ? raw : /^\d{4}-\d{2}-\d{2}(T.*(Z|[+-]\d{2}:\d{2}))?$/i.test(raw) ? new Date(raw) : null;
41
+ if (date && Number.isFinite(date.getTime())) {
42
+ if (Object.keys(attributes).length >= 64) delete attributes[Object.keys(attributes)[63]!];
43
+ attributes.registeredAt = date.toISOString();
44
+ }
45
+ }
46
+ return attributes;
47
+ }