@oxygen-agent/cli 1.377.3 → 1.591.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/column-run-notices.d.ts +11 -0
- package/dist/column-run-notices.js +37 -0
- package/dist/command-manifest.js +13 -8
- package/dist/help.js +79 -16
- package/dist/index.js +3812 -460
- package/dist/skills.js +106 -1
- package/node_modules/@oxygen/formula/dist/coerce.d.ts +8 -0
- package/node_modules/@oxygen/formula/dist/coerce.js +10 -0
- package/node_modules/@oxygen/formula/dist/evaluate.d.ts +31 -0
- package/node_modules/@oxygen/formula/dist/evaluate.js +248 -0
- package/node_modules/@oxygen/formula/dist/expression.d.ts +64 -0
- package/node_modules/@oxygen/formula/dist/expression.js +428 -0
- package/node_modules/@oxygen/formula/dist/formula-functions.d.ts +71 -0
- package/node_modules/@oxygen/formula/dist/formula-functions.js +1100 -0
- package/node_modules/@oxygen/formula/dist/index.d.ts +17 -0
- package/node_modules/@oxygen/formula/dist/index.js +17 -0
- package/node_modules/@oxygen/formula/dist/value-normalizers.d.ts +30 -0
- package/node_modules/@oxygen/formula/dist/value-normalizers.js +80 -0
- package/node_modules/@oxygen/formula/package.json +26 -0
- package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +30 -0
- package/node_modules/@oxygen/recipe-sdk/dist/index.js +2 -2
- package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +60 -0
- package/node_modules/@oxygen/shared/dist/billing-anchors.js +135 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +99 -5
- package/node_modules/@oxygen/shared/dist/billing.js +185 -8
- package/node_modules/@oxygen/shared/dist/call-outcomes.d.ts +59 -0
- package/node_modules/@oxygen/shared/dist/call-outcomes.js +73 -0
- package/node_modules/@oxygen/shared/dist/cli-result.js +1 -0
- package/node_modules/@oxygen/shared/dist/credit-guidance.js +3 -1
- package/node_modules/@oxygen/shared/dist/crm-reply-events.d.ts +35 -0
- package/node_modules/@oxygen/shared/dist/crm-reply-events.js +31 -0
- package/node_modules/@oxygen/shared/dist/dial-guardrail-overrides.d.ts +50 -0
- package/node_modules/@oxygen/shared/dist/dial-guardrail-overrides.js +65 -0
- package/node_modules/@oxygen/shared/dist/directory.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/directory.js +1 -0
- package/node_modules/@oxygen/shared/dist/file-import.js +58 -11
- package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +19 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/index.js +9 -0
- package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.d.ts +31 -0
- package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.js +56 -0
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +5 -4
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +5 -4
- package/node_modules/@oxygen/shared/dist/linkedin-url.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/linkedin-url.js +7 -4
- package/node_modules/@oxygen/shared/dist/log.js +41 -2
- package/node_modules/@oxygen/shared/dist/microsoft-consent-url.d.ts +7 -0
- package/node_modules/@oxygen/shared/dist/microsoft-consent-url.js +29 -0
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +31 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +61 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +636 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.js +199 -0
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +89 -23
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +88 -24
- package/node_modules/@oxygen/shared/dist/sequence-crm-events.d.ts +291 -0
- package/node_modules/@oxygen/shared/dist/sequence-crm-events.js +224 -0
- package/node_modules/@oxygen/shared/dist/sequence-template.d.ts +42 -1
- package/node_modules/@oxygen/shared/dist/sequence-template.js +0 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +287 -24
- package/node_modules/@oxygen/shared/dist/sequences.js +940 -60
- package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +70 -0
- package/node_modules/@oxygen/shared/dist/spend-safety.js +106 -0
- package/node_modules/@oxygen/shared/dist/tags.d.ts +90 -1
- package/node_modules/@oxygen/shared/dist/tags.js +122 -6
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.js +4 -0
- package/node_modules/@oxygen/shared/package.json +95 -0
- package/node_modules/@oxygen/workflows/dist/event-dispatch.d.ts +126 -0
- package/node_modules/@oxygen/workflows/dist/event-dispatch.js +173 -0
- package/node_modules/@oxygen/workflows/dist/graph/expression.d.ts +78 -0
- package/node_modules/@oxygen/workflows/dist/graph/expression.js +700 -0
- package/node_modules/@oxygen/workflows/dist/graph/index.d.ts +20 -0
- package/node_modules/@oxygen/workflows/dist/graph/index.js +20 -0
- package/node_modules/@oxygen/workflows/dist/graph/lint.d.ts +4 -0
- package/node_modules/@oxygen/workflows/dist/graph/lint.js +812 -0
- package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +501 -0
- package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.js +200 -0
- package/node_modules/@oxygen/workflows/dist/graph/params.d.ts +86 -0
- package/node_modules/@oxygen/workflows/dist/graph/params.js +173 -0
- package/node_modules/@oxygen/workflows/dist/graph/remap.d.ts +48 -0
- package/node_modules/@oxygen/workflows/dist/graph/remap.js +213 -0
- package/node_modules/@oxygen/workflows/dist/graph/topology.d.ts +46 -0
- package/node_modules/@oxygen/workflows/dist/graph/topology.js +280 -0
- package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +270 -0
- package/node_modules/@oxygen/workflows/dist/graph/types.js +93 -0
- package/node_modules/@oxygen/workflows/dist/index.d.ts +113 -1
- package/node_modules/@oxygen/workflows/dist/index.js +179 -13
- package/node_modules/@oxygen/workflows/dist/tool-effects.d.ts +1 -0
- package/node_modules/@oxygen/workflows/dist/tool-effects.js +19 -0
- package/node_modules/@oxygen/workflows/dist/usage-estimate.js +135 -4
- package/node_modules/@oxygen/workflows/package.json +4 -0
- package/package.json +7 -5
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @oxygen/formula — the OXYGEN expression language, host-free.
|
|
3
|
+
*
|
|
4
|
+
* Tokenizer, parser, static analysis, function registry, and a scope-driven
|
|
5
|
+
* evaluator with no node builtins and no dependency beyond the dependency-free
|
|
6
|
+
* `@oxygen/shared/cli-result` leaf (for `OxygenError`) — so the exact semantics
|
|
7
|
+
* that run a formula column in the worker also run in the browser (formula
|
|
8
|
+
* editor previews, the visual workflow editor).
|
|
9
|
+
*
|
|
10
|
+
* Hosts bind identifiers by implementing `FormulaScope.resolve` —
|
|
11
|
+
* `@oxygen/tenant-db` binds them to a workspace table row.
|
|
12
|
+
*/
|
|
13
|
+
export { isRecord } from "./coerce.js";
|
|
14
|
+
export * from "./evaluate.js";
|
|
15
|
+
export * from "./expression.js";
|
|
16
|
+
export * from "./formula-functions.js";
|
|
17
|
+
export * from "./value-normalizers.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @oxygen/formula — the OXYGEN expression language, host-free.
|
|
3
|
+
*
|
|
4
|
+
* Tokenizer, parser, static analysis, function registry, and a scope-driven
|
|
5
|
+
* evaluator with no node builtins and no dependency beyond the dependency-free
|
|
6
|
+
* `@oxygen/shared/cli-result` leaf (for `OxygenError`) — so the exact semantics
|
|
7
|
+
* that run a formula column in the worker also run in the browser (formula
|
|
8
|
+
* editor previews, the visual workflow editor).
|
|
9
|
+
*
|
|
10
|
+
* Hosts bind identifiers by implementing `FormulaScope.resolve` —
|
|
11
|
+
* `@oxygen/tenant-db` binds them to a workspace table row.
|
|
12
|
+
*/
|
|
13
|
+
export { isRecord } from "./coerce.js";
|
|
14
|
+
export * from "./evaluate.js";
|
|
15
|
+
export * from "./expression.js";
|
|
16
|
+
export * from "./formula-functions.js";
|
|
17
|
+
export * from "./value-normalizers.js";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical value normalizers shared by every subsystem that compares
|
|
3
|
+
* identity-shaped values: CRM identity resolution, the formula function
|
|
4
|
+
* registry, row dedupe, and Records↔Tables binding. One source of truth so
|
|
5
|
+
* "does foo@Bar.com match FOO@bar.com" answers the same everywhere — including
|
|
6
|
+
* in the browser. Pure string functions — no I/O, no tenant context.
|
|
7
|
+
*/
|
|
8
|
+
export declare const VALUE_NORMALIZATIONS: readonly ["exact_text_v1", "lower_trim_v1", "email_v1", "domain_v1", "linkedin_url_v1", "uuid_v1"];
|
|
9
|
+
export type ValueNormalization = (typeof VALUE_NORMALIZATIONS)[number];
|
|
10
|
+
export declare function isValueNormalization(value: unknown): value is ValueNormalization;
|
|
11
|
+
/** `email_v1`: case-insensitive mailbox match. */
|
|
12
|
+
export declare function normalizeEmail(value: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* `domain_v1`: reduce a URL, hostname, or bare domain to its canonical
|
|
15
|
+
* registrable form — lowercased hostname without `www.` or trailing dots.
|
|
16
|
+
*/
|
|
17
|
+
export declare function normalizeDomain(value: string): string;
|
|
18
|
+
/** `linkedin_url_v1`: trailing-slash-insensitive, case-insensitive profile URL match. */
|
|
19
|
+
export declare function normalizeLinkedinUrl(value: string): string;
|
|
20
|
+
/** `uuid_v1`: case-insensitive UUID match. */
|
|
21
|
+
export declare function normalizeUuid(value: string): string;
|
|
22
|
+
/** `exact_text_v1`: whitespace-trimmed verbatim match. */
|
|
23
|
+
export declare function normalizeExactText(value: string): string;
|
|
24
|
+
/** `lower_trim_v1`: the default loose text match (trimmed, case-insensitive). */
|
|
25
|
+
export declare function normalizeLowerTrim(value: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Apply a named normalization. Unknown names fall back to `exact_text_v1`
|
|
28
|
+
* semantics, matching the historical CRM identity behavior.
|
|
29
|
+
*/
|
|
30
|
+
export declare function normalizeValue(normalization: string, value: string): string;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical value normalizers shared by every subsystem that compares
|
|
3
|
+
* identity-shaped values: CRM identity resolution, the formula function
|
|
4
|
+
* registry, row dedupe, and Records↔Tables binding. One source of truth so
|
|
5
|
+
* "does foo@Bar.com match FOO@bar.com" answers the same everywhere — including
|
|
6
|
+
* in the browser. Pure string functions — no I/O, no tenant context.
|
|
7
|
+
*/
|
|
8
|
+
export const VALUE_NORMALIZATIONS = [
|
|
9
|
+
"exact_text_v1",
|
|
10
|
+
"lower_trim_v1",
|
|
11
|
+
"email_v1",
|
|
12
|
+
"domain_v1",
|
|
13
|
+
"linkedin_url_v1",
|
|
14
|
+
"uuid_v1",
|
|
15
|
+
];
|
|
16
|
+
export function isValueNormalization(value) {
|
|
17
|
+
return typeof value === "string" && VALUE_NORMALIZATIONS.includes(value);
|
|
18
|
+
}
|
|
19
|
+
/** `email_v1`: case-insensitive mailbox match. */
|
|
20
|
+
export function normalizeEmail(value) {
|
|
21
|
+
return value.trim().toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* `domain_v1`: reduce a URL, hostname, or bare domain to its canonical
|
|
25
|
+
* registrable form — lowercased hostname without `www.` or trailing dots.
|
|
26
|
+
*/
|
|
27
|
+
export function normalizeDomain(value) {
|
|
28
|
+
const trimmed = value.trim().toLowerCase();
|
|
29
|
+
if (!trimmed)
|
|
30
|
+
return "";
|
|
31
|
+
try {
|
|
32
|
+
const parsed = new URL(trimmed.includes("://") ? trimmed : `https://${trimmed}`);
|
|
33
|
+
return parsed.hostname.replace(/^www\./, "").replace(/\.+$/, "");
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return trimmed
|
|
37
|
+
.replace(/^https?:\/\//, "")
|
|
38
|
+
.split("/")[0]
|
|
39
|
+
?.replace(/^www\./, "")
|
|
40
|
+
.replace(/\.+$/, "")
|
|
41
|
+
?? "";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** `linkedin_url_v1`: trailing-slash-insensitive, case-insensitive profile URL match. */
|
|
45
|
+
export function normalizeLinkedinUrl(value) {
|
|
46
|
+
return value.trim().replace(/\/+$/, "").toLowerCase();
|
|
47
|
+
}
|
|
48
|
+
/** `uuid_v1`: case-insensitive UUID match. */
|
|
49
|
+
export function normalizeUuid(value) {
|
|
50
|
+
return value.trim().toLowerCase();
|
|
51
|
+
}
|
|
52
|
+
/** `exact_text_v1`: whitespace-trimmed verbatim match. */
|
|
53
|
+
export function normalizeExactText(value) {
|
|
54
|
+
return value.trim();
|
|
55
|
+
}
|
|
56
|
+
/** `lower_trim_v1`: the default loose text match (trimmed, case-insensitive). */
|
|
57
|
+
export function normalizeLowerTrim(value) {
|
|
58
|
+
return value.trim().toLowerCase();
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Apply a named normalization. Unknown names fall back to `exact_text_v1`
|
|
62
|
+
* semantics, matching the historical CRM identity behavior.
|
|
63
|
+
*/
|
|
64
|
+
export function normalizeValue(normalization, value) {
|
|
65
|
+
switch (normalization) {
|
|
66
|
+
case "email_v1":
|
|
67
|
+
return normalizeEmail(value);
|
|
68
|
+
case "domain_v1":
|
|
69
|
+
return normalizeDomain(value);
|
|
70
|
+
case "linkedin_url_v1":
|
|
71
|
+
return normalizeLinkedinUrl(value);
|
|
72
|
+
case "uuid_v1":
|
|
73
|
+
return normalizeUuid(value);
|
|
74
|
+
case "lower_trim_v1":
|
|
75
|
+
return normalizeLowerTrim(value);
|
|
76
|
+
case "exact_text_v1":
|
|
77
|
+
default:
|
|
78
|
+
return normalizeExactText(value);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oxygen/formula",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./functions": {
|
|
15
|
+
"types": "./dist/formula-functions.d.ts",
|
|
16
|
+
"import": "./dist/formula-functions.js",
|
|
17
|
+
"default": "./dist/formula-functions.js"
|
|
18
|
+
},
|
|
19
|
+
"./value-normalizers": {
|
|
20
|
+
"types": "./dist/value-normalizers.d.ts",
|
|
21
|
+
"import": "./dist/value-normalizers.js",
|
|
22
|
+
"default": "./dist/value-normalizers.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {}
|
|
26
|
+
}
|
|
@@ -130,6 +130,19 @@ export type RecipeStepOptions<T> = {
|
|
|
130
130
|
effect?: WorkflowStepEffect;
|
|
131
131
|
run: () => T | Promise<T>;
|
|
132
132
|
};
|
|
133
|
+
export type RecipeWaitOptions = {
|
|
134
|
+
/** Relative deadline. Exactly one of `seconds` or `until` is required. */
|
|
135
|
+
seconds?: number;
|
|
136
|
+
/** Absolute deadline as an ISO-8601 instant. */
|
|
137
|
+
until?: string;
|
|
138
|
+
};
|
|
139
|
+
export type RecipeWaitResult = {
|
|
140
|
+
waited: boolean;
|
|
141
|
+
/** The deadline this wait resolved against, ISO-8601. */
|
|
142
|
+
resume_at: string;
|
|
143
|
+
/** True when the mode short-circuited the wait instead of parking. */
|
|
144
|
+
simulated?: boolean;
|
|
145
|
+
};
|
|
133
146
|
export type RecipeContext = {
|
|
134
147
|
input: unknown;
|
|
135
148
|
mode: WorkflowMode;
|
|
@@ -143,6 +156,23 @@ export type RecipeContext = {
|
|
|
143
156
|
approvals: RecipeApprovalApi;
|
|
144
157
|
log: (level: RecipeLogLevel, message: string, payload?: Record<string, unknown>) => void;
|
|
145
158
|
step: <T = unknown>(key: string, options: RecipeStepOptions<T>) => Promise<T>;
|
|
159
|
+
/**
|
|
160
|
+
* Park the run until a deadline, durably.
|
|
161
|
+
*
|
|
162
|
+
* NOT a sleep: the run's lease is released and the worker moves on, so a wait
|
|
163
|
+
* of three days costs no compute and survives a deploy or a crash. When the
|
|
164
|
+
* deadline passes the recipe replays from its last checkpoint and this call
|
|
165
|
+
* returns — so everything before it keeps its recorded results and no tool is
|
|
166
|
+
* re-run.
|
|
167
|
+
*
|
|
168
|
+
* The `key` is the checkpoint identity, exactly like ctx.step(): it is what
|
|
169
|
+
* lets a replay recognise a wait it has already served rather than restarting
|
|
170
|
+
* the clock. Reusing a key within one execution is refused.
|
|
171
|
+
*
|
|
172
|
+
* Outside `live` the wait returns immediately — a dry run has no real clock to
|
|
173
|
+
* honour, and blocking a preview for three days would make previews unusable.
|
|
174
|
+
*/
|
|
175
|
+
wait: (key: string, options: RecipeWaitOptions) => Promise<RecipeWaitResult>;
|
|
146
176
|
now: () => Promise<string>;
|
|
147
177
|
uuid: () => Promise<string>;
|
|
148
178
|
};
|
|
@@ -14,8 +14,8 @@ export function defineRecipe(input) {
|
|
|
14
14
|
if (input.runtime !== undefined && input.runtime !== "durable") {
|
|
15
15
|
throw new Error("Recipe runtime must be durable.");
|
|
16
16
|
}
|
|
17
|
-
if (!Array.isArray(input.tools)
|
|
18
|
-
throw new Error("Durable recipes must declare
|
|
17
|
+
if (!Array.isArray(input.tools)) {
|
|
18
|
+
throw new Error("Durable recipes must declare a tools array (use tools: [] for pure recipes).");
|
|
19
19
|
}
|
|
20
20
|
const tools = Array.from(new Set(input.tools.map((tool) => {
|
|
21
21
|
if (typeof tool !== "string" || !tool.trim()) {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The immutable anchor day of a commitment — the UTC day-of-month it was
|
|
3
|
+
* created on. Stored once at creation and never recomputed, so the clamp in
|
|
4
|
+
* {@link nextAnchorDueAt} can always project from the original intent (the 31st)
|
|
5
|
+
* rather than from a previously clamped value (the 28th).
|
|
6
|
+
*/
|
|
7
|
+
export declare function anchorDayOf(anchorAt: Date): number;
|
|
8
|
+
/**
|
|
9
|
+
* The next due instant for a commitment: exactly one calendar month after
|
|
10
|
+
* `periodStart`, on `anchorDay` clamped to the target month's length, preserving
|
|
11
|
+
* `periodStart`'s UTC time-of-day.
|
|
12
|
+
*
|
|
13
|
+
* Time-of-day is preserved so a mailbox connected at 09:14 bills at 09:14 — a
|
|
14
|
+
* commitment created minutes ago must not become due "today at 00:00" and get
|
|
15
|
+
* charged twice in one day by the next sweep tick.
|
|
16
|
+
*
|
|
17
|
+
* `anchorDay` is clamped into 1..31 defensively; a caller passing a value from a
|
|
18
|
+
* corrupted row gets a sane projection instead of an Invalid Date.
|
|
19
|
+
*/
|
|
20
|
+
export declare function nextAnchorDueAt(periodStart: Date, anchorDay: number): Date;
|
|
21
|
+
/**
|
|
22
|
+
* The atomic-claim cursor for one commitment period. Replaces the calendar-month
|
|
23
|
+
* `YYYY-MM` key the other billers use; the shared billMonthlyCreditCycle engine
|
|
24
|
+
* never inspects the key's shape, so the swap is contained here.
|
|
25
|
+
*
|
|
26
|
+
* The period start is the identity (not the due date): a period is claimed once,
|
|
27
|
+
* and stamping the START means a claim written before the anchor advances still
|
|
28
|
+
* names the period that was actually billed.
|
|
29
|
+
*/
|
|
30
|
+
export declare function commitmentCycleKey(commitmentId: string, periodStart: Date): string;
|
|
31
|
+
/**
|
|
32
|
+
* How many times a commitment on `anchorDay` falls due in the half-open window
|
|
33
|
+
* [from, to). Used to size the block for subscription periods longer than a
|
|
34
|
+
* month and to project "what will this cost me before my next renewal" on the
|
|
35
|
+
* commitments surface.
|
|
36
|
+
*
|
|
37
|
+
* Walks period by period rather than dividing elapsed days, because month
|
|
38
|
+
* lengths differ and the clamped anchor is not a fixed stride.
|
|
39
|
+
*/
|
|
40
|
+
export declare function occurrencesInPeriod(from: Date, to: Date, anchorDay: number): number;
|
|
41
|
+
/**
|
|
42
|
+
* How many months of every fixed resource the block must hold, given the
|
|
43
|
+
* subscription period length in days.
|
|
44
|
+
*
|
|
45
|
+
* The block is a STANDING one-month-per-resource reserve (see
|
|
46
|
+
* credit-commitments.ts): because every anchor is at most one month out, holding
|
|
47
|
+
* one month per active resource always covers every fixed charge falling due
|
|
48
|
+
* inside any coming subscription period of <= 1 month. Longer periods (annual
|
|
49
|
+
* plans) need proportionally more held, or the block runs dry mid-term.
|
|
50
|
+
*
|
|
51
|
+
* Returns at least 1 — a missing/zero/garbage period length must never collapse
|
|
52
|
+
* the block to nothing, which would silently disable the whole feature.
|
|
53
|
+
*/
|
|
54
|
+
export declare function blockPeriodsForSubscription(periodDays: number | null | undefined): number;
|
|
55
|
+
/**
|
|
56
|
+
* Days between two instants, floored. Small helper so callers deriving
|
|
57
|
+
* `blockPeriodsForSubscription` from a Stripe period pair do not each reimplement
|
|
58
|
+
* the millisecond math (and get it wrong across DST by using local dates).
|
|
59
|
+
*/
|
|
60
|
+
export declare function periodLengthDays(start: Date | null | undefined, end: Date | null | undefined): number | null;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Rolling per-resource monthly anchors for FIXED (committed) credit charges.
|
|
2
|
+
//
|
|
3
|
+
// Every other recurring biller in Oxygen (managed inboxes, warmup, placement,
|
|
4
|
+
// LinkedIn seats, egress addons) keys its cycle on the CALENDAR month —
|
|
5
|
+
// `YYYY-MM`. That is correct for a rail whose vendor invoices monthly, but it is
|
|
6
|
+
// wrong for a per-resource commitment: a mailbox connected on the 9th should
|
|
7
|
+
// bill on the 9th, and a LinkedIn account connected on the 22nd on the 22nd, so
|
|
8
|
+
// each individual resource rolls its own month independently of the calendar and
|
|
9
|
+
// independently of the subscription period.
|
|
10
|
+
//
|
|
11
|
+
// THE ANCHOR DAY IS STORED SEPARATELY FROM THE LAST DUE DATE, and that is the
|
|
12
|
+
// whole trick. If you derive the next due date from the previous one by "same
|
|
13
|
+
// day next month, clamped", a 31st anchor DECAYS: 31 Jan -> 28 Feb -> 28 Mar ->
|
|
14
|
+
// 28 Apr, and the customer silently drifts three days earlier every year. Keeping
|
|
15
|
+
// the immutable `anchorDay` and re-clamping from it each period gives the Stripe
|
|
16
|
+
// anchor rule instead: 31 Jan -> 28 Feb -> 31 Mar. The clamp is a per-period
|
|
17
|
+
// projection, never a mutation of the anchor.
|
|
18
|
+
//
|
|
19
|
+
// Pure module: no DB, no clock reads beyond the arguments handed in, no
|
|
20
|
+
// dependencies. All arithmetic is UTC — a rolling anchor must not shift when the
|
|
21
|
+
// worker host's local zone crosses DST.
|
|
22
|
+
/** Milliseconds in a day. Local to keep this module dependency-free. */
|
|
23
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
24
|
+
/**
|
|
25
|
+
* Days in a given UTC (year, monthIndex). `Date.UTC(y, m + 1, 0)` is the last
|
|
26
|
+
* day of month `m`, which is exactly the count.
|
|
27
|
+
*/
|
|
28
|
+
function daysInUtcMonth(year, monthIndex) {
|
|
29
|
+
return new Date(Date.UTC(year, monthIndex + 1, 0)).getUTCDate();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The immutable anchor day of a commitment — the UTC day-of-month it was
|
|
33
|
+
* created on. Stored once at creation and never recomputed, so the clamp in
|
|
34
|
+
* {@link nextAnchorDueAt} can always project from the original intent (the 31st)
|
|
35
|
+
* rather than from a previously clamped value (the 28th).
|
|
36
|
+
*/
|
|
37
|
+
export function anchorDayOf(anchorAt) {
|
|
38
|
+
return anchorAt.getUTCDate();
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The next due instant for a commitment: exactly one calendar month after
|
|
42
|
+
* `periodStart`, on `anchorDay` clamped to the target month's length, preserving
|
|
43
|
+
* `periodStart`'s UTC time-of-day.
|
|
44
|
+
*
|
|
45
|
+
* Time-of-day is preserved so a mailbox connected at 09:14 bills at 09:14 — a
|
|
46
|
+
* commitment created minutes ago must not become due "today at 00:00" and get
|
|
47
|
+
* charged twice in one day by the next sweep tick.
|
|
48
|
+
*
|
|
49
|
+
* `anchorDay` is clamped into 1..31 defensively; a caller passing a value from a
|
|
50
|
+
* corrupted row gets a sane projection instead of an Invalid Date.
|
|
51
|
+
*/
|
|
52
|
+
export function nextAnchorDueAt(periodStart, anchorDay) {
|
|
53
|
+
const safeAnchorDay = Math.min(31, Math.max(1, Math.trunc(anchorDay)));
|
|
54
|
+
const year = periodStart.getUTCFullYear();
|
|
55
|
+
const monthIndex = periodStart.getUTCMonth();
|
|
56
|
+
// Normalize the +1 month ourselves rather than letting Date roll it: passing
|
|
57
|
+
// monthIndex 12 to Date.UTC is well-defined, but computing the target's day
|
|
58
|
+
// count needs the normalized (year, month) pair anyway.
|
|
59
|
+
const targetYear = monthIndex === 11 ? year + 1 : year;
|
|
60
|
+
const targetMonthIndex = monthIndex === 11 ? 0 : monthIndex + 1;
|
|
61
|
+
const day = Math.min(safeAnchorDay, daysInUtcMonth(targetYear, targetMonthIndex));
|
|
62
|
+
return new Date(Date.UTC(targetYear, targetMonthIndex, day, periodStart.getUTCHours(), periodStart.getUTCMinutes(), periodStart.getUTCSeconds(), periodStart.getUTCMilliseconds()));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The atomic-claim cursor for one commitment period. Replaces the calendar-month
|
|
66
|
+
* `YYYY-MM` key the other billers use; the shared billMonthlyCreditCycle engine
|
|
67
|
+
* never inspects the key's shape, so the swap is contained here.
|
|
68
|
+
*
|
|
69
|
+
* The period start is the identity (not the due date): a period is claimed once,
|
|
70
|
+
* and stamping the START means a claim written before the anchor advances still
|
|
71
|
+
* names the period that was actually billed.
|
|
72
|
+
*/
|
|
73
|
+
export function commitmentCycleKey(commitmentId, periodStart) {
|
|
74
|
+
return `${commitmentId}:${periodStart.toISOString()}`;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* How many times a commitment on `anchorDay` falls due in the half-open window
|
|
78
|
+
* [from, to). Used to size the block for subscription periods longer than a
|
|
79
|
+
* month and to project "what will this cost me before my next renewal" on the
|
|
80
|
+
* commitments surface.
|
|
81
|
+
*
|
|
82
|
+
* Walks period by period rather than dividing elapsed days, because month
|
|
83
|
+
* lengths differ and the clamped anchor is not a fixed stride.
|
|
84
|
+
*/
|
|
85
|
+
export function occurrencesInPeriod(from, to, anchorDay) {
|
|
86
|
+
if (!(from instanceof Date) || !(to instanceof Date))
|
|
87
|
+
return 0;
|
|
88
|
+
if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime()))
|
|
89
|
+
return 0;
|
|
90
|
+
if (to <= from)
|
|
91
|
+
return 0;
|
|
92
|
+
let count = 0;
|
|
93
|
+
let cursor = from;
|
|
94
|
+
// Hard bound: a caller passing a decade-wide window should get a number, not a
|
|
95
|
+
// hang. 1200 periods is 100 years — far past any real subscription.
|
|
96
|
+
for (let guard = 0; guard < 1200; guard += 1) {
|
|
97
|
+
cursor = nextAnchorDueAt(cursor, anchorDay);
|
|
98
|
+
if (cursor >= to)
|
|
99
|
+
break;
|
|
100
|
+
count += 1;
|
|
101
|
+
}
|
|
102
|
+
return count;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* How many months of every fixed resource the block must hold, given the
|
|
106
|
+
* subscription period length in days.
|
|
107
|
+
*
|
|
108
|
+
* The block is a STANDING one-month-per-resource reserve (see
|
|
109
|
+
* credit-commitments.ts): because every anchor is at most one month out, holding
|
|
110
|
+
* one month per active resource always covers every fixed charge falling due
|
|
111
|
+
* inside any coming subscription period of <= 1 month. Longer periods (annual
|
|
112
|
+
* plans) need proportionally more held, or the block runs dry mid-term.
|
|
113
|
+
*
|
|
114
|
+
* Returns at least 1 — a missing/zero/garbage period length must never collapse
|
|
115
|
+
* the block to nothing, which would silently disable the whole feature.
|
|
116
|
+
*/
|
|
117
|
+
export function blockPeriodsForSubscription(periodDays) {
|
|
118
|
+
if (typeof periodDays !== "number" || !Number.isFinite(periodDays) || periodDays <= 0) {
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
return Math.max(1, Math.ceil(periodDays / 30));
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Days between two instants, floored. Small helper so callers deriving
|
|
125
|
+
* `blockPeriodsForSubscription` from a Stripe period pair do not each reimplement
|
|
126
|
+
* the millisecond math (and get it wrong across DST by using local dates).
|
|
127
|
+
*/
|
|
128
|
+
export function periodLengthDays(start, end) {
|
|
129
|
+
if (!start || !end)
|
|
130
|
+
return null;
|
|
131
|
+
const ms = end.getTime() - start.getTime();
|
|
132
|
+
if (!Number.isFinite(ms) || ms <= 0)
|
|
133
|
+
return null;
|
|
134
|
+
return Math.floor(ms / DAY_MS);
|
|
135
|
+
}
|
|
@@ -35,6 +35,60 @@ export declare const CREDIT_TOPUP_DEFAULT_CREDITS = 20000;
|
|
|
35
35
|
export declare function isValidCreditTopupCredits(credits: number): boolean;
|
|
36
36
|
export declare function creditTopupUsdCents(credits: number): number | null;
|
|
37
37
|
export declare const AUTOMATION_ACTION_CREDITS = 0.01;
|
|
38
|
+
/** Kinds of resource that carry a fixed monthly credit commitment. */
|
|
39
|
+
export declare const CREDIT_COMMITMENT_KINDS: readonly ["sending_mailbox", "managed_mailbox", "mailbox_warmup", "deliverability_unit", "linkedin_account", "whatsapp_account"];
|
|
40
|
+
export type CreditCommitmentKind = (typeof CREDIT_COMMITMENT_KINDS)[number];
|
|
41
|
+
/**
|
|
42
|
+
* Sequencer platform fee per CONNECTED SENDING MAILBOX per month, in credits
|
|
43
|
+
* ($1.00). Charged on EVERY mailbox wired to the sequencer — BYOK/self-connected
|
|
44
|
+
* Gmail and Microsoft inboxes included — and it STACKS on Oxygen-sold mailboxes,
|
|
45
|
+
* which additionally pay their own mailbox/warmup/placement lines.
|
|
46
|
+
*/
|
|
47
|
+
export declare const SENDING_MAILBOX_MONTHLY_CREDITS = 1000;
|
|
48
|
+
/**
|
|
49
|
+
* Oxygen-sold managed mailbox per month, in credits ($3.00). Flat, superseding
|
|
50
|
+
* the dynamic vendor-COGS x 1.25 quote for the recurring mailbox line.
|
|
51
|
+
*/
|
|
52
|
+
export declare const MANAGED_MAILBOX_MONTHLY_CREDITS = 3000;
|
|
53
|
+
/**
|
|
54
|
+
* Grace window after a commitment goes past_due before the owning subsystem may
|
|
55
|
+
* suspend the resource. Notify -> pause -> lapse; never auto-cancel. Aligned with
|
|
56
|
+
* the subscription entitlement grace so a customer never hits two different
|
|
57
|
+
* clocks for the same missed payment.
|
|
58
|
+
*/
|
|
59
|
+
export declare const COMMITMENT_PAST_DUE_GRACE_DAYS = 14;
|
|
60
|
+
/**
|
|
61
|
+
* Reconnect window in which an ENDED commitment resumes instead of starting a new
|
|
62
|
+
* one. The single most important anti-double-charge rule: a Unipile re-auth, a
|
|
63
|
+
* mailbox re-import, or a reconciler blip must not re-charge a full month. Past
|
|
64
|
+
* this window a reconnect is treated as genuinely new and gets a fresh anchor,
|
|
65
|
+
* so a long-dead row can never resurrect and bill a stale period.
|
|
66
|
+
*/
|
|
67
|
+
export declare const COMMITMENT_RESUME_WINDOW_MS: number;
|
|
68
|
+
/**
|
|
69
|
+
* Maximum periods one commitment may catch up in a single sweep tick. A worker
|
|
70
|
+
* outage (or a resurrected row) must never drain a wallet in one lump: past this
|
|
71
|
+
* many overdue periods the biller skips forward, stamps metadata.skipped_periods,
|
|
72
|
+
* and logs. Fail closed TOWARD the customer.
|
|
73
|
+
*/
|
|
74
|
+
export declare const COMMITMENT_MAX_CATCHUP_PERIODS = 3;
|
|
75
|
+
/**
|
|
76
|
+
* Free month granted to resources that already existed when commitments went
|
|
77
|
+
* live. Combined with CREDIT_COMMITMENT_EPOCH_AT (which pins their anchor to
|
|
78
|
+
* go-live rather than their original created_at), this guarantees no existing
|
|
79
|
+
* customer is charged for infrastructure that was free when they connected it.
|
|
80
|
+
*/
|
|
81
|
+
export declare const COMMITMENT_GRACE_DAYS = 30;
|
|
82
|
+
/**
|
|
83
|
+
* Flat ratified monthly price for a commitment kind, in credits — or null when
|
|
84
|
+
* the price is not owned HERE (warmup and deliverability are ratified in
|
|
85
|
+
* @oxygen/control-db; their callers use the integrations readers).
|
|
86
|
+
*
|
|
87
|
+
* Returning null is the fail-closed signal: an unpriced kind creates NO
|
|
88
|
+
* commitment row rather than a zero-credit one, matching the
|
|
89
|
+
* InboxPricingUnsignedError / WarmupPricingUnsignedError doctrine.
|
|
90
|
+
*/
|
|
91
|
+
export declare function ratifiedCommitmentCredits(kind: CreditCommitmentKind): number | null;
|
|
38
92
|
export declare const CREDIT_TOPUP_PACKS: readonly [{
|
|
39
93
|
readonly id: "10";
|
|
40
94
|
readonly usdCents: 1000;
|
|
@@ -64,16 +118,16 @@ export declare const BASE_PRICING_PLANS: {
|
|
|
64
118
|
readonly tier: "free";
|
|
65
119
|
readonly name: "Free";
|
|
66
120
|
readonly monthlyPriceCents: 0;
|
|
67
|
-
readonly monthlyCredits:
|
|
121
|
+
readonly monthlyCredits: 0;
|
|
68
122
|
readonly weeklyCreditsLimit: null;
|
|
69
|
-
readonly rolloverCap:
|
|
123
|
+
readonly rolloverCap: null;
|
|
70
124
|
readonly monthlyAutomationActions: null;
|
|
71
125
|
readonly automationOverageCentsPerMillion: null;
|
|
72
126
|
readonly automationOverageEnabledDefault: false;
|
|
73
127
|
readonly byokEnabled: false;
|
|
74
|
-
readonly description: "
|
|
75
|
-
readonly ctaLabel: "Start
|
|
76
|
-
readonly features: readonly ["
|
|
128
|
+
readonly description: "No active plan. Start a 7-day Starter trial to use OXYGEN — existing credit balances remain spendable.";
|
|
129
|
+
readonly ctaLabel: "Start trial";
|
|
130
|
+
readonly features: readonly ["Existing credits stay spendable", "Read access to your workspace"];
|
|
77
131
|
};
|
|
78
132
|
readonly starter: {
|
|
79
133
|
readonly tier: "starter";
|
|
@@ -185,6 +239,46 @@ export declare function isBillingCurrency(value: string): value is BillingCurren
|
|
|
185
239
|
export declare function normalizeBillingCurrency(value: string | null | undefined): BillingCurrency;
|
|
186
240
|
export declare function getCurrentFreeTierCycleKey(date?: Date): string;
|
|
187
241
|
export declare function getCurrentBillingCycleKey(date?: Date): string;
|
|
242
|
+
/**
|
|
243
|
+
* A monthly Stripe period runs 28-31 days; 35 leaves slack for a proration or a
|
|
244
|
+
* billing-anchor shift without admitting a quarterly or annual period.
|
|
245
|
+
*/
|
|
246
|
+
export declare const CREDIT_CYCLE_MAX_PERIOD_DAYS = 35;
|
|
247
|
+
export type CreditCycleSource = "subscription_period" | "calendar_month";
|
|
248
|
+
export type CreditCycleWindow = {
|
|
249
|
+
start: Date;
|
|
250
|
+
/** Exclusive; may be in the future (the cycle is usually still running). */
|
|
251
|
+
end: Date;
|
|
252
|
+
source: CreditCycleSource;
|
|
253
|
+
/** "July 2026" | "Jun 10 – Jul 10" */
|
|
254
|
+
label: string;
|
|
255
|
+
};
|
|
256
|
+
/**
|
|
257
|
+
* The window the credit-allowance bar measures: "this month's credits", resolved
|
|
258
|
+
* to the boundary at which the plan's grant actually renews.
|
|
259
|
+
*
|
|
260
|
+
* A Stripe-managed monthly subscription renews on its own period boundary, so
|
|
261
|
+
* that is the honest cycle for it. Everything else — off-Stripe custom plans,
|
|
262
|
+
* the free tier, and any non-monthly Stripe period — renews by CALENDAR month,
|
|
263
|
+
* which is the same boundary already used by getCurrentBillingCycleKey(),
|
|
264
|
+
* getCurrentFreeTierCycleKey(), the shared-billing workspace cap, and the
|
|
265
|
+
* automation-actions meter. The fallback is therefore consistent with every
|
|
266
|
+
* other monthly concept in the product rather than an invention.
|
|
267
|
+
*
|
|
268
|
+
* The length guard cuts BOTH ways deliberately. `now - start <= 35d` rejects a
|
|
269
|
+
* stale period a webhook never advanced; `end - start <= 35d` rejects a genuine
|
|
270
|
+
* annual subscription, which would otherwise pass the first check on day 3 and
|
|
271
|
+
* render a 365-day "month".
|
|
272
|
+
*/
|
|
273
|
+
export declare function resolveCreditCycleWindow(input: {
|
|
274
|
+
subscription: {
|
|
275
|
+
currentPeriodStart: Date | null;
|
|
276
|
+
currentPeriodEnd: Date | null;
|
|
277
|
+
/** isOffStripeSubscription(subscription.metadata) — resolved by the caller. */
|
|
278
|
+
offStripe: boolean;
|
|
279
|
+
} | null;
|
|
280
|
+
now?: Date;
|
|
281
|
+
}): CreditCycleWindow;
|
|
188
282
|
export declare function evaluateWeeklyQuota(usedCredits: number, requestedCredits: number, weeklyCreditsLimit: number): {
|
|
189
283
|
usedCredits: number;
|
|
190
284
|
requestedCredits: number;
|