@cqrs-ddd/pipeline-idempotency 0.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 (76) hide show
  1. package/COMMERCIAL_LICENSE.txt +34 -0
  2. package/LICENSE +661 -0
  3. package/README.md +39 -0
  4. package/dist/constants/tokens.d.ts +3 -0
  5. package/dist/constants/tokens.d.ts.map +1 -0
  6. package/dist/constants/tokens.js +4 -0
  7. package/dist/constants/tokens.js.map +1 -0
  8. package/dist/errors/idempotency-completion.error.d.ts +32 -0
  9. package/dist/errors/idempotency-completion.error.d.ts.map +1 -0
  10. package/dist/errors/idempotency-completion.error.js +33 -0
  11. package/dist/errors/idempotency-completion.error.js.map +1 -0
  12. package/dist/errors/idempotency-conflict.error.d.ts +33 -0
  13. package/dist/errors/idempotency-conflict.error.d.ts.map +1 -0
  14. package/dist/errors/idempotency-conflict.error.js +48 -0
  15. package/dist/errors/idempotency-conflict.error.js.map +1 -0
  16. package/dist/errors/missing-partition.error.d.ts +17 -0
  17. package/dist/errors/missing-partition.error.d.ts.map +1 -0
  18. package/dist/errors/missing-partition.error.js +18 -0
  19. package/dist/errors/missing-partition.error.js.map +1 -0
  20. package/dist/helpers/build-attributes.d.ts +47 -0
  21. package/dist/helpers/build-attributes.d.ts.map +1 -0
  22. package/dist/helpers/build-attributes.js +59 -0
  23. package/dist/helpers/build-attributes.js.map +1 -0
  24. package/dist/helpers/fingerprint.d.ts +8 -0
  25. package/dist/helpers/fingerprint.d.ts.map +1 -0
  26. package/dist/helpers/fingerprint.js +13 -0
  27. package/dist/helpers/fingerprint.js.map +1 -0
  28. package/dist/helpers/idempotency.intent.d.ts +31 -0
  29. package/dist/helpers/idempotency.intent.d.ts.map +1 -0
  30. package/dist/helpers/idempotency.intent.js +26 -0
  31. package/dist/helpers/idempotency.intent.js.map +1 -0
  32. package/dist/helpers/json-snapshot.d.ts +6 -0
  33. package/dist/helpers/json-snapshot.d.ts.map +1 -0
  34. package/dist/helpers/json-snapshot.js +23 -0
  35. package/dist/helpers/json-snapshot.js.map +1 -0
  36. package/dist/helpers/partitioned-key.d.ts +96 -0
  37. package/dist/helpers/partitioned-key.d.ts.map +1 -0
  38. package/dist/helpers/partitioned-key.js +36 -0
  39. package/dist/helpers/partitioned-key.js.map +1 -0
  40. package/dist/http.d.ts +20 -0
  41. package/dist/http.d.ts.map +1 -0
  42. package/dist/http.js +26 -0
  43. package/dist/http.js.map +1 -0
  44. package/dist/idempotency.behavior.d.ts +117 -0
  45. package/dist/idempotency.behavior.d.ts.map +1 -0
  46. package/dist/idempotency.behavior.js +332 -0
  47. package/dist/idempotency.behavior.js.map +1 -0
  48. package/dist/index.d.ts +21 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +19 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/interfaces/idempotency-options.interface.d.ts +97 -0
  53. package/dist/interfaces/idempotency-options.interface.d.ts.map +1 -0
  54. package/dist/interfaces/idempotency-options.interface.js +3 -0
  55. package/dist/interfaces/idempotency-options.interface.js.map +1 -0
  56. package/dist/interfaces/idempotency-record.interface.d.ts +52 -0
  57. package/dist/interfaces/idempotency-record.interface.d.ts.map +1 -0
  58. package/dist/interfaces/idempotency-record.interface.js +3 -0
  59. package/dist/interfaces/idempotency-record.interface.js.map +1 -0
  60. package/dist/interfaces/idempotency-store.interface.d.ts +59 -0
  61. package/dist/interfaces/idempotency-store.interface.d.ts.map +1 -0
  62. package/dist/interfaces/idempotency-store.interface.js +3 -0
  63. package/dist/interfaces/idempotency-store.interface.js.map +1 -0
  64. package/dist/stores/memory.store.d.ts +48 -0
  65. package/dist/stores/memory.store.d.ts.map +1 -0
  66. package/dist/stores/memory.store.js +119 -0
  67. package/dist/stores/memory.store.js.map +1 -0
  68. package/dist/stores/postgres.store.d.ts +72 -0
  69. package/dist/stores/postgres.store.d.ts.map +1 -0
  70. package/dist/stores/postgres.store.js +187 -0
  71. package/dist/stores/postgres.store.js.map +1 -0
  72. package/dist/stores/redis.store.d.ts +62 -0
  73. package/dist/stores/redis.store.d.ts.map +1 -0
  74. package/dist/stores/redis.store.js +79 -0
  75. package/dist/stores/redis.store.js.map +1 -0
  76. package/package.json +71 -0
package/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # @cqrs-ddd/pipeline-idempotency
2
+
3
+ Idempotency for `@cqrs-ddd/pipeline`: a command runs once per operation key, a duplicate
4
+ gets the stored response, a key reused with another payload is refused, and with a
5
+ replay scope a response is replayed only to a caller authorized under the same scope.
6
+ Memory, Redis and PostgreSQL stores are included; `toHttpResponse()` from
7
+ `@cqrs-ddd/pipeline-idempotency/http` turns a conflict into a 409 or 422 answer.
8
+
9
+ **Documentation:** [guide](https://aristoteliss.github.io/ddd-cqrs/packages/pipeline-idempotency/) · [API reference](https://aristoteliss.github.io/ddd-cqrs/api/cqrs-ddd/pipeline-idempotency/) · [all packages](https://aristoteliss.github.io/ddd-cqrs/)
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ pnpm add @cqrs-ddd/pipeline-idempotency @cqrs-ddd/pipeline
15
+ ```
16
+
17
+ Requires Node.js 22.12 or later. Published as an ES module; a CommonJS application loads
18
+ it with `require()`.
19
+
20
+ ## Example
21
+
22
+ ```ts
23
+ const pipeline = createPipeline({
24
+ behaviors: [new IdempotencyBehavior(new MemoryIdempotencyStore())],
25
+ });
26
+
27
+ export const charge = pipeline.wrap(
28
+ { name: 'charge', kind: 'command' },
29
+ idempotent({ keyFactory: (context) => (context.request as { key: string }).key }),
30
+ )(async (input: { key: string; amount: number }) => payments.charge(input));
31
+ ```
32
+
33
+ An idempotency key is an operation identity: partition it by tenant and principal, for
34
+ example with `createPartitionedIdempotencyKeyFactory()`.
35
+
36
+ ## License
37
+
38
+ Dual-licensed under **AGPLv3** and a **Commercial License**. See `LICENSE` and
39
+ `COMMERCIAL_LICENSE.txt`.
@@ -0,0 +1,3 @@
1
+ /** Default time-to-live for an idempotency key: 24 hours, in milliseconds. */
2
+ export declare const DEFAULT_IDEMPOTENCY_TTL_MS = 86400000;
3
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/constants/tokens.ts"],"names":[],"mappings":"AAEA,8EAA8E;AAC9E,eAAO,MAAM,0BAA0B,WAAa,CAAC"}
@@ -0,0 +1,4 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ /** Default time-to-live for an idempotency key: 24 hours, in milliseconds. */
3
+ export const DEFAULT_IDEMPOTENCY_TTL_MS = 86_400_000;
4
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/constants/tokens.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,8EAA8E;AAC9E,MAAM,CAAC,MAAM,0BAA0B,GAAG,UAAU,CAAC"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Which post-execution step failed.
3
+ *
4
+ * - `snapshot` — the handler's response could not be serialized into a replayable
5
+ * record (a cycle, a function, an unsupported type).
6
+ * - `store` — the record was serializable but the store rejected the write.
7
+ */
8
+ export type IdempotencyFinalizationPhase = 'snapshot' | 'store';
9
+ /**
10
+ * Raised when the business handler succeeded but the idempotency record could
11
+ * not be finalized.
12
+ *
13
+ * Retrying the request may repeat business side effects even though the caller
14
+ * received an error. Consumers can use {@link executionSucceeded} to distinguish this state from a
15
+ * handler failure and make an explicit retry/reconciliation decision, and
16
+ * {@link phase} to tell an unusable response apart from an unavailable store.
17
+ *
18
+ * In both phases the claim is deliberately retained until its TTL expires. The
19
+ * side effects have already happened, so releasing the key would let the very
20
+ * next retry repeat them immediately. Retention does not prevent all duplicates
21
+ * — it only prevents immediate reentry while the claim is still live.
22
+ */
23
+ export declare class IdempotencyCompletionError extends Error {
24
+ readonly key: string;
25
+ readonly claimId: string;
26
+ readonly cause: unknown;
27
+ readonly phase: IdempotencyFinalizationPhase;
28
+ readonly name = "IdempotencyCompletionError";
29
+ readonly executionSucceeded: true;
30
+ constructor(key: string, claimId: string, cause: unknown, phase?: IdempotencyFinalizationPhase);
31
+ }
32
+ //# sourceMappingURL=idempotency-completion.error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotency-completion.error.d.ts","sourceRoot":"","sources":["../../src/errors/idempotency-completion.error.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,MAAM,4BAA4B,GAAG,UAAU,GAAG,OAAO,CAAC;AAEhE;;;;;;;;;;;;;GAaG;AACH,qBAAa,0BAA2B,SAAQ,KAAK;aAKjC,GAAG,EAAE,MAAM;aACX,OAAO,EAAE,MAAM;aACf,KAAK,EAAE,OAAO;aACd,KAAK,EAAE,4BAA4B;IAPrD,SAAkB,IAAI,gCAAgC;IACtD,QAAQ,CAAC,kBAAkB,OAAiB;IAE5C,YACkB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,OAAO,EACd,KAAK,GAAE,4BAAsC,EAO9D;CACF"}
@@ -0,0 +1,33 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ /**
3
+ * Raised when the business handler succeeded but the idempotency record could
4
+ * not be finalized.
5
+ *
6
+ * Retrying the request may repeat business side effects even though the caller
7
+ * received an error. Consumers can use {@link executionSucceeded} to distinguish this state from a
8
+ * handler failure and make an explicit retry/reconciliation decision, and
9
+ * {@link phase} to tell an unusable response apart from an unavailable store.
10
+ *
11
+ * In both phases the claim is deliberately retained until its TTL expires. The
12
+ * side effects have already happened, so releasing the key would let the very
13
+ * next retry repeat them immediately. Retention does not prevent all duplicates
14
+ * — it only prevents immediate reentry while the claim is still live.
15
+ */
16
+ export class IdempotencyCompletionError extends Error {
17
+ key;
18
+ claimId;
19
+ cause;
20
+ phase;
21
+ name = 'IdempotencyCompletionError';
22
+ executionSucceeded = true;
23
+ constructor(key, claimId, cause, phase = 'store') {
24
+ super(phase === 'snapshot'
25
+ ? `Handler execution succeeded, but its response for idempotency key "${key}" could not be serialized into a replayable record. The claim is retained until it expires so the side effects are not immediately repeated.`
26
+ : `Handler execution succeeded, but the idempotency record for key "${key}" could not be completed.`);
27
+ this.key = key;
28
+ this.claimId = claimId;
29
+ this.cause = cause;
30
+ this.phase = phase;
31
+ }
32
+ }
33
+ //# sourceMappingURL=idempotency-completion.error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotency-completion.error.js","sourceRoot":"","sources":["../../src/errors/idempotency-completion.error.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAWtE;;;;;;;;;;;;;GAaG;AACH,MAAM,OAAO,0BAA2B,SAAQ,KAAK;IAKjC,GAAG;IACH,OAAO;IACP,KAAK;IACL,KAAK;IAPL,IAAI,GAAG,4BAA4B,CAAC;IAC7C,kBAAkB,GAAG,IAAa,CAAC;IAE5C,YACkB,GAAW,EACX,OAAe,EACf,KAAc,EACd,KAAK,GAAiC,OAAO;QAE7D,KAAK,CACH,KAAK,KAAK,UAAU;YAClB,CAAC,CAAC,sEAAsE,GAAG,8IAA8I;YACzN,CAAC,CAAC,oEAAoE,GAAG,2BAA2B,CACvG,CAAC;mBATc,GAAG;uBACH,OAAO;qBACP,KAAK;qBACL,KAAK;IAOvB,CAAC;CACF"}
@@ -0,0 +1,33 @@
1
+ /** Why an idempotent request was rejected. */
2
+ export type IdempotencyConflictReason = 'in_progress' | 'key_reuse' | 'replay_scope';
3
+ /**
4
+ * Thrown by {@link IdempotencyBehavior} when a request cannot be served:
5
+ *
6
+ * - `in_progress` — an identical request is still running (`409 Conflict`);
7
+ * - `key_reuse` — the key was already used with a **different** payload
8
+ * (`422 Unprocessable Entity`);
9
+ * - `replay_scope` — the stored response was authorized under a different scope
10
+ * than the current caller's, or the record predates scope capture
11
+ * (`409 Conflict`). The handler is not re-executed and the record is kept.
12
+ *
13
+ * `toHttpResponse` of `@cqrs-ddd/pipeline-idempotency/http` maps it to its HTTP status.
14
+ */
15
+ export declare class IdempotencyConflictError extends Error {
16
+ /** The idempotency key in conflict. */
17
+ readonly key: string;
18
+ /** The request that was rejected, e.g. `CreateUserCommand`. */
19
+ readonly requestName: string;
20
+ /** Why the request was rejected. */
21
+ readonly reason: IdempotencyConflictReason;
22
+ /**
23
+ * Suggested HTTP status: `409` for `in_progress` and `replay_scope`, `422` for
24
+ * `key_reuse`.
25
+ */
26
+ readonly statusCode: number;
27
+ constructor(params: {
28
+ key: string;
29
+ requestName: string;
30
+ reason: IdempotencyConflictReason;
31
+ });
32
+ }
33
+ //# sourceMappingURL=idempotency-conflict.error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotency-conflict.error.d.ts","sourceRoot":"","sources":["../../src/errors/idempotency-conflict.error.ts"],"names":[],"mappings":"AAEA,8CAA8C;AAC9C,MAAM,MAAM,yBAAyB,GACjC,aAAa,GACb,WAAW,GACX,cAAc,CAAC;AA0BnB;;;;;;;;;;;GAWG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;IACjD,uCAAuC;IACvC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,+DAA+D;IAC/D,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,oCAAoC;IACpC,QAAQ,CAAC,MAAM,EAAE,yBAAyB,CAAC;IAC3C;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAE5B,YAAY,MAAM,EAAE;QAClB,GAAG,EAAE,MAAM,CAAC;QACZ,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,yBAAyB,CAAC;KACnC,EAOA;CACF"}
@@ -0,0 +1,48 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ function conflictMessage(params) {
3
+ switch (params.reason) {
4
+ case 'in_progress':
5
+ return (`A request with idempotency key "${params.key}" is already in ` +
6
+ `progress for ${params.requestName}`);
7
+ case 'replay_scope':
8
+ return (`Idempotency key "${params.key}" holds a ${params.requestName} response ` +
9
+ 'authorized under a different scope than this caller; it cannot be replayed');
10
+ default:
11
+ return (`Idempotency key "${params.key}" was already used for ` +
12
+ `${params.requestName} with a different payload`);
13
+ }
14
+ }
15
+ /**
16
+ * Thrown by {@link IdempotencyBehavior} when a request cannot be served:
17
+ *
18
+ * - `in_progress` — an identical request is still running (`409 Conflict`);
19
+ * - `key_reuse` — the key was already used with a **different** payload
20
+ * (`422 Unprocessable Entity`);
21
+ * - `replay_scope` — the stored response was authorized under a different scope
22
+ * than the current caller's, or the record predates scope capture
23
+ * (`409 Conflict`). The handler is not re-executed and the record is kept.
24
+ *
25
+ * `toHttpResponse` of `@cqrs-ddd/pipeline-idempotency/http` maps it to its HTTP status.
26
+ */
27
+ export class IdempotencyConflictError extends Error {
28
+ /** The idempotency key in conflict. */
29
+ key;
30
+ /** The request that was rejected, e.g. `CreateUserCommand`. */
31
+ requestName;
32
+ /** Why the request was rejected. */
33
+ reason;
34
+ /**
35
+ * Suggested HTTP status: `409` for `in_progress` and `replay_scope`, `422` for
36
+ * `key_reuse`.
37
+ */
38
+ statusCode;
39
+ constructor(params) {
40
+ super(conflictMessage(params));
41
+ this.name = 'IdempotencyConflictError';
42
+ this.key = params.key;
43
+ this.requestName = params.requestName;
44
+ this.reason = params.reason;
45
+ this.statusCode = params.reason === 'key_reuse' ? 422 : 409;
46
+ }
47
+ }
48
+ //# sourceMappingURL=idempotency-conflict.error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotency-conflict.error.js","sourceRoot":"","sources":["../../src/errors/idempotency-conflict.error.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAQtE,SAAS,eAAe,CAAC,MAIxB;IACC,QAAQ,MAAM,CAAC,MAAM,EAAE,CAAC;QACtB,KAAK,aAAa;YAChB,OAAO,CACL,mCAAmC,MAAM,CAAC,GAAG,kBAAkB;gBAC/D,gBAAgB,MAAM,CAAC,WAAW,EAAE,CACrC,CAAC;QACJ,KAAK,cAAc;YACjB,OAAO,CACL,oBAAoB,MAAM,CAAC,GAAG,aAAa,MAAM,CAAC,WAAW,YAAY;gBACzE,4EAA4E,CAC7E,CAAC;QACJ;YACE,OAAO,CACL,oBAAoB,MAAM,CAAC,GAAG,yBAAyB;gBACvD,GAAG,MAAM,CAAC,WAAW,2BAA2B,CACjD,CAAC;IACN,CAAC;AACH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,wBAAyB,SAAQ,KAAK;IACjD,uCAAuC;IAC9B,GAAG,CAAS;IACrB,+DAA+D;IACtD,WAAW,CAAS;IAC7B,oCAAoC;IAC3B,MAAM,CAA4B;IAC3C;;;OAGG;IACM,UAAU,CAAS;IAE5B,YAAY,MAIX;QACC,KAAK,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC;QAC/B,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAC;QACvC,IAAI,CAAC,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC;QACtB,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC;QACtC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QAC5B,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;IAC9D,CAAC;CACF"}
@@ -0,0 +1,17 @@
1
+ import { MissingPartitionError } from '@cqrs-ddd/pipeline';
2
+ /** Which dimension of the idempotency key could not be resolved. */
3
+ export type IdempotencyPartitionDimension = 'tenant' | 'principal' | 'operation';
4
+ /**
5
+ * Raised before a key is claimed when a dimension required by
6
+ * {@link createPartitionedIdempotencyKeyFactory} cannot be resolved.
7
+ *
8
+ * Omitting a missing tenant or principal would
9
+ * merge isolation domains: every caller whose identity could not be resolved
10
+ * would share one deduplication namespace, so one caller's completed operation
11
+ * could be replayed to another, or suppress another's first execution.
12
+ */
13
+ export declare class MissingIdempotencyPartitionError extends MissingPartitionError<IdempotencyPartitionDimension> {
14
+ readonly name = "MissingIdempotencyPartitionError";
15
+ constructor(requestName: string, dimension: IdempotencyPartitionDimension, remedy: string);
16
+ }
17
+ //# sourceMappingURL=missing-partition.error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missing-partition.error.d.ts","sourceRoot":"","sources":["../../src/errors/missing-partition.error.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,oEAAoE;AACpE,MAAM,MAAM,6BAA6B,GACrC,QAAQ,GACR,WAAW,GACX,WAAW,CAAC;AAEhB;;;;;;;;GAQG;AACH,qBAAa,gCAAiC,SAAQ,qBAAqB,CAAC,6BAA6B,CAAC;IACxG,SAAkB,IAAI,sCAAsC;IAE5D,YACE,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,6BAA6B,EACxC,MAAM,EAAE,MAAM,EAGf;CACF"}
@@ -0,0 +1,18 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ import { MissingPartitionError } from '@cqrs-ddd/pipeline';
3
+ /**
4
+ * Raised before a key is claimed when a dimension required by
5
+ * {@link createPartitionedIdempotencyKeyFactory} cannot be resolved.
6
+ *
7
+ * Omitting a missing tenant or principal would
8
+ * merge isolation domains: every caller whose identity could not be resolved
9
+ * would share one deduplication namespace, so one caller's completed operation
10
+ * could be replayed to another, or suppress another's first execution.
11
+ */
12
+ export class MissingIdempotencyPartitionError extends MissingPartitionError {
13
+ name = 'MissingIdempotencyPartitionError';
14
+ constructor(requestName, dimension, remedy) {
15
+ super('Idempotency', requestName, dimension, remedy);
16
+ }
17
+ }
18
+ //# sourceMappingURL=missing-partition.error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missing-partition.error.js","sourceRoot":"","sources":["../../src/errors/missing-partition.error.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAQ3D;;;;;;;;GAQG;AACH,MAAM,OAAO,gCAAiC,SAAQ,qBAAoD;IACtF,IAAI,GAAG,kCAAkC,CAAC;IAE5D,YACE,WAAmB,EACnB,SAAwC,EACxC,MAAc;QAEd,KAAK,CAAC,aAAa,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;IACvD,CAAC;CACF"}
@@ -0,0 +1,47 @@
1
+ import { type IPipelineContext } from '@cqrs-ddd/pipeline';
2
+ /**
3
+ * The idempotency decision of one execution as flat attributes:
4
+ * `idempotency.replayed` once `IdempotencyBehavior` decided whether to replay,
5
+ * and `idempotency.ownership_lost: true` only when the claim was lost while the
6
+ * handler ran. `{}` when the behavior did not run. The key is never included:
7
+ * it carries the tenant, the principal and the client's operation id.
8
+ *
9
+ * The result is plain data and needs no telemetry package: put it on a span, a
10
+ * log line, an audit record or a metric label (bounded values only).
11
+ *
12
+ * @param context - The pipeline execution, read after `IdempotencyBehavior` ran.
13
+ * @returns The attributes; empty when the behavior did not run.
14
+ *
15
+ * @example Span attributes
16
+ * With `AttributesBehavior` of `@cqrs-ddd/pipeline-opentelemetry`:
17
+ * ```ts
18
+ * createPipeline({
19
+ * globalBehaviors: {
20
+ * before: [TraceBehavior, [AttributesBehavior, { factories: [buildIdempotencyAttributes] }]],
21
+ * },
22
+ * });
23
+ * ```
24
+ *
25
+ * @example Audit metadata
26
+ * `audit()` is declared outside `idempotent()`, so the record is built after its decision:
27
+ * ```ts
28
+ * @pipeline.wrap(
29
+ * { kind: 'command' },
30
+ * audit({ action: 'order.create', metadata: buildIdempotencyAttributes }),
31
+ * idempotent({ keyFactory }),
32
+ * )
33
+ * ```
34
+ *
35
+ * @example A log line from a custom behavior, after the chain ran
36
+ * ```ts
37
+ * async handle(context: IPipelineContext, next: NextDelegate) {
38
+ * try {
39
+ * return await next();
40
+ * } finally {
41
+ * this.logger.log({ request: context.requestName, ...buildIdempotencyAttributes(context) });
42
+ * }
43
+ * }
44
+ * ```
45
+ */
46
+ export declare function buildIdempotencyAttributes(context: IPipelineContext): Record<string, boolean>;
47
+ //# sourceMappingURL=build-attributes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-attributes.d.ts","sourceRoot":"","sources":["../../src/helpers/build-attributes.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAC;AAM5B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,gBAAgB,GACxB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAYzB"}
@@ -0,0 +1,59 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ import { getPipelineItem, hasPipelineItem, } from '@cqrs-ddd/pipeline';
3
+ import { IDEMPOTENCY_OWNERSHIP_LOST_ITEM_TOKEN, IDEMPOTENCY_REPLAYED_ITEM_TOKEN, } from '../idempotency.behavior.js';
4
+ /**
5
+ * The idempotency decision of one execution as flat attributes:
6
+ * `idempotency.replayed` once `IdempotencyBehavior` decided whether to replay,
7
+ * and `idempotency.ownership_lost: true` only when the claim was lost while the
8
+ * handler ran. `{}` when the behavior did not run. The key is never included:
9
+ * it carries the tenant, the principal and the client's operation id.
10
+ *
11
+ * The result is plain data and needs no telemetry package: put it on a span, a
12
+ * log line, an audit record or a metric label (bounded values only).
13
+ *
14
+ * @param context - The pipeline execution, read after `IdempotencyBehavior` ran.
15
+ * @returns The attributes; empty when the behavior did not run.
16
+ *
17
+ * @example Span attributes
18
+ * With `AttributesBehavior` of `@cqrs-ddd/pipeline-opentelemetry`:
19
+ * ```ts
20
+ * createPipeline({
21
+ * globalBehaviors: {
22
+ * before: [TraceBehavior, [AttributesBehavior, { factories: [buildIdempotencyAttributes] }]],
23
+ * },
24
+ * });
25
+ * ```
26
+ *
27
+ * @example Audit metadata
28
+ * `audit()` is declared outside `idempotent()`, so the record is built after its decision:
29
+ * ```ts
30
+ * @pipeline.wrap(
31
+ * { kind: 'command' },
32
+ * audit({ action: 'order.create', metadata: buildIdempotencyAttributes }),
33
+ * idempotent({ keyFactory }),
34
+ * )
35
+ * ```
36
+ *
37
+ * @example A log line from a custom behavior, after the chain ran
38
+ * ```ts
39
+ * async handle(context: IPipelineContext, next: NextDelegate) {
40
+ * try {
41
+ * return await next();
42
+ * } finally {
43
+ * this.logger.log({ request: context.requestName, ...buildIdempotencyAttributes(context) });
44
+ * }
45
+ * }
46
+ * ```
47
+ */
48
+ export function buildIdempotencyAttributes(context) {
49
+ const attributes = {};
50
+ if (hasPipelineItem(context, IDEMPOTENCY_REPLAYED_ITEM_TOKEN)) {
51
+ attributes['idempotency.replayed'] =
52
+ getPipelineItem(context, IDEMPOTENCY_REPLAYED_ITEM_TOKEN) === true;
53
+ }
54
+ if (getPipelineItem(context, IDEMPOTENCY_OWNERSHIP_LOST_ITEM_TOKEN) === true) {
55
+ attributes['idempotency.ownership_lost'] = true;
56
+ }
57
+ return attributes;
58
+ }
59
+ //# sourceMappingURL=build-attributes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-attributes.js","sourceRoot":"","sources":["../../src/helpers/build-attributes.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,OAAO,EACL,eAAe,EACf,eAAe,GAEhB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,qCAAqC,EACrC,+BAA+B,GAChC,MAAM,4BAA4B,CAAC;AAEpC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,MAAM,UAAU,0BAA0B,CACxC,OAAyB;IAEzB,MAAM,UAAU,GAA4B,EAAE,CAAC;IAC/C,IAAI,eAAe,CAAC,OAAO,EAAE,+BAA+B,CAAC,EAAE,CAAC;QAC9D,UAAU,CAAC,sBAAsB,CAAC;YAChC,eAAe,CAAC,OAAO,EAAE,+BAA+B,CAAC,KAAK,IAAI,CAAC;IACvE,CAAC;IACD,IACE,eAAe,CAAC,OAAO,EAAE,qCAAqC,CAAC,KAAK,IAAI,EACxE,CAAC;QACD,UAAU,CAAC,4BAA4B,CAAC,GAAG,IAAI,CAAC;IAClD,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Produces a stable SHA-256 hex digest of an acyclic JSON-serializable value,
3
+ * with object keys sorted so semantically-equal payloads hash identically
4
+ * regardless of property order. Used to detect an idempotency key being reused
5
+ * with a different body.
6
+ */
7
+ export declare function fingerprintValue(value: unknown): string;
8
+ //# sourceMappingURL=fingerprint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fingerprint.d.ts","sourceRoot":"","sources":["../../src/helpers/fingerprint.ts"],"names":[],"mappings":"AAKA;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEvD"}
@@ -0,0 +1,13 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ import { createHash } from 'node:crypto';
3
+ import { stableStringify } from '@cqrs-ddd/safe-stringify';
4
+ /**
5
+ * Produces a stable SHA-256 hex digest of an acyclic JSON-serializable value,
6
+ * with object keys sorted so semantically-equal payloads hash identically
7
+ * regardless of property order. Used to detect an idempotency key being reused
8
+ * with a different body.
9
+ */
10
+ export function fingerprintValue(value) {
11
+ return createHash('sha256').update(stableStringify(value)).digest('hex');
12
+ }
13
+ //# sourceMappingURL=fingerprint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fingerprint.js","sourceRoot":"","sources":["../../src/helpers/fingerprint.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAE3D;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC3E,CAAC"}
@@ -0,0 +1,31 @@
1
+ import type { PipelineBehaviorTuple } from '@cqrs-ddd/pipeline';
2
+ import { IdempotencyBehavior } from '../idempotency.behavior.js';
3
+ import type { IdempotencyBehaviorOptions } from '../interfaces/idempotency-options.interface.js';
4
+ export type IdempotencyIntentOptions = Omit<IdempotencyBehaviorOptions, 'keyFactory'> & ({
5
+ keyFactory: NonNullable<IdempotencyBehaviorOptions['keyFactory']>;
6
+ inheritModuleKey?: never;
7
+ } | {
8
+ inheritModuleKey: true;
9
+ keyFactory?: never;
10
+ });
11
+ /**
12
+ * Returns an idempotency behavior entry for `pipeline.wrap()` or `createPipeline({ globalBehaviors })`.
13
+ * @param options A key factory and idempotency options, or `inheritModuleKey: true`
14
+ * when the behavior's constructor defaults supply the key factory. The inheritance marker is not
15
+ * forwarded to the behavior and does not validate module configuration.
16
+ * @returns The behavior class and options tuple.
17
+ * Keys must distinguish operations and any tenant/principal scope that changes
18
+ * the authorized response; a shared key can replay another caller's response.
19
+ * @example
20
+ * ```ts
21
+ * export const createUser = pipeline.wrap(
22
+ * { name: 'createUser', kind: 'command' },
23
+ * idempotent({ keyFactory: createUserKey, ttl: 86_400_000 }),
24
+ * )(users.create);
25
+ *
26
+ * // The key factory comes from the IdempotencyBehavior constructor defaults.
27
+ * const entry = idempotent({ inheritModuleKey: true });
28
+ * ```
29
+ */
30
+ export declare function idempotent(options: IdempotencyIntentOptions): PipelineBehaviorTuple<IdempotencyBehavior, IdempotencyBehaviorOptions>;
31
+ //# sourceMappingURL=idempotency.intent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotency.intent.d.ts","sourceRoot":"","sources":["../../src/helpers/idempotency.intent.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,gDAAgD,CAAC;AAEjG,MAAM,MAAM,wBAAwB,GAAG,IAAI,CACzC,0BAA0B,EAC1B,YAAY,CACb,GACC,CACI;IACE,UAAU,EAAE,WAAW,CAAC,0BAA0B,CAAC,YAAY,CAAC,CAAC,CAAC;IAClE,gBAAgB,CAAC,EAAE,KAAK,CAAC;CAC1B,GACD;IAAE,gBAAgB,EAAE,IAAI,CAAC;IAAC,UAAU,CAAC,EAAE,KAAK,CAAA;CAAE,CACjD,CAAC;AAEJ;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,wBAAwB,GAChC,qBAAqB,CAAC,mBAAmB,EAAE,0BAA0B,CAAC,CAGxE"}
@@ -0,0 +1,26 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ import { IdempotencyBehavior } from '../idempotency.behavior.js';
3
+ /**
4
+ * Returns an idempotency behavior entry for `pipeline.wrap()` or `createPipeline({ globalBehaviors })`.
5
+ * @param options A key factory and idempotency options, or `inheritModuleKey: true`
6
+ * when the behavior's constructor defaults supply the key factory. The inheritance marker is not
7
+ * forwarded to the behavior and does not validate module configuration.
8
+ * @returns The behavior class and options tuple.
9
+ * Keys must distinguish operations and any tenant/principal scope that changes
10
+ * the authorized response; a shared key can replay another caller's response.
11
+ * @example
12
+ * ```ts
13
+ * export const createUser = pipeline.wrap(
14
+ * { name: 'createUser', kind: 'command' },
15
+ * idempotent({ keyFactory: createUserKey, ttl: 86_400_000 }),
16
+ * )(users.create);
17
+ *
18
+ * // The key factory comes from the IdempotencyBehavior constructor defaults.
19
+ * const entry = idempotent({ inheritModuleKey: true });
20
+ * ```
21
+ */
22
+ export function idempotent(options) {
23
+ const { inheritModuleKey: _inheritModuleKey, ...behaviorOptions } = options;
24
+ return [IdempotencyBehavior, behaviorOptions];
25
+ }
26
+ //# sourceMappingURL=idempotency.intent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotency.intent.js","sourceRoot":"","sources":["../../src/helpers/idempotency.intent.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAGtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAejE;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,UAAU,CACxB,OAAiC;IAEjC,MAAM,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,GAAG,eAAe,EAAE,GAAG,OAAO,CAAC;IAC5E,OAAO,CAAC,mBAAmB,EAAE,eAAe,CAAC,CAAC;AAChD,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { IdempotencyRecord, JsonValue } from '../interfaces/idempotency-record.interface.js';
2
+ /** Convert a replay value to the representation shared by durable stores. */
3
+ export declare function toJsonSnapshot(value: unknown): JsonValue | undefined;
4
+ /** Clone a record with the same JSON boundary used by Redis and Postgres. */
5
+ export declare function cloneIdempotencyRecord(record: IdempotencyRecord): IdempotencyRecord;
6
+ //# sourceMappingURL=json-snapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-snapshot.d.ts","sourceRoot":"","sources":["../../src/helpers/json-snapshot.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,iBAAiB,EACjB,SAAS,EACV,MAAM,+CAA+C,CAAC;AAEvD,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAUpE;AAED,6EAA6E;AAC7E,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,iBAAiB,GACxB,iBAAiB,CAQnB"}
@@ -0,0 +1,23 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ import { toStrictJsonValue } from '@cqrs-ddd/safe-stringify';
3
+ /** Convert a replay value to the representation shared by durable stores. */
4
+ export function toJsonSnapshot(value) {
5
+ if (value === undefined)
6
+ return undefined;
7
+ try {
8
+ return toStrictJsonValue(value);
9
+ }
10
+ catch {
11
+ throw new TypeError('Idempotency responses must be acyclic JSON-serializable values.');
12
+ }
13
+ }
14
+ /** Clone a record with the same JSON boundary used by Redis and Postgres. */
15
+ export function cloneIdempotencyRecord(record) {
16
+ try {
17
+ return JSON.parse(JSON.stringify(record));
18
+ }
19
+ catch {
20
+ throw new TypeError('Idempotency records must contain only acyclic JSON-serializable values.');
21
+ }
22
+ }
23
+ //# sourceMappingURL=json-snapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-snapshot.js","sourceRoot":"","sources":["../../src/helpers/json-snapshot.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAM7D,6EAA6E;AAC7E,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAE1C,IAAI,CAAC;QACH,OAAO,iBAAiB,CAAC,KAAK,CAAc,CAAC;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,SAAS,CACjB,iEAAiE,CAClE,CAAC;IACJ,CAAC;AACH,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,sBAAsB,CACpC,MAAyB;IAEzB,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAsB,CAAC;IACjE,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,SAAS,CACjB,yEAAyE,CAC1E,CAAC;IACJ,CAAC;AACH,CAAC"}
@@ -0,0 +1,96 @@
1
+ import { type IPipelineContext, type TenantPartitionOptions } from '@cqrs-ddd/pipeline';
2
+ import type { IdempotencyKeyFactory } from '../interfaces/idempotency-options.interface.js';
3
+ /**
4
+ * Resolves the principal performing an operation.
5
+ *
6
+ * Return one identifier, or several segments when the identity has more than one
7
+ * dimension — for example `['service', clientId]`, so a service and a user that
8
+ * share an id string never share a namespace. Read it from authenticated context,
9
+ * never from a request body field the caller controls.
10
+ */
11
+ export type IdempotencyPrincipalFactory = (context: IPipelineContext) => string | readonly string[] | undefined;
12
+ /**
13
+ * Resolves the identity of one operation within the principal's namespace: a
14
+ * client-supplied `Idempotency-Key`, or a business identifier the operation is
15
+ * deduplicated by.
16
+ */
17
+ export type IdempotencyOperationFactory = (context: IPipelineContext) => string | undefined;
18
+ /** Options for {@link createPartitionedIdempotencyKeyFactory}. */
19
+ export interface PartitionedIdempotencyKeyOptions extends TenantPartitionOptions {
20
+ /** Who performs the operation. Required, and never defaulted. */
21
+ principal: IdempotencyPrincipalFactory;
22
+ /** Which operation this is, within that principal's namespace. */
23
+ operation: IdempotencyOperationFactory;
24
+ /**
25
+ * Name of the operation kind. Defaults to `context.requestName`; pass a stable
26
+ * value when the request class may be renamed without the operation changing.
27
+ */
28
+ action?: string;
29
+ /**
30
+ * Leading namespace segment. Change it only deliberately: a new namespace
31
+ * abandons every claim stored under the old one, so a completed operation can
32
+ * execute again until those records expire.
33
+ */
34
+ version?: string;
35
+ /**
36
+ * Behavior when the operation identity is absent.
37
+ *
38
+ * - `throw` — the request is rejected.
39
+ * - `skip` — no key is produced, so the request runs without deduplication.
40
+ * Use this for an optional client `Idempotency-Key` header.
41
+ *
42
+ * A missing principal always throws: there is no safe namespace to fall back to.
43
+ *
44
+ * @default 'throw'
45
+ */
46
+ onMissingOperation?: 'throw' | 'skip';
47
+ }
48
+ /**
49
+ * Creates an {@link IdempotencyKeyFactory} whose keys partition by tenant,
50
+ * principal and operation, and fail closed when any of them is missing.
51
+ *
52
+ * Segments are escaped and joined with `joinKeySegments` from
53
+ * `@cqrs-ddd/safe-stringify`, so values
54
+ * containing the separator — email addresses, external subject claims, composite
55
+ * ids — cannot make two different operations collide on one key.
56
+ *
57
+ * The key is an operation identity. Do not fold permissions into it: a key that
58
+ * changed with permissions would let the same side effect run again. Bind replay
59
+ * to the caller's authorization with `replayScopeFactory` instead.
60
+ *
61
+ * The produced key is
62
+ * `[version:]<tenantId>:<principal…>:<action>:<operation>`, with the tenant
63
+ * segment omitted when `includeTenant` is `false`.
64
+ *
65
+ * @throws {MissingIdempotencyPartitionError} When the tenant or principal is
66
+ * absent, or the operation is absent and `onMissingOperation` is `'throw'`.
67
+ *
68
+ * @example Per authenticated principal, deduplicated by a business identifier
69
+ * ```ts
70
+ * const createOrderKey = createPartitionedIdempotencyKeyFactory({
71
+ * version: 'v1',
72
+ * action: 'order.create',
73
+ * principal: (ctx) => ['user', ctx.items.get(CURRENT_USER_ID) as string],
74
+ * operation: (ctx) => (ctx.request as CreateOrderCommand).externalRef,
75
+ * });
76
+ *
77
+ * class CreateOrderHandler {
78
+ * @pipeline.wrap({ kind: 'command' }, [IdempotencyBehavior, { keyFactory: createOrderKey }])
79
+ * async handle(command: CreateOrderCommand) {}
80
+ * }
81
+ * ```
82
+ *
83
+ * @example Optional client-supplied Idempotency-Key
84
+ * ```ts
85
+ * createPartitionedIdempotencyKeyFactory({
86
+ * principal: readUserId,
87
+ * operation: (ctx) => (ctx.request as PayCommand).idempotencyKey,
88
+ * onMissingOperation: 'skip',
89
+ * });
90
+ * ```
91
+ */
92
+ export declare function createPartitionedIdempotencyKeyFactory(options: PartitionedIdempotencyKeyOptions & {
93
+ onMissingOperation?: 'throw';
94
+ }): (context: IPipelineContext) => string;
95
+ export declare function createPartitionedIdempotencyKeyFactory(options: PartitionedIdempotencyKeyOptions): IdempotencyKeyFactory;
96
+ //# sourceMappingURL=partitioned-key.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"partitioned-key.d.ts","sourceRoot":"","sources":["../../src/helpers/partitioned-key.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAE5B,MAAM,oBAAoB,CAAC;AAG5B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gDAAgD,CAAC;AAE5F;;;;;;;GAOG;AACH,MAAM,MAAM,2BAA2B,GAAG,CACxC,OAAO,EAAE,gBAAgB,KACtB,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;AAE5C;;;;GAIG;AACH,MAAM,MAAM,2BAA2B,GAAG,CACxC,OAAO,EAAE,gBAAgB,KACtB,MAAM,GAAG,SAAS,CAAC;AAExB,kEAAkE;AAClE,MAAM,WAAW,gCACf,SAAQ,sBAAsB;IAC9B,iEAAiE;IACjE,SAAS,EAAE,2BAA2B,CAAC;IAEvC,kEAAkE;IAClE,SAAS,EAAE,2BAA2B,CAAC;IAEvC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;;;;;;;;OAUG;IACH,kBAAkB,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CACvC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAgB,sCAAsC,CACpD,OAAO,EAAE,gCAAgC,GAAG;IAAE,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAAE,GAC3E,CAAC,OAAO,EAAE,gBAAgB,KAAK,MAAM,CAAC;AACzC,wBAAgB,sCAAsC,CACpD,OAAO,EAAE,gCAAgC,GACxC,qBAAqB,CAAC"}