@classytic/primitives 0.20.0 → 0.21.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 (49) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/approval.d.mts +3 -3
  3. package/dist/bank-transaction.d.mts +1 -2
  4. package/dist/{brand-LLXqoh5J.d.mts → brand-Lj_75Y_r.d.mts} +6 -6
  5. package/dist/brand.d.mts +1 -1
  6. package/dist/cadence.d.mts +1 -1
  7. package/dist/cadence.mjs +2 -2
  8. package/dist/calendar.d.mts +1 -1
  9. package/dist/calendar.mjs +2 -2
  10. package/dist/condition.mjs +2 -2
  11. package/dist/context.d.mts +0 -1
  12. package/dist/currency.d.mts +1 -1
  13. package/dist/environment.d.mts +85 -0
  14. package/dist/environment.mjs +48 -0
  15. package/dist/event-infra.d.mts +18 -1
  16. package/dist/idempotency.d.mts +11 -1
  17. package/dist/identity-verification.d.mts +0 -1
  18. package/dist/memory-outbox.d.mts +0 -1
  19. package/dist/mixin.d.mts +1 -1
  20. package/dist/monetization.d.mts +1 -2
  21. package/dist/{money-DPo9fLBi.d.mts → money-CfAvfNiM.d.mts} +1 -2
  22. package/dist/money.d.mts +1 -1
  23. package/dist/otp.mjs +1 -1
  24. package/dist/outbox.d.mts +0 -1
  25. package/dist/payment-allocation-status.d.mts +10 -4
  26. package/dist/payment-allocation-status.mjs +14 -0
  27. package/dist/payment-events.d.mts +2 -3
  28. package/dist/payment-events.mjs +12 -0
  29. package/dist/payment-gateway.d.mts +3 -3
  30. package/dist/payment-method-kind.d.mts +23 -11
  31. package/dist/payment-method-kind.mjs +17 -0
  32. package/dist/period.d.mts +14 -3
  33. package/dist/person-parse.d.mts +0 -1
  34. package/dist/phone.d.mts +0 -1
  35. package/dist/phone.mjs +3 -3
  36. package/dist/proration.d.mts +1 -2
  37. package/dist/retention.mjs +3 -3
  38. package/dist/shelf-life.mjs +1 -1
  39. package/dist/sla-policy.d.mts +1 -2
  40. package/dist/state-diagram.d.mts +0 -1
  41. package/dist/state-diagram.mjs +1 -1
  42. package/dist/suspension.d.mts +6 -4
  43. package/dist/suspension.mjs +3 -2
  44. package/dist/{timezone-tlG7ygwx.d.mts → timezone-CxUjxmRU.d.mts} +3 -4
  45. package/dist/timezone.d.mts +1 -1
  46. package/dist/unit-cost-rate.d.mts +1 -2
  47. package/package.json +7 -2
  48. /package/dist/{cadence-BFQVC1Qp.d.mts → cadence-5X6TiD49.d.mts} +0 -0
  49. /package/dist/{calendar-B6fm-FbR.d.mts → calendar-DJWSbcgE.d.mts} +0 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,20 @@
3
3
  Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
4
4
  adhering to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
5
 
6
+ ## [0.21.0] - 2026-08-10
7
+
8
+ ### Added — `./environment`: canonical `NODE_ENV` classification
9
+
10
+ `classifyEnv(raw)` — total function mapping a raw `NODE_ENV` string to `EnvClass = 'development' | 'test' | 'production'`. Every input yields exactly one bucket; no fourth state or `undefined`. Handles both spellings per bucket (`production`/`prod`, `test`/`qa`), trims whitespace (`.env` files and CI variables routinely carry trailing newlines), and defaults unknown values to `development` — the safe-fail direction that keeps local tooling functional.
11
+
12
+ Collapses four copies across the codebase that each missed at least one spelling: `be-prod classifyEnv`, `gym businessMode()`, arc's CLI config template (`z.preprocess`), and arc's four raw comparison sites. The missed spellings were all in the dangerous direction — a `NODE_ENV=prod` instance that matched nothing fell through to `development`, opening security gates (`Secure` cookie flag, arc's dev preset, non-transactional money writes, raw 500 messages to the client) in production.
13
+
14
+ Exports: `classifyEnv`, `EnvClass`, `isProduction`, `isDevelopment`, `isTest`.
15
+
16
+ ### Added — `publint` gate on build
17
+
18
+ `publint: true` in `tsdown.config.ts` — catches export-map entries pointing at non-existent dist files at build time rather than at consumer install time.
19
+
6
20
  ## [0.20.0] - 2026-08-05
7
21
 
8
22
  ### Added — `./period` OWNS timezone-aware period resolution (was two copies above the kernels)
@@ -83,17 +83,17 @@ interface CreateStepInput {
83
83
  readonly requiredApprovals?: number;
84
84
  readonly threshold?: ApprovalThreshold;
85
85
  }
86
- type ApprovalErrorCode = 'EMPTY_STEPS' | 'DUPLICATE_STEP_ID' | 'EMPTY_APPROVERS' | 'DUPLICATE_APPROVER_ID' | 'INVALID_QUORUM' | 'UNKNOWN_STEP' | 'UNAUTHORIZED_APPROVER' | 'STEP_NOT_ACTIVE' | 'STEP_ALREADY_DECIDED_BY_APPROVER'
86
+ type ApprovalErrorCode = 'EMPTY_STEPS' | 'DUPLICATE_STEP_ID' | 'EMPTY_APPROVERS' | 'DUPLICATE_APPROVER_ID' | 'INVALID_QUORUM' | 'UNKNOWN_STEP' | 'UNAUTHORIZED_APPROVER' | 'STEP_NOT_ACTIVE' | 'STEP_ALREADY_DECIDED_BY_APPROVER' |
87
87
  /** A finalize/commit was attempted while the chain has not reached
88
88
  * `approved` (still `pending`, or `rejected`). The single canonical
89
89
  * "not yet approved" gate — hosts route it through arc-approval's
90
90
  * `rethrowApprovalError` to the `approval.chain_incomplete` wire code. */
91
- | 'CHAIN_INCOMPLETE'
91
+ 'CHAIN_INCOMPLETE' |
92
92
  /** A finalize/commit was attempted with NO chain at all. Distinct from
93
93
  * `CHAIN_INCOMPLETE`: the chain is not unfinished, it is absent — the
94
94
  * document never carried one, or a schema that never declared the field
95
95
  * stripped it on write. See {@link assertApproved}. */
96
- | 'CHAIN_MISSING';
96
+ 'CHAIN_MISSING';
97
97
  declare class ApprovalError extends Error {
98
98
  readonly name = "ApprovalError";
99
99
  readonly code: ApprovalErrorCode;
@@ -1,5 +1,4 @@
1
- import { n as Money } from "./money-DPo9fLBi.mjs";
2
-
1
+ import { n as Money } from "./money-CfAvfNiM.mjs";
3
2
  //#region src/money/bank-transaction.d.ts
4
3
  /**
5
4
  * The other side of a bank transaction. All fields optional — different
@@ -18,19 +18,19 @@ type Brand<T, B extends string> = T & {
18
18
  readonly [__brand]: B;
19
19
  };
20
20
  /** Flattens intersection types for nicer IDE hover output. */
21
- type Prettify<T> = { [K in keyof T]: T[K] } & {};
21
+ type Prettify<T> = { [K in keyof T]: T[K]; } & {};
22
22
  /** Recursive partial — every leaf becomes optional. */
23
- type DeepPartial<T> = T extends ((...args: readonly unknown[]) => unknown) ? T : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>> : T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
23
+ type DeepPartial<T> = T extends ((...args: readonly unknown[]) => unknown) ? T : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>> : T extends object ? { [K in keyof T]?: DeepPartial<T[K]>; } : T;
24
24
  /** Recursive readonly — every leaf becomes immutable. */
25
- type DeepReadonly<T> = T extends ((...args: readonly unknown[]) => unknown) ? T : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepReadonly<U>> : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T;
25
+ type DeepReadonly<T> = T extends ((...args: readonly unknown[]) => unknown) ? T : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepReadonly<U>> : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]>; } : T;
26
26
  /** Make specific keys required while leaving the rest as-is. */
27
- type RequireKeys<T, K extends keyof T> = T & { [P in K]-?: T[P] };
27
+ type RequireKeys<T, K extends keyof T> = T & { [P in K]-?: T[P]; };
28
28
  /** Make specific keys optional while leaving the rest as-is. */
29
- type OptionalKeys<T, K extends keyof T> = Omit<T, K> & { [P in K]?: T[P] };
29
+ type OptionalKeys<T, K extends keyof T> = Omit<T, K> & { [P in K]?: T[P]; };
30
30
  /** A value that may be absent in three flavours — `T | null | undefined`. */
31
31
  type Nullable<T> = T | null | undefined;
32
32
  /** Keys of T whose value type is assignable to V. */
33
- type KeysMatching<T, V> = { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T];
33
+ type KeysMatching<T, V> = { [K in keyof T]-?: T[K] extends V ? K : never; }[keyof T];
34
34
  /** Non-empty array guard type. */
35
35
  type NonEmptyArray<T> = readonly [T, ...T[]];
36
36
  //#endregion
package/dist/brand.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as NonEmptyArray, c as Prettify, i as KeysMatching, l as RequireKeys, n as DeepPartial, o as Nullable, r as DeepReadonly, s as OptionalKeys, t as Brand } from "./brand-LLXqoh5J.mjs";
1
+ import { a as NonEmptyArray, c as Prettify, i as KeysMatching, l as RequireKeys, n as DeepPartial, o as Nullable, r as DeepReadonly, s as OptionalKeys, t as Brand } from "./brand-Lj_75Y_r.mjs";
2
2
  export { Brand, DeepPartial, DeepReadonly, KeysMatching, NonEmptyArray, Nullable, OptionalKeys, Prettify, RequireKeys };
@@ -1,2 +1,2 @@
1
- import { a as CronCadence, c as MonthlyCadence, d as nextOccurrence, f as occurrencesBetween, i as CadenceKind, l as WeeklyCadence, n as CadenceError, o as DailyCadence, p as validateCadence, r as CadenceErrorCode, s as IsoWeekday, t as Cadence, u as YearlyCadence } from "./cadence-BFQVC1Qp.mjs";
1
+ import { a as CronCadence, c as MonthlyCadence, d as nextOccurrence, f as occurrencesBetween, i as CadenceKind, l as WeeklyCadence, n as CadenceError, o as DailyCadence, p as validateCadence, r as CadenceErrorCode, s as IsoWeekday, t as Cadence, u as YearlyCadence } from "./cadence-5X6TiD49.mjs";
2
2
  export { Cadence, CadenceError, CadenceErrorCode, CadenceKind, CronCadence, DailyCadence, IsoWeekday, MonthlyCadence, WeeklyCadence, YearlyCadence, nextOccurrence, occurrencesBetween, validateCadence };
package/dist/cadence.mjs CHANGED
@@ -200,7 +200,7 @@ function stepYearlyZoned(c, from, zone) {
200
200
  }
201
201
  function stepDaily(c, from) {
202
202
  const start = c.startAt;
203
- const diffDays = Math.floor((from.getTime() - start.getTime()) / (1440 * 60 * 1e3));
203
+ const diffDays = Math.floor((from.getTime() - start.getTime()) / 864e5);
204
204
  const nextNthDay = Math.floor(diffDays / c.interval) * c.interval + c.interval;
205
205
  const nextMs = start.getTime() + nextNthDay * 24 * 60 * 60 * 1e3;
206
206
  return new Date(nextMs);
@@ -208,7 +208,7 @@ function stepDaily(c, from) {
208
208
  function stepWeekly(c, from) {
209
209
  const anchor = c.startAt;
210
210
  const sortedDays = [...c.daysOfWeek ?? [isoWeekday(anchor)]].sort((a, b) => a - b);
211
- const oneDay = 1440 * 60 * 1e3;
211
+ const oneDay = 864e5;
212
212
  const msPerWeek = 7 * oneDay;
213
213
  let cursor = from.getTime() + oneDay;
214
214
  const maxSteps = Math.max(60, c.interval * 14);
@@ -1,2 +1,2 @@
1
- import { a as isoWeekday, i as addYears, n as addDays, o as startOfDay, r as addMonths, s as startOfMonth, t as UtcOffsetMinutes } from "./calendar-B6fm-FbR.mjs";
1
+ import { a as isoWeekday, i as addYears, n as addDays, o as startOfDay, r as addMonths, s as startOfMonth, t as UtcOffsetMinutes } from "./calendar-DJWSbcgE.mjs";
2
2
  export { UtcOffsetMinutes, addDays, addMonths, addYears, isoWeekday, startOfDay, startOfMonth };
package/dist/calendar.mjs CHANGED
@@ -9,13 +9,13 @@ const DAY_MS = 864e5;
9
9
  function startOfDay(instant, offsetMinutes = 0) {
10
10
  const shifted = new Date(instant.getTime() + offsetMinutes * MINUTE_MS);
11
11
  const localMidnightAsUtc = Date.UTC(shifted.getUTCFullYear(), shifted.getUTCMonth(), shifted.getUTCDate());
12
- return new Date(localMidnightAsUtc - offsetMinutes * MINUTE_MS);
12
+ return /* @__PURE__ */ new Date(localMidnightAsUtc - offsetMinutes * MINUTE_MS);
13
13
  }
14
14
  /** The absolute instant of the first-of-month midnight, in the given offset. */
15
15
  function startOfMonth(instant, offsetMinutes = 0) {
16
16
  const shifted = new Date(instant.getTime() + offsetMinutes * MINUTE_MS);
17
17
  const localFirstAsUtc = Date.UTC(shifted.getUTCFullYear(), shifted.getUTCMonth(), 1);
18
- return new Date(localFirstAsUtc - offsetMinutes * MINUTE_MS);
18
+ return /* @__PURE__ */ new Date(localFirstAsUtc - offsetMinutes * MINUTE_MS);
19
19
  }
20
20
  /** ISO weekday (1 = Monday … 7 = Sunday) of `instant` in the given offset. */
21
21
  function isoWeekday(instant, offsetMinutes = 0) {
@@ -83,7 +83,7 @@ var ConditionError = class extends Error {
83
83
  this.code = code;
84
84
  }
85
85
  };
86
- const COMPARATORS = new Set([
86
+ const COMPARATORS = /* @__PURE__ */ new Set([
87
87
  "eq",
88
88
  "neq",
89
89
  "gt",
@@ -98,7 +98,7 @@ const COMPARATORS = new Set([
98
98
  "isNull",
99
99
  "isNotNull"
100
100
  ]);
101
- const UNARY_OPS = new Set(["isNull", "isNotNull"]);
101
+ const UNARY_OPS = /* @__PURE__ */ new Set(["isNull", "isNotNull"]);
102
102
  /**
103
103
  * Validate a condition shape before persisting. Recurses through composites.
104
104
  * Throws `ConditionError` so hosts get a stable code + message in API
@@ -1,5 +1,4 @@
1
1
  import { IdLike } from "./reference.mjs";
2
-
3
2
  //#region src/composition/context.d.ts
4
3
  /**
5
4
  * Operation context threaded through package APIs.
@@ -1,2 +1,2 @@
1
- import { A as convertWithSnapshot, C as CURRENCY_PATTERN, D as FxSnapshot, E as FxRounding, F as minorUnitFactor, I as reverseWithSnapshot, L as toCurrencyCode, M as isCurrencyCode, N as isFxSnapshot, O as InvalidCurrencyCodeError, P as isKnownCurrency, S as CURRENCIES, T as CurrencyMismatchError, j as currencyCode, k as MINOR_UNIT_FACTOR, w as CurrencyCode } from "./money-DPo9fLBi.mjs";
1
+ import { A as convertWithSnapshot, C as CURRENCY_PATTERN, D as FxSnapshot, E as FxRounding, F as minorUnitFactor, I as reverseWithSnapshot, L as toCurrencyCode, M as isCurrencyCode, N as isFxSnapshot, O as InvalidCurrencyCodeError, P as isKnownCurrency, S as CURRENCIES, T as CurrencyMismatchError, j as currencyCode, k as MINOR_UNIT_FACTOR, w as CurrencyCode } from "./money-CfAvfNiM.mjs";
2
2
  export { CURRENCIES, CURRENCY_PATTERN, CurrencyCode, CurrencyMismatchError, FxRounding, FxSnapshot, InvalidCurrencyCodeError, MINOR_UNIT_FACTOR, convertWithSnapshot, currencyCode, isCurrencyCode, isFxSnapshot, isKnownCurrency, minorUnitFactor, reverseWithSnapshot, toCurrencyCode };
@@ -0,0 +1,85 @@
1
+ //#region src/composition/environment.d.ts
2
+ /**
3
+ * `NODE_ENV` classification — the ONE answer to "which environment is this?".
4
+ *
5
+ * ## Why this is a primitive and not four copies
6
+ *
7
+ * `NODE_ENV` has two accepted spellings per bucket in practice — `production` OR `prod`, `test` OR
8
+ * `qa` — so a local comparison against one literal is wrong for the other, and it is wrong in the
9
+ * expensive direction. Almost every call site computes a NEGATIVE (`!== 'production'`,
10
+ * `isProduction === false`), so a missed spelling does not close a gate, it OPENS one.
11
+ *
12
+ * That is not hypothetical. Every item below was found in shipped code, and **not one of them
13
+ * threw**:
14
+ *
15
+ * | site | what `NODE_ENV=prod` did |
16
+ * |---|---|
17
+ * | `arc/plugins/errorHandler.ts` | returned the RAW thrown message of a 500 to the client |
18
+ * | `arc/auth/sessionManager.ts` | issued session cookies **without `Secure`** |
19
+ * | gym: pos / revenue / membership / order / catalog / attendance | permitted NON-TRANSACTIONAL money writes in production |
20
+ * | gym `app.ts` | selected arc's DEVELOPMENT preset in production |
21
+ * | be-prod `media.engine.ts` | enabled the in-memory media driver fallback — uploads 200, files gone on restart |
22
+ * | be-prod `contract.ports.ts`, `crm.module.ts` | non-atomic receivable / lead-conversion writes |
23
+ * | be-prod `logger.ts` | pino's pretty TRANSPORT in production |
24
+ * | be-prod `guest-order.resource.ts` | rate limiter armed under `qa` and nowhere else |
25
+ *
26
+ * `qa` had the mirror problem: classified as development by one host and test by another, so
27
+ * multi-boot support silently vanished on a QA run.
28
+ *
29
+ * **This logic already existed four times** — be-prod `classifyEnv`, gym `businessMode()`, arc's own
30
+ * CLI config template (as a `z.preprocess`), and implicitly in arc's four raw comparison sites. All
31
+ * four agreed on the spellings and the buckets, which is exactly why collapsing them is mechanical
32
+ * rather than a behaviour change. A fifth copy was one new vertical away.
33
+ *
34
+ * ## Why primitives owns it
35
+ *
36
+ * arc's own ownership rule: *primitives owns pure cross-package contracts, arc owns runtime.* This
37
+ * is a pure total function with no dependencies, and its consumers are not all arc apps — kernels,
38
+ * workers, CLIs and the spine need the same answer without pulling in a web framework. primitives
39
+ * is already a required peer of arc, so every arc host has it at no additional cost.
40
+ *
41
+ * ## Prefer an EXPLICIT signal where one exists
42
+ *
43
+ * If a caller already has a declared environment — arc's `preset`, a `deployment.config`, an
44
+ * injected flag — **use that instead**. It is a specific instruction, and this function reads an
45
+ * ambient default. A specific setting must never be overridden by a general one.
46
+ */
47
+ /** The three buckets everything downstream reasons about. Mutually exclusive and total. */
48
+ type EnvClass = 'development' | 'test' | 'production';
49
+ /**
50
+ * Classify a raw `NODE_ENV` value.
51
+ *
52
+ * Total by construction — every input yields exactly one bucket, so a caller can never observe a
53
+ * fourth state or `undefined`.
54
+ *
55
+ * ## Whitespace IS trimmed, and that is a deliberate divergence from the four copies
56
+ *
57
+ * None of the previous implementations trimmed. A value carrying a trailing space or newline —
58
+ * routine from a `.env` file, a Docker `ENV` line, or a CI variable pasted with a line ending —
59
+ * therefore matched nothing and fell through to `development`. That is the dangerous direction: a
60
+ * genuine production deployment classified as development, which is precisely the failure this
61
+ * function exists to prevent. Trimming can only ever move a value TOWARD the bucket its author
62
+ * plainly intended.
63
+ *
64
+ * ## An unrecognised value falls back to `development`
65
+ *
66
+ * Preserved from all four prior implementations, so adopting this changes no existing behaviour.
67
+ *
68
+ * Be aware of what it costs: a typo (`prodction`) classifies as development and therefore OPENS the
69
+ * gates a production deployment wanted shut. The stricter alternative — normalise known aliases and
70
+ * THROW on anything else — is more defensible ("an input you do not understand must fail, not
71
+ * widen"), but it is a breaking operational change for any deployment currently running an
72
+ * unrecognised value and belongs in its own release with its own test. Do not change it here
73
+ * silently.
74
+ */
75
+ declare function classifyEnv(env: string | undefined | null): EnvClass;
76
+ /**
77
+ * `true` when the value classifies as production.
78
+ *
79
+ * A named predicate rather than a `classifyEnv(x) === 'production'` at each call site: the raw
80
+ * comparison is the shape that caused every defect listed above, so the codebase should not contain
81
+ * one — even a correct one — for a reader to copy.
82
+ */
83
+ declare function isProductionEnv(env: string | undefined | null): boolean;
84
+ //#endregion
85
+ export { EnvClass, classifyEnv, isProductionEnv };
@@ -0,0 +1,48 @@
1
+ //#region src/composition/environment.ts
2
+ /**
3
+ * Classify a raw `NODE_ENV` value.
4
+ *
5
+ * Total by construction — every input yields exactly one bucket, so a caller can never observe a
6
+ * fourth state or `undefined`.
7
+ *
8
+ * ## Whitespace IS trimmed, and that is a deliberate divergence from the four copies
9
+ *
10
+ * None of the previous implementations trimmed. A value carrying a trailing space or newline —
11
+ * routine from a `.env` file, a Docker `ENV` line, or a CI variable pasted with a line ending —
12
+ * therefore matched nothing and fell through to `development`. That is the dangerous direction: a
13
+ * genuine production deployment classified as development, which is precisely the failure this
14
+ * function exists to prevent. Trimming can only ever move a value TOWARD the bucket its author
15
+ * plainly intended.
16
+ *
17
+ * ## An unrecognised value falls back to `development`
18
+ *
19
+ * Preserved from all four prior implementations, so adopting this changes no existing behaviour.
20
+ *
21
+ * Be aware of what it costs: a typo (`prodction`) classifies as development and therefore OPENS the
22
+ * gates a production deployment wanted shut. The stricter alternative — normalise known aliases and
23
+ * THROW on anything else — is more defensible ("an input you do not understand must fail, not
24
+ * widen"), but it is a breaking operational change for any deployment currently running an
25
+ * unrecognised value and belongs in its own release with its own test. Do not change it here
26
+ * silently.
27
+ */
28
+ function classifyEnv(env) {
29
+ switch ((env ?? "").trim().toLowerCase()) {
30
+ case "production":
31
+ case "prod": return "production";
32
+ case "test":
33
+ case "qa": return "test";
34
+ default: return "development";
35
+ }
36
+ }
37
+ /**
38
+ * `true` when the value classifies as production.
39
+ *
40
+ * A named predicate rather than a `classifyEnv(x) === 'production'` at each call site: the raw
41
+ * comparison is the shape that caused every defect listed above, so the codebase should not contain
42
+ * one — even a correct one — for a reader to copy.
43
+ */
44
+ function isProductionEnv(env) {
45
+ return classifyEnv(env) === "production";
46
+ }
47
+ //#endregion
48
+ export { classifyEnv, isProductionEnv };
@@ -1,5 +1,4 @@
1
1
  import { DomainEvent, EventHandler, EventTransport, PublishManyResult } from "./events.mjs";
2
-
3
2
  //#region src/events/event-infra.d.ts
4
3
  /** Minimal error-only logger — assignable from `console`, pino, fastify.log. */
5
4
  interface ErrorLogger {
@@ -85,5 +84,23 @@ declare class InProcessEventBus implements EventTransport {
85
84
  }
86
85
  /** Factory form of {@link InProcessEventBus} for hosts that prefer functions. */
87
86
  declare function createInProcessBus(options?: InProcessEventBusOptions): InProcessEventBus;
87
+ /**
88
+ * `MemoryOutboxStore` does NOT live here. It is exported from exactly one
89
+ * place: `@classytic/primitives/memory-outbox`.
90
+ *
91
+ * This module used to also export it — first as a second, MINIMAL
92
+ * implementation (save / getPending / acknowledge / purge, no leases, no
93
+ * retry, no dead-lettering, no dedupe, no validation), then briefly as a
94
+ * back-compat re-export. Both are gone.
95
+ *
96
+ * The re-export was tempting: seven kernels imported it from here, and one
97
+ * line would have spared seven rebuilds. It was still the wrong call. Two live
98
+ * import paths for one class means new code picks either, the migration stays
99
+ * permanently half-done, and the next person has to learn which path is
100
+ * "real". The cost of the move is seven rebuilds; paying it once is cheaper
101
+ * than carrying a shim forever.
102
+ *
103
+ * This subpath is the in-process BUS. The outbox is its own subpath.
104
+ */
88
105
  //#endregion
89
106
  export { ErrorLogger, InProcessEventBus, InProcessEventBusOptions, createInProcessBus };
@@ -121,7 +121,17 @@ interface IdempotencyClaim<TValue = unknown> {
121
121
  /** Monotonic attempt counter — each successful takeover increments it. */
122
122
  readonly attempts: number;
123
123
  }
124
- type IdempotencyErrorCode = /** Same key, different request body. Never a retry — two commands sharing one key. */'FINGERPRINT_MISMATCH' /** A terminal claim carrying no stored result. Unreplayable; must not answer "ok". */ | 'MISSING_REPLAY' /** The attempt no longer owns the lease — another attempt took over. */ | 'LEASE_LOST' /** Completing / renewing a claim that already reached a terminal state. */ | 'ALREADY_TERMINAL' /** Malformed input (empty key, non-positive lease, invalid instant). */ | 'INVALID_CLAIM';
124
+ type IdempotencyErrorCode =
125
+ /** Same key, different request body. Never a retry — two commands sharing one key. */
126
+ 'FINGERPRINT_MISMATCH' |
127
+ /** A terminal claim carrying no stored result. Unreplayable; must not answer "ok". */
128
+ 'MISSING_REPLAY' |
129
+ /** The attempt no longer owns the lease — another attempt took over. */
130
+ 'LEASE_LOST' |
131
+ /** Completing / renewing a claim that already reached a terminal state. */
132
+ 'ALREADY_TERMINAL' |
133
+ /** Malformed input (empty key, non-positive lease, invalid instant). */
134
+ 'INVALID_CLAIM';
125
135
  declare class IdempotencyError extends Error {
126
136
  readonly name = "IdempotencyError";
127
137
  readonly code: IdempotencyErrorCode;
@@ -1,5 +1,4 @@
1
1
  import { PersonName } from "./person.mjs";
2
-
3
2
  //#region src/identity/identity-verification.d.ts
4
3
  /**
5
4
  * Government / institutional identity documents. `trade_license` and
@@ -1,6 +1,5 @@
1
1
  import { DeadLetteredEvent, DomainEvent } from "./events.mjs";
2
2
  import { OutboxAcknowledgeOptions, OutboxClaimOptions, OutboxErrorInfo, OutboxFailOptions, OutboxStatus, OutboxStore, OutboxWriteOptions } from "./outbox.mjs";
3
-
4
3
  //#region src/events/memory-outbox.d.ts
5
4
  interface MemoryEntry {
6
5
  event: DomainEvent;
package/dist/mixin.d.mts CHANGED
@@ -45,7 +45,7 @@ interface Mixinable {
45
45
  * mixin payload through a well-typed accessor.
46
46
  */
47
47
  type WithMixin<T, K extends string, M> = T & {
48
- readonly mixins: Readonly<Record<string, unknown>> & { readonly [P in K]: M };
48
+ readonly mixins: Readonly<Record<string, unknown>> & { readonly [P in K]: M; };
49
49
  };
50
50
  /**
51
51
  * Attach (or overwrite) a mixin on a base entity. Returns a NEW object —
@@ -1,5 +1,4 @@
1
- import { n as Money } from "./money-DPo9fLBi.mjs";
2
-
1
+ import { n as Money } from "./money-CfAvfNiM.mjs";
3
2
  //#region src/money/monetization.d.ts
4
3
  /**
5
4
  * A map of currency code → {@link Money} for explicit per-currency pricing.
@@ -1,5 +1,4 @@
1
- import { t as Brand } from "./brand-LLXqoh5J.mjs";
2
-
1
+ import { t as Brand } from "./brand-Lj_75Y_r.mjs";
3
2
  //#region src/money/currency.d.ts
4
3
  /**
5
4
  * ISO 4217 currency code (3 uppercase letters).
package/dist/money.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { T as CurrencyMismatchError, _ as negateMoney, a as compareMoney, b as sumMoney, c as fromMajor, d as isPositiveMoney, f as isZeroMoney, g as multiplyMoney, h as money, i as addMoney, l as isMoney, m as minorUnitsToMajor, n as Money, o as equalsMoney, p as majorToMinorUnits, r as absMoney, s as formatMinorUnits, t as MinorUnits, u as isNegativeMoney, v as percentOfMinor, x as toMajor, y as subtractMoney } from "./money-DPo9fLBi.mjs";
1
+ import { T as CurrencyMismatchError, _ as negateMoney, a as compareMoney, b as sumMoney, c as fromMajor, d as isPositiveMoney, f as isZeroMoney, g as multiplyMoney, h as money, i as addMoney, l as isMoney, m as minorUnitsToMajor, n as Money, o as equalsMoney, p as majorToMinorUnits, r as absMoney, s as formatMinorUnits, t as MinorUnits, u as isNegativeMoney, v as percentOfMinor, x as toMajor, y as subtractMoney } from "./money-CfAvfNiM.mjs";
2
2
  export { CurrencyMismatchError, MinorUnits, Money, absMoney, addMoney, compareMoney, equalsMoney, formatMinorUnits, fromMajor, isMoney, isNegativeMoney, isPositiveMoney, isZeroMoney, majorToMinorUnits, minorUnitsToMajor, money, multiplyMoney, negateMoney, percentOfMinor, subtractMoney, sumMoney, toMajor };
package/dist/otp.mjs CHANGED
@@ -26,7 +26,7 @@ import { createHmac, randomInt, timingSafeEqual } from "node:crypto";
26
26
  * modeled here so the verify primitive stays stateless between attempts.
27
27
  */
28
28
  const DEFAULT_LENGTH = 6;
29
- const DEFAULT_TTL_MS = 300 * 1e3;
29
+ const DEFAULT_TTL_MS = 3e5;
30
30
  const DEFAULT_MAX_ATTEMPTS = 5;
31
31
  function hmac(code, secret) {
32
32
  return createHmac("sha256", secret).update(code).digest("hex");
package/dist/outbox.d.mts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { DeadLetteredEvent, DomainEvent } from "./events.mjs";
2
-
3
2
  //#region src/events/outbox.d.ts
4
3
  /**
5
4
  * Options passed to {@link OutboxStore.save} for richer write semantics.
@@ -20,8 +20,11 @@
20
20
  * Zero runtime, zero deps.
21
21
  */
22
22
  declare const PAYMENT_ALLOCATION_STATUS: {
23
- /** No portion of the payment has been applied to any obligation yet. */readonly UNALLOCATED: "unallocated"; /** Some of the payment is applied; remainder is available credit. */
24
- readonly PARTIAL: "partial"; /** Fully applied — `sum(allocations) === payment.totalAmount`. */
23
+ /** No portion of the payment has been applied to any obligation yet. */
24
+ readonly UNALLOCATED: "unallocated";
25
+ /** Some of the payment is applied; remainder is available credit. */
26
+ readonly PARTIAL: "partial";
27
+ /** Fully applied — `sum(allocations) === payment.totalAmount`. */
25
28
  readonly ALLOCATED: "allocated";
26
29
  /**
27
30
  * Sum of allocations exceeds the payment total — invariant violation
@@ -31,8 +34,11 @@ declare const PAYMENT_ALLOCATION_STATUS: {
31
34
  };
32
35
  type PaymentAllocationStatus = (typeof PAYMENT_ALLOCATION_STATUS)[keyof typeof PAYMENT_ALLOCATION_STATUS];
33
36
  declare const RECONCILIATION_STATUS: {
34
- /** No bank statement line / settlement record has been linked yet. */readonly UNRECONCILED: "unreconciled"; /** Auto-matched to a `BankTransaction` by amount + date + reference. */
35
- readonly MATCHED: "matched"; /** Operator-confirmed match (auto-match failed or was overridden). */
37
+ /** No bank statement line / settlement record has been linked yet. */
38
+ readonly UNRECONCILED: "unreconciled";
39
+ /** Auto-matched to a `BankTransaction` by amount + date + reference. */
40
+ readonly MATCHED: "matched";
41
+ /** Operator-confirmed match (auto-match failed or was overridden). */
36
42
  readonly MANUAL: "manual";
37
43
  /**
38
44
  * Match was contested — amount mismatch, duplicate claim, chargeback
@@ -20,15 +20,29 @@
20
20
  * Zero runtime, zero deps.
21
21
  */
22
22
  const PAYMENT_ALLOCATION_STATUS = {
23
+ /** No portion of the payment has been applied to any obligation yet. */
23
24
  UNALLOCATED: "unallocated",
25
+ /** Some of the payment is applied; remainder is available credit. */
24
26
  PARTIAL: "partial",
27
+ /** Fully applied — `sum(allocations) === payment.totalAmount`. */
25
28
  ALLOCATED: "allocated",
29
+ /**
30
+ * Sum of allocations exceeds the payment total — invariant violation
31
+ * that should be flagged for operator review, never silently accepted.
32
+ */
26
33
  OVER_ALLOCATED: "over_allocated"
27
34
  };
28
35
  const RECONCILIATION_STATUS = {
36
+ /** No bank statement line / settlement record has been linked yet. */
29
37
  UNRECONCILED: "unreconciled",
38
+ /** Auto-matched to a `BankTransaction` by amount + date + reference. */
30
39
  MATCHED: "matched",
40
+ /** Operator-confirmed match (auto-match failed or was overridden). */
31
41
  MANUAL: "manual",
42
+ /**
43
+ * Match was contested — amount mismatch, duplicate claim, chargeback
44
+ * in flight. Halts further automated processing until resolved.
45
+ */
32
46
  DISPUTED: "disputed"
33
47
  };
34
48
  const ALLOCATION_VALUES = new Set(Object.values(PAYMENT_ALLOCATION_STATUS));
@@ -1,7 +1,6 @@
1
- import { n as Money } from "./money-DPo9fLBi.mjs";
1
+ import { n as Money } from "./money-CfAvfNiM.mjs";
2
2
  import { PaymentMethodKind } from "./payment-method-kind.mjs";
3
3
  import { ProviderUnknownCause } from "./payment-gateway.mjs";
4
-
5
4
  //#region src/events/payment-events.d.ts
6
5
  /**
7
6
  * Canonical event-name constants. Packages SHOULD publish events
@@ -378,7 +377,7 @@ type _NoStrayMapKeys = Assert<keyof PaymentEventPayloadMap extends PaymentEventT
378
377
  /** Each payload's own `eventType` literal matches the key it is filed under. */
379
378
  type _DiscriminantMatchesKey = Assert<{ [K in keyof PaymentEventPayloadMap]: PaymentEventPayloadMap[K] extends {
380
379
  eventType: K;
381
- } ? true : false }[keyof PaymentEventPayloadMap] extends true ? true : false>;
380
+ } ? true : false; }[keyof PaymentEventPayloadMap] extends true ? true : false>;
382
381
  type PaymentEventContractGuards = [_EveryTypeMapped, _NoStrayMapKeys, _DiscriminantMatchesKey];
383
382
  /**
384
383
  * Narrow a payload by event type at RUNTIME, with the compile-time narrowing
@@ -18,6 +18,18 @@ const PAYMENT_EVENT_TYPE = {
18
18
  DISPUTE_WON: "payment.dispute_won",
19
19
  DISPUTE_LOST: "payment.dispute_lost",
20
20
  SETTLED: "payment.settled",
21
+ /**
22
+ * The provider command produced NO observable outcome — timeout, abort,
23
+ * network fault, or a provider that holds no record and refuses to guess.
24
+ *
25
+ * This member exists because the port contract (`ProviderCommandResult` in
26
+ * `/payment-gateway`) is three-valued — `confirmed | declined | unknown` —
27
+ * while this catalogue was two-valued. An adapter that hit the third case had
28
+ * nowhere to publish it, so the only shapes on the bus were SUCCEEDED and
29
+ * FAILED, and an unobserved outcome had to be forced into one of them.
30
+ * Forcing it into FAILED is the one that double-charges: it licenses a retry
31
+ * for a command that may already have moved money.
32
+ */
21
33
  UNKNOWN: "payment.unknown"
22
34
  };
23
35
  /**
@@ -1,6 +1,5 @@
1
- import { n as Money } from "./money-DPo9fLBi.mjs";
1
+ import { n as Money } from "./money-CfAvfNiM.mjs";
2
2
  import { PaymentMethodKind } from "./payment-method-kind.mjs";
3
-
4
3
  //#region src/money/payment-gateway.d.ts
5
4
  /**
6
5
  * Static capability declaration — providers expose this so the engine
@@ -363,7 +362,8 @@ type ProviderCommandResult<T> = {
363
362
  outcome: 'declined';
364
363
  error: PaymentDecline;
365
364
  } | {
366
- outcome: 'unknown'; /** Whatever handle we have for asking again later. */
365
+ outcome: 'unknown';
366
+ /** Whatever handle we have for asking again later. */
367
367
  providerReference?: string;
368
368
  /**
369
369
  * NORMALIZED reason we do not know — never a raw exception message.
@@ -23,23 +23,35 @@
23
23
  * "what kind of payment was this" needs to be typed.
24
24
  */
25
25
  declare const PAYMENT_METHOD_KIND: {
26
- /** Credit / debit card — Visa, Mastercard, Amex, RuPay, UnionPay, … */readonly CARD: "card"; /** Standard bank transfer — ACH credit, SEPA credit, IMPS, NEFT, RTGS, wire. */
27
- readonly BANK_TRANSFER: "bank_transfer"; /** Real-time bank rails — UPI, Pix, FedNow, PromptPay, FPX, GrabPay rails. */
28
- readonly INSTANT_BANK_TRANSFER: "instant_bank_transfer"; /** Mandate-based bank pull — SEPA Direct Debit, ACH Debit, BACS Direct Debit, AU BECS. */
29
- readonly DIRECT_DEBIT: "direct_debit"; /** Digital wallet — Apple Pay, Google Pay, Link, PayPal, Cashapp, Alipay, WeChat Pay, Revolut Pay. */
30
- readonly WALLET: "wallet"; /** Cash-in/out wallet infrastructure in unbanked markets — bKash, Nagad, M-Pesa, MoMo, GCash. */
31
- readonly MOBILE_MONEY: "mobile_money"; /** Buy now pay later — Klarna, Afterpay, Affirm, Tabby, Tamara. */
32
- readonly BNPL: "bnpl"; /** Customer-purchased prepaid credit / voucher. */
33
- readonly GIFT_CARD: "gift_card"; /** Physical cash. */
34
- readonly CASH: "cash"; /** Paper cheque. */
35
- readonly CHEQUE: "cheque"; /** Cryptocurrency settlement. */
26
+ /** Credit / debit card — Visa, Mastercard, Amex, RuPay, UnionPay, … */
27
+ readonly CARD: "card";
28
+ /** Standard bank transfer — ACH credit, SEPA credit, IMPS, NEFT, RTGS, wire. */
29
+ readonly BANK_TRANSFER: "bank_transfer";
30
+ /** Real-time bank rails — UPI, Pix, FedNow, PromptPay, FPX, GrabPay rails. */
31
+ readonly INSTANT_BANK_TRANSFER: "instant_bank_transfer";
32
+ /** Mandate-based bank pull — SEPA Direct Debit, ACH Debit, BACS Direct Debit, AU BECS. */
33
+ readonly DIRECT_DEBIT: "direct_debit";
34
+ /** Digital wallet — Apple Pay, Google Pay, Link, PayPal, Cashapp, Alipay, WeChat Pay, Revolut Pay. */
35
+ readonly WALLET: "wallet";
36
+ /** Cash-in/out wallet infrastructure in unbanked markets — bKash, Nagad, M-Pesa, MoMo, GCash. */
37
+ readonly MOBILE_MONEY: "mobile_money";
38
+ /** Buy now pay later — Klarna, Afterpay, Affirm, Tabby, Tamara. */
39
+ readonly BNPL: "bnpl";
40
+ /** Customer-purchased prepaid credit / voucher. */
41
+ readonly GIFT_CARD: "gift_card";
42
+ /** Physical cash. */
43
+ readonly CASH: "cash";
44
+ /** Paper cheque. */
45
+ readonly CHEQUE: "cheque";
46
+ /** Cryptocurrency settlement. */
36
47
  readonly CRYPTO: "cryptocurrency";
37
48
  /**
38
49
  * Operator-recorded settlement that doesn't fit any other kind — e.g.
39
50
  * an offline bank deposit confirmed by phone, an in-kind exchange, a
40
51
  * manual write-off. Always-valid escape hatch.
41
52
  */
42
- readonly MANUAL: "manual"; /** Unknown or vendor-specific kind — last resort. Prefer a specific kind. */
53
+ readonly MANUAL: "manual";
54
+ /** Unknown or vendor-specific kind — last resort. Prefer a specific kind. */
43
55
  readonly OTHER: "other";
44
56
  };
45
57
  type PaymentMethodKind = (typeof PAYMENT_METHOD_KIND)[keyof typeof PAYMENT_METHOD_KIND];
@@ -23,18 +23,35 @@
23
23
  * "what kind of payment was this" needs to be typed.
24
24
  */
25
25
  const PAYMENT_METHOD_KIND = {
26
+ /** Credit / debit card — Visa, Mastercard, Amex, RuPay, UnionPay, … */
26
27
  CARD: "card",
28
+ /** Standard bank transfer — ACH credit, SEPA credit, IMPS, NEFT, RTGS, wire. */
27
29
  BANK_TRANSFER: "bank_transfer",
30
+ /** Real-time bank rails — UPI, Pix, FedNow, PromptPay, FPX, GrabPay rails. */
28
31
  INSTANT_BANK_TRANSFER: "instant_bank_transfer",
32
+ /** Mandate-based bank pull — SEPA Direct Debit, ACH Debit, BACS Direct Debit, AU BECS. */
29
33
  DIRECT_DEBIT: "direct_debit",
34
+ /** Digital wallet — Apple Pay, Google Pay, Link, PayPal, Cashapp, Alipay, WeChat Pay, Revolut Pay. */
30
35
  WALLET: "wallet",
36
+ /** Cash-in/out wallet infrastructure in unbanked markets — bKash, Nagad, M-Pesa, MoMo, GCash. */
31
37
  MOBILE_MONEY: "mobile_money",
38
+ /** Buy now pay later — Klarna, Afterpay, Affirm, Tabby, Tamara. */
32
39
  BNPL: "bnpl",
40
+ /** Customer-purchased prepaid credit / voucher. */
33
41
  GIFT_CARD: "gift_card",
42
+ /** Physical cash. */
34
43
  CASH: "cash",
44
+ /** Paper cheque. */
35
45
  CHEQUE: "cheque",
46
+ /** Cryptocurrency settlement. */
36
47
  CRYPTO: "cryptocurrency",
48
+ /**
49
+ * Operator-recorded settlement that doesn't fit any other kind — e.g.
50
+ * an offline bank deposit confirmed by phone, an in-kind exchange, a
51
+ * manual write-off. Always-valid escape hatch.
52
+ */
37
53
  MANUAL: "manual",
54
+ /** Unknown or vendor-specific kind — last resort. Prefer a specific kind. */
38
55
  OTHER: "other"
39
56
  };
40
57
  const KIND_VALUES = new Set(Object.values(PAYMENT_METHOD_KIND));
package/dist/period.d.mts CHANGED
@@ -1,5 +1,4 @@
1
- import { t as CivilDate } from "./timezone-tlG7ygwx.mjs";
2
-
1
+ import { t as CivilDate } from "./timezone-CxUjxmRU.mjs";
3
2
  //#region src/scheduling/period.d.ts
4
3
  /**
5
4
  * A date range `[start, end]`.
@@ -55,7 +54,19 @@ type PeriodGranularity = 'year' | 'quarter' | 'month' | 'custom';
55
54
  declare class DateRangeError extends Error {
56
55
  readonly name = "DateRangeError";
57
56
  }
58
- type PeriodErrorCode = /** Two resolution shapes at once (`month` + `quarter`, or either + `start`/`end`). */'AMBIGUOUS_PERIOD' /** `start` without `end`, or vice versa. */ | 'INCOMPLETE_RANGE' /** `year` / `month` / `quarter` outside its legal domain, or non-integer. */ | 'INVALID_FIELD' /** No zone on the period and none supplied by the caller. */ | 'MISSING_TIMEZONE' /** The period's own zone and the caller's zone disagree. */ | 'TIMEZONE_CONFLICT' /** A `'YYYY'` / `'YYYY-MM'` / `'YYYY-Qn'` label that could not be read. */ | 'INVALID_LABEL';
57
+ type PeriodErrorCode =
58
+ /** Two resolution shapes at once (`month` + `quarter`, or either + `start`/`end`). */
59
+ 'AMBIGUOUS_PERIOD' |
60
+ /** `start` without `end`, or vice versa. */
61
+ 'INCOMPLETE_RANGE' |
62
+ /** `year` / `month` / `quarter` outside its legal domain, or non-integer. */
63
+ 'INVALID_FIELD' |
64
+ /** No zone on the period and none supplied by the caller. */
65
+ 'MISSING_TIMEZONE' |
66
+ /** The period's own zone and the caller's zone disagree. */
67
+ 'TIMEZONE_CONFLICT' |
68
+ /** A `'YYYY'` / `'YYYY-MM'` / `'YYYY-Qn'` label that could not be read. */
69
+ 'INVALID_LABEL';
59
70
  declare class PeriodError extends Error {
60
71
  readonly name = "PeriodError";
61
72
  readonly code: PeriodErrorCode;
@@ -1,5 +1,4 @@
1
1
  import { PersonName } from "./person.mjs";
2
-
3
2
  //#region src/identity/person-parse.d.ts
4
3
  /**
5
4
  * Does this `PersonName` carry at least one usable, non-empty name part?
package/dist/phone.d.mts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { Result } from "./result.mjs";
2
-
3
2
  //#region src/identity/phone.d.ts
4
3
  type PhoneErrorCode = 'EMPTY' | 'MISSING_PLUS' | 'NOT_E164' | 'TOO_SHORT' | 'TOO_LONG';
5
4
  declare class PhoneError extends Error {
package/dist/phone.mjs CHANGED
@@ -42,7 +42,7 @@ var PhoneError = class extends Error {
42
42
  * Kept inline (not exported) because it's an implementation detail of the
43
43
  * parser. Length is the only thing we actually need for the split.
44
44
  */
45
- const E164_PREFIXES_3 = new Set([
45
+ const E164_PREFIXES_3 = /* @__PURE__ */ new Set([
46
46
  "210",
47
47
  "211",
48
48
  "212",
@@ -215,7 +215,7 @@ const E164_PREFIXES_3 = new Set([
215
215
  "996",
216
216
  "998"
217
217
  ]);
218
- const E164_PREFIXES_2 = new Set([
218
+ const E164_PREFIXES_2 = /* @__PURE__ */ new Set([
219
219
  "20",
220
220
  "27",
221
221
  "30",
@@ -261,7 +261,7 @@ const E164_PREFIXES_2 = new Set([
261
261
  "95",
262
262
  "98"
263
263
  ]);
264
- const E164_PREFIXES_1 = new Set(["1", "7"]);
264
+ const E164_PREFIXES_1 = /* @__PURE__ */ new Set(["1", "7"]);
265
265
  /**
266
266
  * Parse free-form phone input into a normalized `PhoneNumber`. Accepts:
267
267
  * - `'+14155550182'` → e164 unchanged
@@ -1,5 +1,4 @@
1
- import { n as Money } from "./money-DPo9fLBi.mjs";
2
-
1
+ import { n as Money } from "./money-CfAvfNiM.mjs";
3
2
  //#region src/money/proration.d.ts
4
3
  /**
5
4
  * How to count days within a period.
@@ -1,15 +1,15 @@
1
1
  //#region src/retention/purge-evidence.ts
2
- const STRATEGY_KINDS = new Set([
2
+ const STRATEGY_KINDS = /* @__PURE__ */ new Set([
3
3
  "hard",
4
4
  "soft",
5
5
  "anonymize"
6
6
  ]);
7
- const ACTOR_KINDS = new Set([
7
+ const ACTOR_KINDS = /* @__PURE__ */ new Set([
8
8
  "user",
9
9
  "system",
10
10
  "service"
11
11
  ]);
12
- const STATUS_KINDS = new Set([
12
+ const STATUS_KINDS = /* @__PURE__ */ new Set([
13
13
  "completed",
14
14
  "partial",
15
15
  "failed"
@@ -43,7 +43,7 @@ var ShelfLifeError = class extends Error {
43
43
  this.code = code;
44
44
  }
45
45
  };
46
- const DAY_MS = 1440 * 60 * 1e3;
46
+ const DAY_MS = 864e5;
47
47
  const TRACKING_MODES = [
48
48
  "none",
49
49
  "lot",
@@ -1,6 +1,5 @@
1
- import { s as IsoWeekday } from "./cadence-BFQVC1Qp.mjs";
1
+ import { s as IsoWeekday } from "./cadence-5X6TiD49.mjs";
2
2
  import { SLA } from "./sla.mjs";
3
-
4
3
  //#region src/scheduling/sla-policy.d.ts
5
4
  /**
6
5
  * Tier of severity. Hosts decide the keys ('urgent' / 'p0' / 'gold' / etc.) —
@@ -1,5 +1,4 @@
1
1
  import { StateMachine } from "./state-machine.mjs";
2
-
3
2
  //#region src/workflow/state-diagram.d.ts
4
3
  interface StateDiagramOptions<TStatus extends string> {
5
4
  /**
@@ -54,7 +54,7 @@ function describeStateMachine(machine) {
54
54
  * that can never leave the state it is stuck in.
55
55
  */
56
56
  function unreachableStates(machine, initial) {
57
- const seen = new Set([initial]);
57
+ const seen = /* @__PURE__ */ new Set([initial]);
58
58
  const queue = [initial];
59
59
  while (queue.length > 0) for (const next of machine.validTargets(queue.pop())) if (!seen.has(next)) {
60
60
  seen.add(next);
@@ -1,7 +1,6 @@
1
- import { t as UtcOffsetMinutes } from "./calendar-B6fm-FbR.mjs";
1
+ import { t as UtcOffsetMinutes } from "./calendar-DJWSbcgE.mjs";
2
2
  import { DateRange } from "./period.mjs";
3
3
  import { StatusChangeEntry } from "./status-history.mjs";
4
-
5
4
  //#region src/workflow/suspension.d.ts
6
5
  /**
7
6
  * One suspension, past or current.
@@ -84,7 +83,9 @@ declare function allowancePeriod(at: Date, periodMonths?: number, offsetMinutes?
84
83
  * December. Ignoring that double-counts a straddling pause in both periods and
85
84
  * refuses a member who is within their allowance.
86
85
  */
87
- declare function suspendedDaysInPeriod(history: readonly SuspensionSpan[], period: DateRange, /** Now — bounds still-open spans. Defaults to the period end. */at?: Date): number;
86
+ declare function suspendedDaysInPeriod(history: readonly SuspensionSpan[], period: DateRange,
87
+ /** Now — bounds still-open spans. Defaults to the period end. */
88
+ at?: Date): number;
88
89
  /** Is any span still open at `at`? */
89
90
  declare function openSpans(history: readonly SuspensionSpan[], at: Date): readonly SuspensionSpan[];
90
91
  /**
@@ -144,7 +145,8 @@ declare function spansFromStatusHistory<TStatus extends string>(history: readonl
144
145
  * resume yet (a crash between the write and the append). Leaving it open would
145
146
  * charge the member allowance for time they were not frozen.
146
147
  */
147
- readonly currentStatus?: TStatus | undefined; /** Closes a genuinely-open trailing span for counting. Defaults to open. */
148
+ readonly currentStatus?: TStatus | undefined;
149
+ /** Closes a genuinely-open trailing span for counting. Defaults to open. */
148
150
  readonly openEndedAt?: Date | undefined;
149
151
  }): SuspensionSpan[];
150
152
  //#endregion
@@ -39,11 +39,12 @@ import { addMonths, startOfDay } from "./calendar.mjs";
39
39
  * PURE: no clock reads beyond the `at` you pass, no I/O. The same evaluation runs
40
40
  * on the server, in an admin preview, and in a test.
41
41
  */
42
- const MS_PER_DAY = 1440 * 60 * 1e3;
42
+ const MS_PER_DAY = 864e5;
43
43
  /** The allowance period containing `at`, as a half-open range. */
44
44
  function allowancePeriod(at, periodMonths = 12, offsetMinutes = 0) {
45
+ const anchor = startOfDay(at, offsetMinutes);
45
46
  return {
46
- start: addMonths(startOfDay(at, offsetMinutes), -periodMonths),
47
+ start: addMonths(anchor, -periodMonths),
47
48
  end: at
48
49
  };
49
50
  }
@@ -1,7 +1,6 @@
1
- import { t as Brand } from "./brand-LLXqoh5J.mjs";
2
- import { s as IsoWeekday } from "./cadence-BFQVC1Qp.mjs";
3
- import { t as UtcOffsetMinutes } from "./calendar-B6fm-FbR.mjs";
4
-
1
+ import { t as Brand } from "./brand-Lj_75Y_r.mjs";
2
+ import { s as IsoWeekday } from "./cadence-5X6TiD49.mjs";
3
+ import { t as UtcOffsetMinutes } from "./calendar-DJWSbcgE.mjs";
5
4
  //#region src/scheduling/timezone.d.ts
6
5
  declare class TimeZoneError extends Error {
7
6
  constructor(message: string);
@@ -1,2 +1,2 @@
1
- import { a as civilDate, c as civilDaysBetween, d as isoWeekOfCivilDate, f as listTimeZones, h as zoneOffsetMinutes, i as addCivilDays, l as isCivilDate, m as zoneOffsetLabel, n as LocalTimeParts, o as civilDateOf, p as localTimeParts, r as TimeZoneError, s as civilDateToInstant, t as CivilDate, u as isValidTimeZone } from "./timezone-tlG7ygwx.mjs";
1
+ import { a as civilDate, c as civilDaysBetween, d as isoWeekOfCivilDate, f as listTimeZones, h as zoneOffsetMinutes, i as addCivilDays, l as isCivilDate, m as zoneOffsetLabel, n as LocalTimeParts, o as civilDateOf, p as localTimeParts, r as TimeZoneError, s as civilDateToInstant, t as CivilDate, u as isValidTimeZone } from "./timezone-CxUjxmRU.mjs";
2
2
  export { CivilDate, LocalTimeParts, TimeZoneError, addCivilDays, civilDate, civilDateOf, civilDateToInstant, civilDaysBetween, isCivilDate, isValidTimeZone, isoWeekOfCivilDate, listTimeZones, localTimeParts, zoneOffsetLabel, zoneOffsetMinutes };
@@ -1,5 +1,4 @@
1
- import { t as MinorUnits, w as CurrencyCode } from "./money-DPo9fLBi.mjs";
2
-
1
+ import { t as MinorUnits, w as CurrencyCode } from "./money-CfAvfNiM.mjs";
3
2
  //#region src/money/unit-cost-rate.d.ts
4
3
  /** Fixed, ecosystem-wide rate scale: 6 decimal places below the minor unit. */
5
4
  declare const RATE_SCALE = 1000000;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/primitives",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "Framework-agnostic TypeScript primitives shared across Classytic packages — Money, events, pagination, tenant config, and other building blocks.",
5
5
  "author": "Classytic",
6
6
  "license": "MIT",
@@ -57,6 +57,10 @@
57
57
  "types": "./dist/context.d.mts",
58
58
  "default": "./dist/context.mjs"
59
59
  },
60
+ "./environment": {
61
+ "types": "./dist/environment.d.mts",
62
+ "default": "./dist/environment.mjs"
63
+ },
60
64
  "./events": {
61
65
  "types": "./dist/events.d.mts",
62
66
  "default": "./dist/events.mjs"
@@ -225,7 +229,8 @@
225
229
  "@biomejs/biome": "^2.4.9",
226
230
  "@classytic/dev-tools": "^0.2.0",
227
231
  "@types/node": "^25.5.0",
228
- "tsdown": "^0.21.7",
232
+ "publint": "^0.3.23",
233
+ "tsdown": "^0.22.14",
229
234
  "typescript": "^5.7.0",
230
235
  "vitest": "^3.0.0"
231
236
  },