@clipboard-health/notifications 0.4.1 → 0.5.1

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/package.json CHANGED
@@ -1,16 +1,17 @@
1
1
  {
2
2
  "name": "@clipboard-health/notifications",
3
3
  "description": "Send notifications through third-party providers.",
4
- "version": "0.4.1",
4
+ "version": "0.5.1",
5
5
  "bugs": "https://github.com/ClipboardHealth/core-utils/issues",
6
6
  "dependencies": {
7
- "@clipboard-health/phone-number": "0.3.0",
8
- "@clipboard-health/util-ts": "3.12.2",
7
+ "@clipboard-health/phone-number": "0.4.1",
8
+ "@clipboard-health/util-ts": "3.13.1",
9
9
  "@knocklabs/node": "1.16.0",
10
+ "fast-json-stable-stringify": "2.1.0",
10
11
  "tslib": "2.8.1"
11
12
  },
12
13
  "devDependencies": {
13
- "@clipboard-health/testing-core": "0.21.2"
14
+ "@clipboard-health/testing-core": "0.22.1"
14
15
  },
15
16
  "keywords": [],
16
17
  "license": "MIT",
@@ -1,15 +1,17 @@
1
+ export declare const MAX_IDEMPOTENCY_KEY_LENGTH = 255;
1
2
  /**
2
3
  * Creates a deterministic hash for use as an idempotency key.
3
4
  *
4
- * The function sorts `valuesToHash`, generates a SHA-256 hash, prepends the workflow key, and
5
- * truncates the result to 255 characters maximum.
5
+ * The function normalizes `value` (using a stable JSON.stringify for non-string values), generates
6
+ * a SHA-256 hash, prepends the workflow key, and truncates the result to MAX_IDEMPOTENCY_KEY_LENGTH
7
+ * maximum.
6
8
  *
7
9
  * @param params.key - Workflow key to prepend to the hash.
8
- * @param params.valuesToHash - Array of strings to hash.
10
+ * @param params.value - Value to hash (string, string[], object, etc.).
9
11
  *
10
12
  * @returns A hash string prefixed with the workflow key.
11
13
  */
12
14
  export declare function createIdempotencyKey(params: {
13
15
  key: string;
14
- valuesToHash: string[];
16
+ value: unknown;
15
17
  }): string;
@@ -1,26 +1,28 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_IDEMPOTENCY_KEY_LENGTH = void 0;
3
4
  exports.createIdempotencyKey = createIdempotencyKey;
5
+ const tslib_1 = require("tslib");
4
6
  const node_crypto_1 = require("node:crypto");
5
- const MAX_IDEMPOTENCY_KEY_LENGTH = 255;
7
+ const fast_json_stable_stringify_1 = tslib_1.__importDefault(require("fast-json-stable-stringify"));
8
+ exports.MAX_IDEMPOTENCY_KEY_LENGTH = 255;
6
9
  /**
7
10
  * Creates a deterministic hash for use as an idempotency key.
8
11
  *
9
- * The function sorts `valuesToHash`, generates a SHA-256 hash, prepends the workflow key, and
10
- * truncates the result to 255 characters maximum.
12
+ * The function normalizes `value` (using a stable JSON.stringify for non-string values), generates
13
+ * a SHA-256 hash, prepends the workflow key, and truncates the result to MAX_IDEMPOTENCY_KEY_LENGTH
14
+ * maximum.
11
15
  *
12
16
  * @param params.key - Workflow key to prepend to the hash.
13
- * @param params.valuesToHash - Array of strings to hash.
17
+ * @param params.value - Value to hash (string, string[], object, etc.).
14
18
  *
15
19
  * @returns A hash string prefixed with the workflow key.
16
20
  */
17
21
  function createIdempotencyKey(params) {
18
- const { key, valuesToHash } = params;
22
+ const { key, value } = params;
19
23
  const hash = (0, node_crypto_1.createHash)("sha256")
20
- // Unicode code-points for deterministic, locale-independent sorting; don't mutate input array.
21
- .update(JSON.stringify([...valuesToHash].sort()))
24
+ .update(typeof value === "string" ? value : (0, fast_json_stable_stringify_1.default)(value))
22
25
  .digest("hex");
23
- const result = `${key}${hash}`;
24
- return result.slice(0, MAX_IDEMPOTENCY_KEY_LENGTH);
26
+ return `${key}:${hash}`.slice(0, exports.MAX_IDEMPOTENCY_KEY_LENGTH);
25
27
  }
26
28
  //# sourceMappingURL=createIdempotencyKey.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"createIdempotencyKey.js","sourceRoot":"","sources":["../../../../../packages/notifications/src/lib/createIdempotencyKey.ts"],"names":[],"mappings":";;AAeA,oDAUC;AAzBD,6CAAyC;AAEzC,MAAM,0BAA0B,GAAG,GAAG,CAAC;AAEvC;;;;;;;;;;GAUG;AACH,SAAgB,oBAAoB,CAAC,MAA+C;IAClF,MAAM,EAAE,GAAG,EAAE,YAAY,EAAE,GAAG,MAAM,CAAC;IAErC,MAAM,IAAI,GAAG,IAAA,wBAAU,EAAC,QAAQ,CAAC;QAC/B,+FAA+F;SAC9F,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;SAChD,MAAM,CAAC,KAAK,CAAC,CAAC;IAEjB,MAAM,MAAM,GAAG,GAAG,GAAG,GAAG,IAAI,EAAE,CAAC;IAC/B,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,0BAA0B,CAAC,CAAC;AACrD,CAAC"}
1
+ {"version":3,"file":"createIdempotencyKey.js","sourceRoot":"","sources":["../../../../../packages/notifications/src/lib/createIdempotencyKey.ts"],"names":[],"mappings":";;;AAkBA,oDAQC;;AA1BD,6CAAyC;AAEzC,oGAAmD;AAEtC,QAAA,0BAA0B,GAAG,GAAG,CAAC;AAE9C;;;;;;;;;;;GAWG;AACH,SAAgB,oBAAoB,CAAC,MAAuC;IAC1E,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,MAAM,CAAC;IAE9B,MAAM,IAAI,GAAG,IAAA,wBAAU,EAAC,QAAQ,CAAC;SAC9B,MAAM,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAA,oCAAS,EAAC,KAAK,CAAC,CAAC;SAC5D,MAAM,CAAC,KAAK,CAAC,CAAC;IAEjB,OAAO,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,kCAA0B,CAAC,CAAC;AAC/D,CAAC"}
@@ -4,8 +4,8 @@ import type { AppendPushTokenRequest, AppendPushTokenResponse, LogParams, Notifi
4
4
  export declare const MAXIMUM_RECIPIENTS_COUNT = 1000;
5
5
  export declare const ERROR_CODES: {
6
6
  readonly expired: "expired";
7
- readonly recipientCountAboveMaximum: "recipientCountAboveMaximum";
8
7
  readonly recipientCountBelowMinimum: "recipientCountBelowMinimum";
8
+ readonly recipientCountAboveMaximum: "recipientCountAboveMaximum";
9
9
  readonly missingSigningKey: "missingSigningKey";
10
10
  readonly unknown: "unknown";
11
11
  };
@@ -30,7 +30,7 @@ export declare class NotificationClient {
30
30
  * Triggers a notification through third-party providers.
31
31
  *
32
32
  * This method handles:
33
- * - Stale notifications prevention through expiration checks.
33
+ * - Stale notifications prevention through expiresAt.
34
34
  * - Logging with sensitive data redaction.
35
35
  * - Distributed tracing with notification metadata.
36
36
  * - Idempotency to prevent duplicate notifications.
@@ -30,8 +30,8 @@ const LOG_PARAMS = {
30
30
  exports.MAXIMUM_RECIPIENTS_COUNT = 1000;
31
31
  exports.ERROR_CODES = {
32
32
  expired: "expired",
33
- recipientCountAboveMaximum: "recipientCountAboveMaximum",
34
33
  recipientCountBelowMinimum: "recipientCountBelowMinimum",
34
+ recipientCountAboveMaximum: "recipientCountAboveMaximum",
35
35
  missingSigningKey: "missingSigningKey",
36
36
  unknown: "unknown",
37
37
  };
@@ -62,7 +62,7 @@ class NotificationClient {
62
62
  * Triggers a notification through third-party providers.
63
63
  *
64
64
  * This method handles:
65
- * - Stale notifications prevention through expiration checks.
65
+ * - Stale notifications prevention through expiresAt.
66
66
  * - Logging with sensitive data redaction.
67
67
  * - Distributed tracing with notification metadata.
68
68
  * - Idempotency to prevent duplicate notifications.
@@ -129,7 +129,12 @@ export interface TriggerRequest {
129
129
  body: TriggerBody;
130
130
  /**
131
131
  * Key to prevent duplicate requests if provider supports it. It's important it is deterministic
132
- * ({@link createIdempotencyKey}) and remains the same across any retry logic.
132
+ * ({@link createIdempotencyKey}) and remains the same across retry logic.
133
+ *
134
+ * If you retry a request with the same idempotency key within 24 hours from the original request,
135
+ * we will return the same response as the original request. Idempotent requests are expected to
136
+ * be identical. To prevent accidental misuse, the client throws an error when incoming parameters
137
+ * don't match those from the original request.
133
138
  *
134
139
  * Ensure your idempotency key doesn't prevent recipients from receiving notifications. For
135
140
  * example, if you use the workflow key and the recipient's ID as the idempotency key, but it's
@@ -139,9 +144,17 @@ export interface TriggerRequest {
139
144
  idempotencyKey: string;
140
145
  /** Array of data keys to redact in logs for privacy. */
141
146
  keysToRedact?: string[];
142
- /** Expiration timestamp after which the request is dropped. */
147
+ /**
148
+ * Expiration timestamp after which the request is dropped. Use this to prevent stale
149
+ * notifications. If, for example, you're notifying about an event that starts in one hour, you
150
+ * might set this to one hour from now.
151
+ *
152
+ * If you're triggering from a background job, don't set this at the call site! Set it when you
153
+ * enqueue the job. Otherwise, it gets updated each time the job retries, will always be in the
154
+ * future, and won't prevent stale notifications.
155
+ */
143
156
  expiresAt: Date;
144
- /** Attempt number for tracking. */
157
+ /** Attempt number for tracing. */
145
158
  attempt: number;
146
159
  }
147
160
  /**