@openlup/core 0.11.0 → 0.13.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.
- package/AGENTS.md +103 -0
- package/CHANGELOG.md +64 -8
- package/CONTRIBUTING.md +4 -2
- package/MAINTAINERS.md +2 -1
- package/README.md +59 -21
- package/SECURITY.md +4 -4
- package/dist/outbox/index.d.ts +55 -0
- package/dist/outbox/index.js +1 -0
- package/dist/outbox/vocabulary.d.ts +7 -0
- package/dist/outbox/vocabulary.js +36 -0
- package/dist/platform-runtime/index.d.ts +1 -0
- package/dist/platform-runtime/ports.d.ts +24 -0
- package/dist/readiness/contracts.d.ts +121 -0
- package/dist/readiness/contracts.js +2 -0
- package/dist/readiness/declarations.d.ts +45 -0
- package/dist/readiness/declarations.js +58 -0
- package/dist/readiness/index.d.ts +5 -0
- package/dist/readiness/index.js +147 -0
- package/dist/readiness/schemaProbe.d.ts +13 -0
- package/dist/readiness/schemaProbe.js +25 -0
- package/dist/standard-schema/index.d.ts +26 -0
- package/dist/standard-schema/index.js +1 -0
- package/docs/SUBSCRIPTION_ENGINE.md +235 -0
- package/package.json +33 -11
- package/release-gates.json +91 -29
- package/src/outbox/index.ts +44 -0
- package/src/outbox/vocabulary.ts +38 -0
- package/src/platform-runtime/index.ts +2 -0
- package/src/platform-runtime/ports.ts +43 -0
- package/src/readiness/contracts.ts +57 -0
- package/src/readiness/declarations.ts +38 -0
- package/src/readiness/index.ts +97 -0
- package/src/readiness/schemaProbe.ts +24 -0
- package/src/standard-schema/index.ts +17 -0
- package/dist/checkout/idempotency.d.ts +0 -29
- package/dist/checkout/idempotency.js +0 -37
- package/dist/checkout/index.d.ts +0 -2
- package/dist/checkout/index.js +0 -2
- package/src/checkout/idempotency.ts +0 -49
- package/src/checkout/index.ts +0 -2
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ReadinessInput, ReadinessIssue } from "./contracts.js";
|
|
3
|
+
export declare const schemaObjectShape: z.ZodObject<{
|
|
4
|
+
kind: z.ZodEnum<{
|
|
5
|
+
function: "function";
|
|
6
|
+
table: "table";
|
|
7
|
+
column: "column";
|
|
8
|
+
}>;
|
|
9
|
+
name: z.ZodString;
|
|
10
|
+
signature: z.ZodOptional<z.ZodString>;
|
|
11
|
+
}, z.core.$strict>;
|
|
12
|
+
export declare const manifestShape: z.ZodObject<{
|
|
13
|
+
name: z.ZodString;
|
|
14
|
+
version: z.ZodString;
|
|
15
|
+
kind: z.ZodEnum<{
|
|
16
|
+
kernel: "kernel";
|
|
17
|
+
rail: "rail";
|
|
18
|
+
capability: "capability";
|
|
19
|
+
implementation: "implementation";
|
|
20
|
+
}>;
|
|
21
|
+
emits: z.ZodArray<z.ZodString>;
|
|
22
|
+
handles: z.ZodArray<z.ZodString>;
|
|
23
|
+
schedules: z.ZodArray<z.ZodObject<{
|
|
24
|
+
id: z.ZodString;
|
|
25
|
+
cadenceSeconds: z.ZodNumber;
|
|
26
|
+
}, z.core.$strict>>;
|
|
27
|
+
routes: z.ZodArray<z.ZodString>;
|
|
28
|
+
requiredPorts: z.ZodArray<z.ZodString>;
|
|
29
|
+
env: z.ZodArray<z.ZodString>;
|
|
30
|
+
requiredSchema: z.ZodOptional<z.ZodObject<{
|
|
31
|
+
version: z.ZodString;
|
|
32
|
+
migration: z.ZodString;
|
|
33
|
+
objects: z.ZodArray<z.ZodObject<{
|
|
34
|
+
kind: z.ZodEnum<{
|
|
35
|
+
function: "function";
|
|
36
|
+
table: "table";
|
|
37
|
+
column: "column";
|
|
38
|
+
}>;
|
|
39
|
+
name: z.ZodString;
|
|
40
|
+
signature: z.ZodOptional<z.ZodString>;
|
|
41
|
+
}, z.core.$strict>>;
|
|
42
|
+
}, z.core.$strict>>;
|
|
43
|
+
}, z.core.$strict>;
|
|
44
|
+
/** Validate declarations without invoking a handler, schedule, route or probe. */
|
|
45
|
+
export declare function declarationIssues(input: ReadinessInput): ReadinessIssue[];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const name = z.string().min(1).max(240).regex(/^[a-zA-Z0-9@_./:-]+$/);
|
|
3
|
+
const version = z.string().regex(/^0\.\d+\.\d+$/);
|
|
4
|
+
const list = z.array(name).refine((items) => new Set(items).size === items.length);
|
|
5
|
+
const kind = z.enum(["kernel", "rail", "capability", "implementation"]);
|
|
6
|
+
export const schemaObjectShape = z.object({ kind: z.enum(["table", "column", "function"]), name,
|
|
7
|
+
signature: z.string().max(1000).optional() }).strict().refine((o) => o.name.split(".").length === (o.kind === "column" ? 3 : 2) && (o.kind !== "function" || o.signature !== undefined));
|
|
8
|
+
export const manifestShape = z.object({ name: name.regex(/^@openlup\/[a-z0-9-]+$/), version, kind,
|
|
9
|
+
emits: list, handles: list, schedules: z.array(z.object({ id: name, cadenceSeconds: z.number().finite().positive() }).strict()),
|
|
10
|
+
routes: list, requiredPorts: list, env: list,
|
|
11
|
+
requiredSchema: z.object({ version: name, migration: name, objects: z.array(schemaObjectShape) }).strict().optional(),
|
|
12
|
+
}).strict();
|
|
13
|
+
/** Validate declarations without invoking a handler, schedule, route or probe. */
|
|
14
|
+
export function declarationIssues(input) {
|
|
15
|
+
const issues = [];
|
|
16
|
+
const issue = (subject, fix, code = "OPENLUP_E_SET_MISMATCH") => issues.push({ code, package: "@openlup/core", subject, fix });
|
|
17
|
+
if (!input || typeof input !== "object") {
|
|
18
|
+
issue("input", "Supply complete candidate observations.");
|
|
19
|
+
return issues;
|
|
20
|
+
}
|
|
21
|
+
if (!input.candidate || ![input.candidate.artifact, input.candidate.configuration, input.candidate.environment, input.candidate.database, input.candidate.schema].every((v) => typeof v === "string" && /^[a-zA-Z0-9_.:/-]{1,240}$/.test(v)))
|
|
22
|
+
issue("candidate", "Supply sanitized immutable artifact, configuration, environment and database identities.");
|
|
23
|
+
if (input.inventoryComplete !== true)
|
|
24
|
+
issue("inventory", "Observe the complete installed package inventory, including core.");
|
|
25
|
+
if (!Array.isArray(input.loadedPackages) || !Array.isArray(input.contributions)) {
|
|
26
|
+
issue("inventory", "Supply loaded packages and contributions as arrays.");
|
|
27
|
+
return issues;
|
|
28
|
+
}
|
|
29
|
+
for (const pkg of input.loadedPackages)
|
|
30
|
+
if (!z.object({ name, version, kind }).strict().safeParse(pkg).success)
|
|
31
|
+
issue("loaded-package", "Supply a valid observed package name, version and kind.");
|
|
32
|
+
for (const c of input.contributions) {
|
|
33
|
+
if (!manifestShape.safeParse(c?.manifest).success) {
|
|
34
|
+
issue("manifest", "Supply a valid complete manifest with unique declarations.");
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (!Array.isArray(c.handlers) || c.handlers.some((h) => !name.safeParse(h?.eventType).success || !Number.isFinite(h?.timeoutMs) || h?.timeoutMs <= 0))
|
|
38
|
+
issue(c.manifest.name, "Supply valid shared handler descriptors.", "OPENLUP_E_EVENT_UNHANDLED");
|
|
39
|
+
if (!Array.isArray(c.schedules) || c.schedules.some((s) => !name.safeParse(s?.id).success || !Number.isFinite(s?.cadenceSeconds) || s?.cadenceSeconds <= 0 || typeof s?.run !== "function"))
|
|
40
|
+
issue(c.manifest.name, "Supply actual runnable schedules.", "OPENLUP_E_SCHEDULE_UNBOUND");
|
|
41
|
+
if (!Array.isArray(c.routes) || c.routes.some((r) => !name.safeParse(r?.id).success || typeof r?.handle !== "function"))
|
|
42
|
+
issue(c.manifest.name, "Supply actual route registrations.", "OPENLUP_E_PORT_MISSING");
|
|
43
|
+
const objects = c.manifest.requiredSchema?.objects ?? [];
|
|
44
|
+
if (new Set(objects.map((o) => `${o.kind}:${o.name}:${o.signature ?? ""}`)).size !== objects.length)
|
|
45
|
+
issue(c.manifest.name, "Remove duplicate schema requirements.", "OPENLUP_E_SCHEMA_BEHIND");
|
|
46
|
+
}
|
|
47
|
+
if (!Array.isArray(input.vocabulary) || input.vocabulary.some((v) => !name.safeParse(v?.eventType).success || !name.safeParse(v?.owner).success))
|
|
48
|
+
issue("vocabulary", "Supply explicit vocabulary entries with owners.", "OPENLUP_E_EVENT_UNHANDLED");
|
|
49
|
+
if (!Array.isArray(input.exemptions) || input.exemptions.some((e) => !name.safeParse(e?.pattern).success || typeof e?.prefix !== "boolean" || !name.safeParse(e?.owner).success || typeof e?.reason !== "string" || !e?.reason.trim() || !["ignored", "dormant"].includes(e?.state)))
|
|
50
|
+
issue("exemptions", "Supply explicit owned exemptions with reasons.", "OPENLUP_E_EVENT_UNHANDLED");
|
|
51
|
+
if (!input.bindings || [input.bindings.ports, input.bindings.routes, input.bindings.triggers, input.bindings.env].some((v) => !v || typeof v !== "object" || Array.isArray(v)))
|
|
52
|
+
issue("bindings", "Supply actual host bindings.", "OPENLUP_E_PORT_MISSING");
|
|
53
|
+
if (!input.schemaProbe || typeof input.schemaProbe.observe !== "function")
|
|
54
|
+
issue("schemaProbe", "Bind a schema observation port.", "OPENLUP_E_SCHEMA_BEHIND");
|
|
55
|
+
if (input.probeTimeoutMs !== undefined && (!Number.isFinite(input.probeTimeoutMs) || input.probeTimeoutMs < 1 || input.probeTimeoutMs > 30000))
|
|
56
|
+
issue("probeTimeoutMs", "Use a bounded probe timeout from 1 to 30000 ms.", "OPENLUP_E_SCHEMA_BEHIND");
|
|
57
|
+
return issues;
|
|
58
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ReadinessInput, ReadinessReport } from "./contracts.js";
|
|
2
|
+
export * from "./contracts.js";
|
|
3
|
+
export { schemaObjectKey, buildSchemaObjectProbe, createSchemaProbe } from "./schemaProbe.js";
|
|
4
|
+
/** @beta Evaluates supplied facts; never performs migration or effect work. */
|
|
5
|
+
export declare function checkReadiness(input: ReadinessInput): Promise<ReadinessReport>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { declarationIssues, manifestShape, schemaObjectShape } from "./declarations.js";
|
|
2
|
+
import { schemaObjectKey } from "./schemaProbe.js";
|
|
3
|
+
export * from "./contracts.js";
|
|
4
|
+
export { schemaObjectKey, buildSchemaObjectProbe, createSchemaProbe } from "./schemaProbe.js";
|
|
5
|
+
/** @beta Evaluates supplied facts; never performs migration or effect work. */
|
|
6
|
+
export async function checkReadiness(input) {
|
|
7
|
+
const issues = declarationIssues(input);
|
|
8
|
+
let unknown = issues.some((i) => ["input", "candidate", "inventory", "loaded-package", "schemaProbe", "probeTimeoutMs"].includes(i.subject));
|
|
9
|
+
const add = (code, pkg, subject, fix, observation) => {
|
|
10
|
+
issues.push({ code, package: pkg, subject, fix, ...(observation ? { observation } : {}) });
|
|
11
|
+
};
|
|
12
|
+
const report = () => ({ state: unknown ? "unknown" : issues.length ? "unsatisfied" : "ready", candidate: input?.candidate,
|
|
13
|
+
loadedPackages: input?.loadedPackages ?? [], issues: issues.sort((a, b) => [a.code, a.package, a.subject, a.observation ?? ""].join("|").localeCompare([b.code, b.package, b.subject, b.observation ?? ""].join("|"))) });
|
|
14
|
+
if (!input || !Array.isArray(input.loadedPackages) || !Array.isArray(input.contributions))
|
|
15
|
+
return report();
|
|
16
|
+
const contributions = input.contributions.filter((c) => manifestShape.safeParse(c?.manifest).success && Array.isArray(c.handlers) && c.handlers.every((h) => h && typeof h.eventType === "string" && Number.isFinite(h.timeoutMs) && h.timeoutMs > 0) && Array.isArray(c.schedules) && c.schedules.every((s) => s && typeof s.id === "string" && typeof s.run === "function") && Array.isArray(c.routes) && c.routes.every((r) => r && typeof r.id === "string" && typeof r.handle === "function"));
|
|
17
|
+
const loaded = input.loadedPackages.filter((p) => p && typeof p.name === "string" && typeof p.version === "string");
|
|
18
|
+
const packages = new Map();
|
|
19
|
+
for (const p of loaded)
|
|
20
|
+
packages.set(p.name, (packages.get(p.name) ?? 0) + 1);
|
|
21
|
+
if (packages.get("@openlup/core") !== 1 || loaded.find((p) => p.name === "@openlup/core")?.kind !== "kernel")
|
|
22
|
+
add("OPENLUP_E_SET_MISMATCH", "@openlup/core", "kernel", "Observe exactly one core kernel.");
|
|
23
|
+
const set = loaded.find((p) => p.name === "@openlup/core")?.version;
|
|
24
|
+
const install = `Align the complete set: npm install ${[...new Set(loaded.map((p) => p.name))].sort().map((name) => `${name}@${set ?? "<set-version>"}`).join(" ")}`;
|
|
25
|
+
for (const [name, count] of packages)
|
|
26
|
+
if (count > 1)
|
|
27
|
+
add("OPENLUP_E_SET_MISMATCH", name, "inventory", "Observe each loaded package exactly once.");
|
|
28
|
+
for (const p of loaded) {
|
|
29
|
+
if (p.version !== set || (p.kind === "kernel" && p.name !== "@openlup/core"))
|
|
30
|
+
add("OPENLUP_E_SET_MISMATCH", p.name, "version", install);
|
|
31
|
+
if (p.kind !== "kernel" && contributions.filter((c) => c.manifest.name === p.name).length !== 1)
|
|
32
|
+
add("OPENLUP_E_SET_MISMATCH", p.name, "contribution", "Compose exactly one contribution for each loaded non-kernel package.");
|
|
33
|
+
}
|
|
34
|
+
const handlers = new Map();
|
|
35
|
+
const emitters = new Map();
|
|
36
|
+
const events = new Set();
|
|
37
|
+
for (const v of (Array.isArray(input.vocabulary) ? input.vocabulary : [])) {
|
|
38
|
+
if (v && typeof v.eventType === "string") {
|
|
39
|
+
if (events.has(v.eventType))
|
|
40
|
+
add("OPENLUP_E_EVENT_DUPLICATE", v.owner, v.eventType, "Declare each vocabulary identity once.");
|
|
41
|
+
events.add(v.eventType);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const c of contributions) {
|
|
45
|
+
const m = c.manifest;
|
|
46
|
+
const p = loaded.filter((p) => p.name === m.name);
|
|
47
|
+
if (p.length !== 1 || p[0]?.version !== m.version || p[0]?.kind !== m.kind)
|
|
48
|
+
add("OPENLUP_E_SET_MISMATCH", m.name, "manifest", "Match the contribution to its observed installed package.");
|
|
49
|
+
for (const type of m.emits) {
|
|
50
|
+
events.add(type);
|
|
51
|
+
if (m.kind !== "kernel")
|
|
52
|
+
emitters.set(type, [...(emitters.get(type) ?? []), m.name]);
|
|
53
|
+
}
|
|
54
|
+
const actual = c.handlers.map((h) => h?.eventType);
|
|
55
|
+
for (const type of new Set([...m.handles, ...actual])) {
|
|
56
|
+
if (m.handles.filter((x) => x === type).length !== 1 || actual.filter((x) => x === type).length !== 1)
|
|
57
|
+
add("OPENLUP_E_EVENT_DUPLICATE", m.name, type, "Match each declared consumer to exactly one composed handler descriptor.");
|
|
58
|
+
else
|
|
59
|
+
handlers.set(type, [...(handlers.get(type) ?? []), m.name]);
|
|
60
|
+
events.add(type);
|
|
61
|
+
}
|
|
62
|
+
for (const port of m.requiredPorts)
|
|
63
|
+
if (!isPort(input.bindings?.ports?.[`${m.name}:${port}`]))
|
|
64
|
+
add("OPENLUP_E_PORT_MISSING", m.name, port, "Bind the selected adapter port in composition.");
|
|
65
|
+
for (const env of m.env)
|
|
66
|
+
if (typeof input.bindings?.env?.[env] !== "string" || !input.bindings.env[env]?.trim())
|
|
67
|
+
add("OPENLUP_E_ENV_MISSING", m.name, env, "Supply the named environment variable without exposing its value.");
|
|
68
|
+
for (const id of new Set([...m.schedules.map((s) => s.id), ...c.schedules.map((s) => s?.id)])) {
|
|
69
|
+
const declarations = m.schedules.filter((s) => s.id === id), actual = c.schedules.filter((s) => s?.id === id);
|
|
70
|
+
if (declarations.length !== 1 || actual.length !== 1 || declarations[0]?.cadenceSeconds !== actual[0]?.cadenceSeconds || typeof actual[0]?.run !== "function" || input.bindings?.triggers?.[`${m.name}:${id}`] !== actual[0]?.run)
|
|
71
|
+
add("OPENLUP_E_SCHEDULE_UNBOUND", m.name, id, "Bind the actual schedule with its declared cadence to one host trigger.");
|
|
72
|
+
}
|
|
73
|
+
for (const id of new Set([...m.routes, ...c.routes.map((r) => r?.id)]))
|
|
74
|
+
if (m.routes.filter((r) => r === id).length !== 1 || c.routes.filter((r) => r?.id === id && typeof r.handle === "function").length !== 1 || input.bindings?.routes?.[`${m.name}:${id}`] !== c.routes.find((r) => r.id === id)?.handle)
|
|
75
|
+
add("OPENLUP_E_PORT_MISSING", m.name, id, "Bind the declared route to its actual host entry.");
|
|
76
|
+
}
|
|
77
|
+
for (const [type, owners] of [...emitters, ...handlers])
|
|
78
|
+
if (owners.length > 1)
|
|
79
|
+
add("OPENLUP_E_EVENT_DUPLICATE", owners.sort().join(","), type, "Use one non-kernel producer owner and one composed consumer per event.");
|
|
80
|
+
for (const type of events)
|
|
81
|
+
if (!handlers.has(type) && !(Array.isArray(input.exemptions) ? input.exemptions : []).some((e) => e && typeof e.reason === "string" && e.reason.trim() && typeof e.owner === "string" && typeof e.pattern === "string" && ["ignored", "dormant"].includes(e.state) && typeof e.prefix === "boolean" && (e.prefix ? type.startsWith(e.pattern) : type === e.pattern)))
|
|
82
|
+
add("OPENLUP_E_EVENT_UNHANDLED", "@openlup/core", type, "Register a handler or an explicit owned ignored/dormant declaration.");
|
|
83
|
+
for (const c of contributions) {
|
|
84
|
+
const req = c.manifest.requiredSchema;
|
|
85
|
+
if (!req)
|
|
86
|
+
continue;
|
|
87
|
+
if (issues.some((i) => ["schemaProbe", "probeTimeoutMs"].includes(i.subject))) {
|
|
88
|
+
unknown = true;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
let observation;
|
|
92
|
+
try {
|
|
93
|
+
observation = await boundedObservation(input, req);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
unknown = true;
|
|
97
|
+
add("OPENLUP_E_SCHEMA_BEHIND", c.manifest.name, req.version, `Observe the selected adapter and apply ${req.migration} through your migration chain.`, "probe_unavailable");
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (!validObservation(observation, req)) {
|
|
101
|
+
unknown = true;
|
|
102
|
+
add("OPENLUP_E_SCHEMA_BEHIND", c.manifest.name, req.version, "Return one valid observation for every requested schema object.", "probe_malformed_or_incomplete");
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (!observation.database || observation.database !== input.candidate?.database || observation.version === null) {
|
|
106
|
+
unknown = true;
|
|
107
|
+
add("OPENLUP_E_SCHEMA_BEHIND", c.manifest.name, req.version, "Observe the exact candidate database and applied migration identity.", "identity_or_version_unknown");
|
|
108
|
+
}
|
|
109
|
+
else if (observation.version !== req.version)
|
|
110
|
+
add("OPENLUP_E_SCHEMA_BEHIND", c.manifest.name, req.version, `Apply ${req.migration} and observe its version.`, "version_mismatch");
|
|
111
|
+
for (const { object, present } of observation.objects)
|
|
112
|
+
if (present !== true) {
|
|
113
|
+
unknown ||= present === null;
|
|
114
|
+
add("OPENLUP_E_SCHEMA_BEHIND", c.manifest.name, schemaObjectKey(object), `Apply ${req.migration} for the selected adapter.`, present === null ? "object_unknown" : "object_absent");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return report();
|
|
118
|
+
}
|
|
119
|
+
function validObservation(value, requirement) {
|
|
120
|
+
if (!value || typeof value !== "object")
|
|
121
|
+
return false;
|
|
122
|
+
const v = value;
|
|
123
|
+
if (typeof v.database !== "string" || !(v.version === null || typeof v.version === "string") || !Array.isArray(v.objects))
|
|
124
|
+
return false;
|
|
125
|
+
const requested = new Set(requirement.objects.map(schemaObjectKey)), seen = new Set();
|
|
126
|
+
for (const item of v.objects) {
|
|
127
|
+
if (!item || !schemaObjectShape.safeParse(item.object).success || ![true, false, null].includes(item.present))
|
|
128
|
+
return false;
|
|
129
|
+
const key = schemaObjectKey(item.object);
|
|
130
|
+
if (!requested.has(key) || seen.has(key))
|
|
131
|
+
return false;
|
|
132
|
+
seen.add(key);
|
|
133
|
+
}
|
|
134
|
+
return seen.size === requested.size;
|
|
135
|
+
}
|
|
136
|
+
async function boundedObservation(input, requirement) {
|
|
137
|
+
const timers = globalThis;
|
|
138
|
+
let timer;
|
|
139
|
+
try {
|
|
140
|
+
return await Promise.race([input.schemaProbe.observe(requirement), new Promise((_, reject) => { timer = timers.setTimeout(() => reject(new Error("probe_timeout")), input.probeTimeoutMs ?? 3000); })]);
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
if (timer !== undefined)
|
|
144
|
+
timers.clearTimeout(timer);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function isPort(value) { return typeof value === "function" || (value !== null && typeof value === "object" && !Array.isArray(value)); }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SchemaObject, SqlExecutor, SchemaProbePort, RequiredSchema } from "./contracts.js";
|
|
2
|
+
/** @beta Identity distinguishes overloads and qualified columns. */
|
|
3
|
+
export declare function schemaObjectKey(object: SchemaObject): string;
|
|
4
|
+
/** @beta Parameterized catalog queries; this builder performs no I/O. */
|
|
5
|
+
export declare function buildSchemaObjectProbe(object: SchemaObject): {
|
|
6
|
+
text: string;
|
|
7
|
+
values: readonly unknown[];
|
|
8
|
+
};
|
|
9
|
+
/** @beta Explicit adapter. Host supplies independently observed database and migration identity. */
|
|
10
|
+
export declare function createSchemaProbe(executor: SqlExecutor, identity: (requirement: RequiredSchema) => Promise<{
|
|
11
|
+
database: string;
|
|
12
|
+
version: string | null;
|
|
13
|
+
}>): SchemaProbePort;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** @beta Identity distinguishes overloads and qualified columns. */
|
|
2
|
+
export function schemaObjectKey(object) {
|
|
3
|
+
return `${object.kind}:${object.name}${object.kind === "function" ? `(${object.signature ?? ""})` : ""}`;
|
|
4
|
+
}
|
|
5
|
+
/** @beta Parameterized catalog queries; this builder performs no I/O. */
|
|
6
|
+
export function buildSchemaObjectProbe(object) {
|
|
7
|
+
if (object.kind === "function")
|
|
8
|
+
return { text: "select to_regprocedure($1)::oid is not null as present", values: [`${object.name}(${object.signature ?? ""})`] };
|
|
9
|
+
if (object.kind === "table")
|
|
10
|
+
return { text: "select exists (select 1 from pg_catalog.pg_class c where c.oid = to_regclass($1) and c.relkind in ('r','p')) as present", values: [object.name] };
|
|
11
|
+
const parts = object.name.split(".");
|
|
12
|
+
return { text: "select exists (select 1 from pg_catalog.pg_attribute where attrelid = to_regclass($1) and attname = $2 and attnum > 0 and not attisdropped) as present", values: [parts.slice(0, -1).join("."), parts.at(-1)] };
|
|
13
|
+
}
|
|
14
|
+
/** @beta Explicit adapter. Host supplies independently observed database and migration identity. */
|
|
15
|
+
export function createSchemaProbe(executor, identity) {
|
|
16
|
+
return { async observe(requirement) {
|
|
17
|
+
const observed = await identity(requirement);
|
|
18
|
+
const objects = await Promise.all(requirement.objects.map(async (object) => {
|
|
19
|
+
const query = buildSchemaObjectProbe(object);
|
|
20
|
+
const { rows } = await executor.query(query.text, query.values);
|
|
21
|
+
return { object, present: rows.length === 1 && typeof rows[0]?.present === "boolean" ? rows[0].present : null };
|
|
22
|
+
}));
|
|
23
|
+
return { ...observed, objects };
|
|
24
|
+
} };
|
|
25
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** @beta Structural Standard Schema v1 interface; no validator dependency required. */
|
|
2
|
+
export interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
3
|
+
readonly "~standard": {
|
|
4
|
+
readonly version: 1;
|
|
5
|
+
readonly vendor: string;
|
|
6
|
+
readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
|
|
7
|
+
readonly types?: {
|
|
8
|
+
readonly input: Input;
|
|
9
|
+
readonly output: Output;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/** @beta */
|
|
14
|
+
export type Result<Output> = {
|
|
15
|
+
readonly value: Output;
|
|
16
|
+
readonly issues?: undefined;
|
|
17
|
+
} | {
|
|
18
|
+
readonly issues: readonly Issue[];
|
|
19
|
+
};
|
|
20
|
+
/** @beta */
|
|
21
|
+
export interface Issue {
|
|
22
|
+
readonly message: string;
|
|
23
|
+
readonly path?: readonly (PropertyKey | {
|
|
24
|
+
readonly key: PropertyKey;
|
|
25
|
+
})[];
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# Subscription engine
|
|
2
|
+
|
|
3
|
+
Owns: the pure subscription lifecycle kernel exported as `@openlup/core/subscription`.
|
|
4
|
+
Watches: `src/subscription/**`
|
|
5
|
+
Verified against: repository state `735d5d7` plus the lifecycle-parity change
|
|
6
|
+
(2026-09-26) — every exported name below was read out of the eight source files
|
|
7
|
+
in `src/subscription/`, the status matrix out of `subscriptionEngineLifecycle.ts`
|
|
8
|
+
and the managed SQL guard it mirrors, the
|
|
9
|
+
payment semantics out of `src/subscription/subscriptionEnginePayment.ts`, and
|
|
10
|
+
the shipped retry class plus fail-open boundary out of `cycleHardening.ts` and
|
|
11
|
+
its exact unit/parity tests.
|
|
12
|
+
|
|
13
|
+
All `src/subscription/` paths and unprefixed commands in this document are
|
|
14
|
+
relative to this package root.
|
|
15
|
+
|
|
16
|
+
`src/subscription/` is the engine. Nothing else in the platform decides when a
|
|
17
|
+
subscription's next cycle falls, whether an edit window is still open, or what a
|
|
18
|
+
recorded payment does to the schedule. A host application that re-exports these
|
|
19
|
+
functions is a caller, not a second engine — see **Consuming it from a host
|
|
20
|
+
application** at the end.
|
|
21
|
+
|
|
22
|
+
## What this package owns
|
|
23
|
+
|
|
24
|
+
Eight files under `src/subscription/`, one barrel (`index.ts`), four groups of
|
|
25
|
+
exports.
|
|
26
|
+
|
|
27
|
+
| Group | File | Exports |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| Cycle planning | `subscriptionEngineCore.ts` | `createInitialSubscriptionCheckoutModel`, `planSubscriptionCycle`, `isRenewalDue`, `findDueSubscriptions`, `isEditWindowOpen`, `editCutoffAt` |
|
|
30
|
+
| Lifecycle transitions | `subscriptionEngineLifecycle.ts` | `SUBSCRIPTION_STATUS_TRANSITIONS`, `canTransitionSubscriptionStatus`, `pauseSubscription`, `resumeSubscription`, `cancelSubscription`, `completeSubscription`, `reactivateSubscription`, `pauseSubscriptionForExpiredDunning`, `resumeSubscriptionFromExpiredDunning`, `RESTART_LEAD_DAYS`, `skipNextCycle`, `slideNextCycle`, `swapTemplateLine`, `addPermanentAddon` |
|
|
31
|
+
| Payment recording | `subscriptionEnginePayment.ts` | `recordPaymentSuccess`, `recordPaymentFailure` |
|
|
32
|
+
| Retry ladder | `cycleHardening.ts` | `DEFAULT_CYCLE_RETRY_CADENCE`, `nextRetryAttemptAt`, `shouldScheduleRetry`, `maxRetryAttempts`, `ladderTerminatedByClass`, `isDunningLadderExhausted` |
|
|
33
|
+
|
|
34
|
+
`subscriptionEngineTypes.ts` and `types.ts` carry the shapes
|
|
35
|
+
(`EngineSubscription`, `EngineCycle`, `EngineEvent`, the status unions, the
|
|
36
|
+
template snapshot) and `subscriptionEngineUtils.ts` the small pure helpers the
|
|
37
|
+
groups above share (`addCadenceDays`, `parseIso`, `validateTemplate`,
|
|
38
|
+
`isTerminalCycle`, `touchSubscription`, `makeEvent`, `success`, `failure`, …).
|
|
39
|
+
|
|
40
|
+
Every mutating function returns the same discriminated result rather than
|
|
41
|
+
throwing:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
type SubscriptionEngineResult<T> =
|
|
45
|
+
| { ok: true; value: T }
|
|
46
|
+
| { ok: false; error: { code: SubscriptionEngineErrorCode; message: string } };
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`SubscriptionEngineErrorCode` is a closed union of eleven neutral codes
|
|
50
|
+
(`invalid_timestamp`, `invalid_template`, `inactive_subscription`,
|
|
51
|
+
`edit_window_closed`, `line_not_found`, `duplicate_line`,
|
|
52
|
+
`invalid_slide_target`, `invalid_transition`, `terminal_cycle`,
|
|
53
|
+
`missing_payment_method`, `dunning_not_exhausted`). Refusals are
|
|
54
|
+
values; add a code rather than an exception.
|
|
55
|
+
|
|
56
|
+
Mutations also return the events they imply (`EngineEvent[]`), each already
|
|
57
|
+
carrying an `idempotencyKey` derived from the cycle's own
|
|
58
|
+
`engineIdempotencyKey`. The engine does not publish them — the host writes them
|
|
59
|
+
to whatever outbox it has, and the stable key is what makes that write
|
|
60
|
+
replay-safe.
|
|
61
|
+
|
|
62
|
+
## No I/O, and no ambient clock
|
|
63
|
+
|
|
64
|
+
Two rules make `src/subscription/` portable, and both are mechanical rather than
|
|
65
|
+
aspirational.
|
|
66
|
+
|
|
67
|
+
1. **No I/O.** No database handle, no HTTP client, no filesystem, no scheduler,
|
|
68
|
+
no settlement adapter. The only import that leaves `src/subscription/` is
|
|
69
|
+
the sibling `paymentFailureTaxonomyContracts` module, another pure kernel.
|
|
70
|
+
2. **No ambient clock.** There is no `Date.now()` and no `new Date()` without an
|
|
71
|
+
argument anywhere in these eight files. Every function that needs the current
|
|
72
|
+
instant takes it as an ISO-8601 string parameter (`now`, `recordedAt`,
|
|
73
|
+
`failedAt`, `firstCycleAt`). `grep -rn "Date.now\|new Date()" src/subscription`
|
|
74
|
+
returns nothing, and it must keep returning nothing.
|
|
75
|
+
|
|
76
|
+
The consequence is that the same inputs always produce the same outputs, so the
|
|
77
|
+
whole engine is testable without a fake clock, a container, or a network.
|
|
78
|
+
|
|
79
|
+
`DEFAULT_TIMEZONE` here is `"UTC"`. The engine does not know which market it
|
|
80
|
+
serves; the composition root names the zone. A caller that has a market zone
|
|
81
|
+
should pass it explicitly rather than rely on the default.
|
|
82
|
+
|
|
83
|
+
## The deterministic-clock contract on `recordPaymentSuccess`
|
|
84
|
+
|
|
85
|
+
`recordPaymentSuccess` takes **two** instants, and both are required:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
recordPaymentSuccess({ subscription, cycle, paidAt, recordedAt })
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- `paidAt` is when the money moved, as the settlement service reports it.
|
|
92
|
+
- `recordedAt` is when this deployment observed that fact. The caller supplies
|
|
93
|
+
it; the engine has no clock of its own to fall back on.
|
|
94
|
+
|
|
95
|
+
Lateness is measured to the **earlier of the two**. A settlement timestamp from
|
|
96
|
+
the future therefore cannot inflate the schedule shift and push the next charge
|
|
97
|
+
out past what the subscriber actually owes, and a delayed observation cannot
|
|
98
|
+
shorten it either. Both instants must parse, or the call answers
|
|
99
|
+
`invalid_timestamp`.
|
|
100
|
+
|
|
101
|
+
## A late payment shifts the cycle; it never compresses it
|
|
102
|
+
|
|
103
|
+
This is the load-bearing rule of the whole engine, and the one most likely to be
|
|
104
|
+
"optimised" away by someone who reads it as defensive arithmetic.
|
|
105
|
+
|
|
106
|
+
The next cycle is computed from the **cycle's own** `scheduledAt` plus its
|
|
107
|
+
cadence — not from the moment the money arrived. A late payment must not silently
|
|
108
|
+
shorten the interval the subscriber bought. The whole days the payment was late
|
|
109
|
+
are then **added**:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
shiftedByDays = max(0, floor((min(paidAt, recordedAt) - cycle.scheduledAt) / 1 day))
|
|
113
|
+
nextCycleAt = max(subscription.nextCycleAt, onTimeNextCycleAt + shiftedByDays days)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The outer `max` is a **monotonic clamp**: `nextCycleAt` may only ever move later,
|
|
117
|
+
never earlier. Without it a replayed webhook, an out-of-order settlement, or a
|
|
118
|
+
correction applied against an older cycle could compute an earlier instant and
|
|
119
|
+
pull a charge forward into a window the subscriber has already been billed for.
|
|
120
|
+
That is the one failure mode this rail must never have. A deployment whose
|
|
121
|
+
durable schedule enforces the same rule in SQL will spell the clamp `GREATEST`;
|
|
122
|
+
the two are one rule with two spellings, and they must move together.
|
|
123
|
+
|
|
124
|
+
`shiftedByDays` is reported on the emitted
|
|
125
|
+
`subscription.next_cycle_at_updated` event alongside `basedOnScheduledAt` and
|
|
126
|
+
`basedOnPaidAt`, so the shift is auditable after the fact rather than inferred.
|
|
127
|
+
|
|
128
|
+
A shift that pushes the instant past the representable timestamp range answers
|
|
129
|
+
`invalid_timestamp` rather than returning an invalid date.
|
|
130
|
+
|
|
131
|
+
## The retry ladder terminates; it does not cap
|
|
132
|
+
|
|
133
|
+
`cycleHardening.ts` is the single authority on what a refused charge does next.
|
|
134
|
+
`nextRetryAttemptAt` answers the instant of the next attempt, or `null` when
|
|
135
|
+
there is none. `DEFAULT_CYCLE_RETRY_CADENCE` ships `backoffHours: [24, 72, 168]`
|
|
136
|
+
— one day, three days, seven days — frozen so nothing can mutate the shipped
|
|
137
|
+
ladder in place, and injectable so a deployment can publish its own.
|
|
138
|
+
|
|
139
|
+
Asking for the next slot **is** the decision. `null` means only that no next
|
|
140
|
+
retry is scheduled; it is a pause signal only after the rung budget is
|
|
141
|
+
exhausted. Class termination before exhaustion leaves the subscription active
|
|
142
|
+
and must not be treated as exhaustion. The function never answers "the last
|
|
143
|
+
slot again". Callers must not add their own
|
|
144
|
+
`attempt <= maxRetryAttempts()` guard — a second copy of the budget check is
|
|
145
|
+
exactly how two rails drift apart.
|
|
146
|
+
|
|
147
|
+
`terminatingFailureClasses` lets a refusal's *class*, rather than the rung it
|
|
148
|
+
reached, end the ladder. `DEFAULT_CYCLE_RETRY_CADENCE` ships exactly one member:
|
|
149
|
+
`["hard_do_not_retry"]`. That class says the issuer refused the instrument, not
|
|
150
|
+
only this charge, so no retry rung can change the answer; every other shipped
|
|
151
|
+
failure class keeps the full ladder.
|
|
152
|
+
|
|
153
|
+
`ladderTerminatedByClass` fails open at every configuration uncertainty. An
|
|
154
|
+
absent or unlisted class does not terminate. A class explicitly listed by a
|
|
155
|
+
custom cadence still does not terminate when the payment taxonomy does not
|
|
156
|
+
recognise it, or when `failureClassDecision` says retry remains allowed. Only a
|
|
157
|
+
listed, recognised class that the decision table already refuses can shorten
|
|
158
|
+
the ladder. The SQL retry rail publishes the same singleton, and the parity test
|
|
159
|
+
fails if those two spellings drift.
|
|
160
|
+
|
|
161
|
+
## The status matrix, and the one non-payment pause
|
|
162
|
+
|
|
163
|
+
`SUBSCRIPTION_STATUS_TRANSITIONS` is the whole status matrix, frozen:
|
|
164
|
+
|
|
165
|
+
| From | To |
|
|
166
|
+
| --- | --- |
|
|
167
|
+
| `active` | `paused`, `cancelled`, `completed` |
|
|
168
|
+
| `paused` | `active`, `cancelled` |
|
|
169
|
+
| `cancelled` | `active` |
|
|
170
|
+
| `completed` | none; terminal |
|
|
171
|
+
|
|
172
|
+
It mirrors the managed SQL guard `public.subscription_guard_status_transition`
|
|
173
|
+
over the engine's four statuses. The guard's remaining edges belong to the
|
|
174
|
+
activation flow, which this engine never drives. `canTransitionSubscriptionStatus`
|
|
175
|
+
answers from the matrix and nothing else.
|
|
176
|
+
|
|
177
|
+
Each operation takes only its own edge. `resumeSubscription` leaves `paused`
|
|
178
|
+
only. `cancelled` -> `active` is the win-back, and only `reactivateSubscription`
|
|
179
|
+
takes it. It needs a stored payment method, because a win-back charges again,
|
|
180
|
+
and it restarts the schedule. The managed SQL `reactivate` action also clears
|
|
181
|
+
the end instant and cancellation reason, requires the customer's confirmation of
|
|
182
|
+
the charge timing, refuses while a cycle is locked and bumps the template
|
|
183
|
+
version. Those durable duties stay with the host.
|
|
184
|
+
|
|
185
|
+
**The sanctioned non-payment rule.** Nothing in this engine suspends an active
|
|
186
|
+
subscription for a reason its owner did not choose, except a renewal that
|
|
187
|
+
exhausted the retry ladder. `isDunningLadderExhausted(cycle, cadence?)` holds
|
|
188
|
+
only when the cycle is `payment_failed`, has no `nextRetryAt`, and its
|
|
189
|
+
`retryAttempt` is past `maxRetryAttempts(cadence)`. A deployment with its own
|
|
190
|
+
ladder passes the same cadence to `recordPaymentFailure` and to both dunning
|
|
191
|
+
functions, or they disagree about exhaustion. Both halves are required: a
|
|
192
|
+
class-terminated refusal on rung one also leaves nothing scheduled, and it must
|
|
193
|
+
leave the subscription active. `pauseSubscriptionForExpiredDunning` pauses an
|
|
194
|
+
active subscription when that predicate holds. It records `subscription.paused`
|
|
195
|
+
with reason `payment_failed_expired` and never touches `nextCycleAt`. It refuses
|
|
196
|
+
every other cycle as `dunning_not_exhausted`, and returns a subscription that is
|
|
197
|
+
not active unchanged with no event, so a replay is harmless.
|
|
198
|
+
|
|
199
|
+
`resumeSubscriptionFromExpiredDunning` is the customer's recovery. It needs a
|
|
200
|
+
paused subscription, the exhausted cycle and a payment method, stored or
|
|
201
|
+
supplied. The given cycle becomes `skipped` rather than being charged late.
|
|
202
|
+
Whether the method is chargeable unattended is the host's check, and so is
|
|
203
|
+
skipping any other uncollected cycle the host holds; the SQL boundary does both.
|
|
204
|
+
A subscription this rule paused must not go through plain `resumeSubscription`,
|
|
205
|
+
which neither skips the cycle nor moves a stale `nextCycleAt`.
|
|
206
|
+
|
|
207
|
+
Both restarts put `nextCycleAt` `RESTART_LEAD_DAYS` (two) days out, but never
|
|
208
|
+
earlier than the instant already stored. The monotonic clamp on
|
|
209
|
+
`recordPaymentSuccess` applies here too: a restart cannot pull a charge into a
|
|
210
|
+
window already paid for. The managed SQL restarts assign `now + 2 days` without
|
|
211
|
+
the clamp, so the two differ exactly when the stored instant is later than
|
|
212
|
+
that. Examples are a win-back of a subscription paid through a later date, or a
|
|
213
|
+
schedule skipped or slid while the ladder was still running. There the SQL side
|
|
214
|
+
charges earlier than the engine would.
|
|
215
|
+
|
|
216
|
+
## Consuming it from a host application
|
|
217
|
+
|
|
218
|
+
A host application should import from the package entry point and keep its own
|
|
219
|
+
module a **re-export shim**:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
// host/src/subscription/subscriptionEnginePayment.ts
|
|
223
|
+
export { recordPaymentSuccess, recordPaymentFailure } from "@openlup/core/subscription";
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The shim exists so host-side call sites keep a stable local import path while the
|
|
227
|
+
engine lives here. The rule is one line long: **never add logic to a shim.**
|
|
228
|
+
A behaviour change belongs in `src/subscription/`, where it is covered by the
|
|
229
|
+
pure tests and shared by every consumer. A shim may narrow a type or require a
|
|
230
|
+
parameter the engine defaults (making a market timezone mandatory, for example),
|
|
231
|
+
and it must say so in a comment; anything beyond that is a second engine.
|
|
232
|
+
|
|
233
|
+
What the host still owns: persistence, the outbox write, settlement execution and
|
|
234
|
+
mandate storage, scheduling, notification copy, and the clock it passes in. This
|
|
235
|
+
package owns none of them and must never acquire one.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openlup/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Neutral D2C subscription and bundle commerce kernels; a development preview with no stable API.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"files": [
|
|
23
23
|
"dist/**",
|
|
24
24
|
"src/**",
|
|
25
|
+
"AGENTS.md",
|
|
25
26
|
"CHANGELOG.md",
|
|
26
27
|
"CODE_OF_CONDUCT.md",
|
|
27
28
|
"CONTRIBUTING.md",
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
"MAINTAINERS.md",
|
|
30
31
|
"README.md",
|
|
31
32
|
"SECURITY.md",
|
|
33
|
+
"docs/SUBSCRIPTION_ENGINE.md",
|
|
32
34
|
"release-gates.json"
|
|
33
35
|
],
|
|
34
36
|
"scripts": {
|
|
@@ -74,16 +76,6 @@
|
|
|
74
76
|
"import": "./dist/bundle/contracts.js",
|
|
75
77
|
"default": "./dist/bundle/contracts.js"
|
|
76
78
|
},
|
|
77
|
-
"./checkout": {
|
|
78
|
-
"core-source": "./src/checkout/index.ts",
|
|
79
|
-
"types": "./dist/checkout/index.d.ts",
|
|
80
|
-
"module": "./dist/checkout/index.js",
|
|
81
|
-
"node": "./dist/checkout/index.js",
|
|
82
|
-
"development": "./dist/checkout/index.js",
|
|
83
|
-
"production": "./dist/checkout/index.js",
|
|
84
|
-
"import": "./dist/checkout/index.js",
|
|
85
|
-
"default": "./dist/checkout/index.js"
|
|
86
|
-
},
|
|
87
79
|
"./company-identity": {
|
|
88
80
|
"core-source": "./src/company-identity/index.ts",
|
|
89
81
|
"types": "./dist/company-identity/index.d.ts",
|
|
@@ -223,6 +215,36 @@
|
|
|
223
215
|
"production": "./dist/testing.js",
|
|
224
216
|
"import": "./dist/testing.js",
|
|
225
217
|
"default": "./dist/testing.js"
|
|
218
|
+
},
|
|
219
|
+
"./outbox": {
|
|
220
|
+
"core-source": "./src/outbox/index.ts",
|
|
221
|
+
"types": "./dist/outbox/index.d.ts",
|
|
222
|
+
"module": "./dist/outbox/index.js",
|
|
223
|
+
"node": "./dist/outbox/index.js",
|
|
224
|
+
"development": "./dist/outbox/index.js",
|
|
225
|
+
"production": "./dist/outbox/index.js",
|
|
226
|
+
"import": "./dist/outbox/index.js",
|
|
227
|
+
"default": "./dist/outbox/index.js"
|
|
228
|
+
},
|
|
229
|
+
"./readiness": {
|
|
230
|
+
"core-source": "./src/readiness/index.ts",
|
|
231
|
+
"types": "./dist/readiness/index.d.ts",
|
|
232
|
+
"module": "./dist/readiness/index.js",
|
|
233
|
+
"node": "./dist/readiness/index.js",
|
|
234
|
+
"development": "./dist/readiness/index.js",
|
|
235
|
+
"production": "./dist/readiness/index.js",
|
|
236
|
+
"import": "./dist/readiness/index.js",
|
|
237
|
+
"default": "./dist/readiness/index.js"
|
|
238
|
+
},
|
|
239
|
+
"./standard-schema": {
|
|
240
|
+
"core-source": "./src/standard-schema/index.ts",
|
|
241
|
+
"types": "./dist/standard-schema/index.d.ts",
|
|
242
|
+
"module": "./dist/standard-schema/index.js",
|
|
243
|
+
"node": "./dist/standard-schema/index.js",
|
|
244
|
+
"development": "./dist/standard-schema/index.js",
|
|
245
|
+
"production": "./dist/standard-schema/index.js",
|
|
246
|
+
"import": "./dist/standard-schema/index.js",
|
|
247
|
+
"default": "./dist/standard-schema/index.js"
|
|
226
248
|
}
|
|
227
249
|
}
|
|
228
250
|
}
|