@oxygen-agent/cli 1.1010.650 → 1.1010.905
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/auto-update.d.ts +129 -0
- package/dist/auto-update.js +392 -0
- package/dist/command-manifest.js +15 -1
- package/dist/credentials.d.ts +2 -0
- package/dist/credentials.js +6 -3
- package/dist/functions-commands.js +1 -1
- package/dist/http-client.js +28 -4
- package/dist/inbox-needs-reply-notice.d.ts +12 -0
- package/dist/inbox-needs-reply-notice.js +51 -0
- package/dist/index.js +756 -177
- package/dist/run-wait.d.ts +3 -1
- package/dist/run-wait.js +19 -5
- package/dist/skills.js +48 -22
- package/dist/streamed-file-import.d.ts +58 -0
- package/dist/streamed-file-import.js +115 -0
- package/dist/update.d.ts +29 -0
- package/dist/update.js +62 -16
- package/dist/workflow-plan-limit-notices.d.ts +8 -0
- package/dist/workflow-plan-limit-notices.js +28 -0
- package/node_modules/@oxygen/cli-ugc/dist/commands.js +3 -3
- package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +50 -2
- package/node_modules/@oxygen/shared/dist/billing-anchors.js +94 -2
- package/node_modules/@oxygen/shared/dist/billing.d.ts +247 -37
- package/node_modules/@oxygen/shared/dist/billing.js +418 -45
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +66 -6
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +6 -6
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +6 -6
- package/node_modules/@oxygen/shared/dist/cost-estimate-view.d.ts +50 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate-view.js +90 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +167 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate.js +361 -0
- package/node_modules/@oxygen/shared/dist/credit-gate.d.ts +26 -0
- package/node_modules/@oxygen/shared/dist/credit-gate.js +65 -0
- package/node_modules/@oxygen/shared/dist/email-deliverability-policy.d.ts +51 -0
- package/node_modules/@oxygen/shared/dist/email-deliverability-policy.js +101 -0
- package/node_modules/@oxygen/shared/dist/email-hard-bounce.d.ts +27 -0
- package/node_modules/@oxygen/shared/dist/email-hard-bounce.js +27 -0
- package/node_modules/@oxygen/shared/dist/error-redaction.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/error-redaction.js +1 -1
- package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +10 -1
- package/node_modules/@oxygen/shared/dist/feature-gates.js +12 -1
- package/node_modules/@oxygen/shared/dist/file-import.d.ts +13 -1
- package/node_modules/@oxygen/shared/dist/file-import.js +33 -6
- package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +73 -3
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +246 -24
- package/node_modules/@oxygen/shared/dist/import-limits.d.ts +25 -1
- package/node_modules/@oxygen/shared/dist/import-limits.js +35 -2
- package/node_modules/@oxygen/shared/dist/index.d.ts +4 -23
- package/node_modules/@oxygen/shared/dist/index.js +4 -43
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +114 -0
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +150 -0
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +17 -0
- package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +41 -0
- package/node_modules/@oxygen/shared/dist/operational-telemetry.js +55 -0
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +19 -2
- package/node_modules/@oxygen/shared/dist/plan-band.d.ts +234 -0
- package/node_modules/@oxygen/shared/dist/plan-band.js +312 -0
- package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +77 -7
- package/node_modules/@oxygen/shared/dist/plan-capabilities.js +87 -7
- package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +219 -0
- package/node_modules/@oxygen/shared/dist/plan-limits-view.js +330 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +335 -126
- package/node_modules/@oxygen/shared/dist/plan-limits.js +277 -86
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +158 -49
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +139 -41
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +42 -23
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +56 -37
- package/node_modules/@oxygen/shared/dist/process-resource.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/process-resource.js +25 -0
- package/node_modules/@oxygen/shared/dist/provider-http-error.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/provider-http-error.js +27 -0
- package/node_modules/@oxygen/shared/dist/repricing.d.ts +257 -0
- package/node_modules/@oxygen/shared/dist/repricing.js +721 -0
- package/node_modules/@oxygen/shared/dist/semver.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/semver.js +41 -0
- package/node_modules/@oxygen/shared/dist/sending-limits.d.ts +30 -0
- package/node_modules/@oxygen/shared/dist/sending-limits.js +43 -0
- package/node_modules/@oxygen/shared/dist/sending-seats.d.ts +18 -15
- package/node_modules/@oxygen/shared/dist/sending-seats.js +22 -17
- package/node_modules/@oxygen/shared/dist/sequence-failures.js +4 -1
- package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +57 -8
- package/node_modules/@oxygen/shared/dist/spend-safety.js +64 -11
- package/node_modules/@oxygen/shared/dist/stripe-price-catalog.d.ts +33 -1
- package/node_modules/@oxygen/shared/dist/stripe-price-catalog.js +71 -1
- package/node_modules/@oxygen/shared/dist/table-capacity.d.ts +68 -10
- package/node_modules/@oxygen/shared/dist/table-capacity.js +85 -4
- package/node_modules/@oxygen/shared/dist/telemetry-export-observer.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/telemetry-export-observer.js +13 -5
- package/node_modules/@oxygen/shared/dist/telemetry-resource.d.ts +40 -0
- package/node_modules/@oxygen/shared/dist/telemetry-resource.js +35 -0
- package/node_modules/@oxygen/shared/dist/telemetry.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/telemetry.js +41 -2
- package/node_modules/@oxygen/shared/dist/trace-context.d.ts +29 -0
- package/node_modules/@oxygen/shared/dist/trace-context.js +88 -0
- package/node_modules/@oxygen/shared/dist/ugc.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/ugc.js +29 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -3
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +14 -27
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +5 -0
- package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +3 -3
- package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +15 -1
- package/node_modules/@oxygen/workflows/dist/graph/types.js +15 -1
- package/node_modules/@oxygen/workflows/dist/index.d.ts +45 -0
- package/node_modules/@oxygen/workflows/dist/index.js +152 -2
- package/node_modules/@oxygen/workflows/dist/usage-estimate.d.ts +10 -1
- package/node_modules/@oxygen/workflows/dist/usage-estimate.js +33 -29
- package/package.json +1 -1
- package/node_modules/@oxygen/shared/dist/email-warmup-readiness.d.ts +0 -64
- package/node_modules/@oxygen/shared/dist/email-warmup-readiness.js +0 -90
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type SemanticVersion = {
|
|
2
|
+
major: number;
|
|
3
|
+
minor: number;
|
|
4
|
+
patch: number;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Parse a three-segment semantic version (e.g. `1.142.17`). Pre-release and
|
|
8
|
+
* build metadata suffixes (`-rc.1`, `+build`) are tolerated but ignored.
|
|
9
|
+
* Returns `null` when the input is not a parseable `major.minor.patch` string.
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseSemver(version: string): SemanticVersion | null;
|
|
12
|
+
/**
|
|
13
|
+
* Compare two semantic versions. Returns -1 when `a < b`, 1 when `a > b`, and
|
|
14
|
+
* 0 when they are equal. Unparseable inputs compare as equal (0) so callers
|
|
15
|
+
* fail open rather than misordering garbage.
|
|
16
|
+
*/
|
|
17
|
+
export declare function compareSemver(a: string, b: string): -1 | 0 | 1;
|
|
18
|
+
/** True when `a` is a strictly greater semantic version than `b`. */
|
|
19
|
+
export declare function isVersionGreater(a: string, b: string): boolean;
|
|
20
|
+
/** True when `a` is a strictly lesser semantic version than `b`. */
|
|
21
|
+
export declare function isVersionLess(a: string, b: string): boolean;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a three-segment semantic version (e.g. `1.142.17`). Pre-release and
|
|
3
|
+
* build metadata suffixes (`-rc.1`, `+build`) are tolerated but ignored.
|
|
4
|
+
* Returns `null` when the input is not a parseable `major.minor.patch` string.
|
|
5
|
+
*/
|
|
6
|
+
export function parseSemver(version) {
|
|
7
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version);
|
|
8
|
+
if (!match)
|
|
9
|
+
return null;
|
|
10
|
+
return {
|
|
11
|
+
major: Number(match[1]),
|
|
12
|
+
minor: Number(match[2]),
|
|
13
|
+
patch: Number(match[3]),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Compare two semantic versions. Returns -1 when `a < b`, 1 when `a > b`, and
|
|
18
|
+
* 0 when they are equal. Unparseable inputs compare as equal (0) so callers
|
|
19
|
+
* fail open rather than misordering garbage.
|
|
20
|
+
*/
|
|
21
|
+
export function compareSemver(a, b) {
|
|
22
|
+
const left = parseSemver(a);
|
|
23
|
+
const right = parseSemver(b);
|
|
24
|
+
if (!left || !right)
|
|
25
|
+
return 0;
|
|
26
|
+
for (const key of ["major", "minor", "patch"]) {
|
|
27
|
+
if (left[key] > right[key])
|
|
28
|
+
return 1;
|
|
29
|
+
if (left[key] < right[key])
|
|
30
|
+
return -1;
|
|
31
|
+
}
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
/** True when `a` is a strictly greater semantic version than `b`. */
|
|
35
|
+
export function isVersionGreater(a, b) {
|
|
36
|
+
return compareSemver(a, b) > 0;
|
|
37
|
+
}
|
|
38
|
+
/** True when `a` is a strictly lesser semantic version than `b`. */
|
|
39
|
+
export function isVersionLess(a, b) {
|
|
40
|
+
return compareSemver(a, b) < 0;
|
|
41
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { RepricingOptions } from "./repricing.js";
|
|
2
|
+
/**
|
|
3
|
+
* The daily cap a new mailbox gets when nobody names one. New mailboxes were
|
|
4
|
+
* created at 18 a day before the repricing, so this is an increase and is live.
|
|
5
|
+
*/
|
|
6
|
+
export declare const EMAIL_MAILBOX_DEFAULT_DAILY_CAP: 25;
|
|
7
|
+
/**
|
|
8
|
+
* Who set a mailbox's stored cap. `default` = OXYGEN's default at creation,
|
|
9
|
+
* `user` = a person or agent set it. A row written before the column existed
|
|
10
|
+
* has no source (null): its cap may be the old column default or a choice.
|
|
11
|
+
*/
|
|
12
|
+
export declare const EMAIL_MAILBOX_DAILY_CAP_SOURCES: readonly ["default", "user"];
|
|
13
|
+
export type EmailMailboxDailyCapSource = (typeof EMAIL_MAILBOX_DAILY_CAP_SOURCES)[number];
|
|
14
|
+
export declare function readEmailMailboxDailyCapSource(value: unknown): EmailMailboxDailyCapSource | null;
|
|
15
|
+
/** The most a mailbox may be set to send a day, independent of billing activation. */
|
|
16
|
+
export declare function emailMailboxDailyCapMaximum(_options?: RepricingOptions): number | null;
|
|
17
|
+
/**
|
|
18
|
+
* The configured cap in force for a stored one: every surface that reads a
|
|
19
|
+
* mailbox's cap, and the send claim, go through this.
|
|
20
|
+
*
|
|
21
|
+
* Stored choices are retained up to the technical maximum.
|
|
22
|
+
*/
|
|
23
|
+
export declare function emailMailboxDailyCapInForce(input: {
|
|
24
|
+
storedCap: number;
|
|
25
|
+
source: EmailMailboxDailyCapSource | null;
|
|
26
|
+
}, options?: RepricingOptions): number;
|
|
27
|
+
/** A new voice number may dial this many times a day once it has warmed up. */
|
|
28
|
+
export declare const VOICE_NUMBER_DEFAULT_DAILY_CAP = 100;
|
|
29
|
+
/** The most a voice number may be set to dial a day. */
|
|
30
|
+
export declare const VOICE_NUMBER_MAX_DAILY_CAP = 300;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Per-channel sending limits that are the same on every plan (2026-09 repricing,
|
|
2
|
+
// decision L3; specification slice S52). Limits guard, credits meter: none of
|
|
3
|
+
// these is sold, and none charges for sending.
|
|
4
|
+
//
|
|
5
|
+
// Email campaign ramp: 5 / 10 / 20 based on accepted-send days, then the
|
|
6
|
+
// configured cap (25 by default, maximum 50), independent of billing activation.
|
|
7
|
+
// The versioned migration changes old defaults once; later choices are retained.
|
|
8
|
+
//
|
|
9
|
+
// Voice per number: 100 dials a day by default, adjustable up to 300.
|
|
10
|
+
import { EMAIL_DELIVERABILITY_DEFAULTS } from "./email-deliverability-policy.js";
|
|
11
|
+
/**
|
|
12
|
+
* The daily cap a new mailbox gets when nobody names one. New mailboxes were
|
|
13
|
+
* created at 18 a day before the repricing, so this is an increase and is live.
|
|
14
|
+
*/
|
|
15
|
+
export const EMAIL_MAILBOX_DEFAULT_DAILY_CAP = EMAIL_DELIVERABILITY_DEFAULTS.campaignDailyCap;
|
|
16
|
+
/**
|
|
17
|
+
* Who set a mailbox's stored cap. `default` = OXYGEN's default at creation,
|
|
18
|
+
* `user` = a person or agent set it. A row written before the column existed
|
|
19
|
+
* has no source (null): its cap may be the old column default or a choice.
|
|
20
|
+
*/
|
|
21
|
+
export const EMAIL_MAILBOX_DAILY_CAP_SOURCES = ["default", "user"];
|
|
22
|
+
export function readEmailMailboxDailyCapSource(value) {
|
|
23
|
+
return value === "default" || value === "user" ? value : null;
|
|
24
|
+
}
|
|
25
|
+
/** The most a mailbox may be set to send a day, independent of billing activation. */
|
|
26
|
+
export function emailMailboxDailyCapMaximum(_options = {}) {
|
|
27
|
+
return 50;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The configured cap in force for a stored one: every surface that reads a
|
|
31
|
+
* mailbox's cap, and the send claim, go through this.
|
|
32
|
+
*
|
|
33
|
+
* Stored choices are retained up to the technical maximum.
|
|
34
|
+
*/
|
|
35
|
+
export function emailMailboxDailyCapInForce(input, options = {}) {
|
|
36
|
+
const stored = Number.isFinite(input.storedCap) ? Math.max(0, Math.floor(input.storedCap)) : 0;
|
|
37
|
+
const maximum = emailMailboxDailyCapMaximum(options);
|
|
38
|
+
return maximum === null ? stored : Math.min(stored, maximum);
|
|
39
|
+
}
|
|
40
|
+
/** A new voice number may dial this many times a day once it has warmed up. */
|
|
41
|
+
export const VOICE_NUMBER_DEFAULT_DAILY_CAP = 100;
|
|
42
|
+
/** The most a voice number may be set to dial a day. */
|
|
43
|
+
export const VOICE_NUMBER_MAX_DAILY_CAP = 300;
|
|
@@ -25,24 +25,15 @@
|
|
|
25
25
|
* "skips the seat check", it is one that holds a legacy entitlement which
|
|
26
26
|
* SATISFIES the seat check. The gate always runs.
|
|
27
27
|
*/
|
|
28
|
-
import {
|
|
28
|
+
import { SENDING_SEAT_USD_CENTS, type SendingSeatKey } from "./pricing-snapshot.generated.js";
|
|
29
|
+
import type { CreditGateKind } from "./credit-gate.js";
|
|
29
30
|
export { SENDING_SEAT_USD_CENTS };
|
|
30
31
|
/**
|
|
31
|
-
* Email infrastructure
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* warm-up. A customer buying an Oxygen mailbox buys ONE thing at ONE price and
|
|
36
|
-
* can never end up holding a mailbox with no seat to send from. A customer
|
|
37
|
-
* bringing their own mailbox pays SENDING_SEAT_USD_CENTS.email_sender instead.
|
|
38
|
-
* The two are alternatives, never both for the same mailbox.
|
|
39
|
-
*
|
|
40
|
-
* The practical consequence for the connect gate: email capacity is
|
|
41
|
-
* (email_sender seats) + (managed inbox slots). Counting only seats would refuse
|
|
42
|
-
* a customer who bought inboxes, which is exactly the assemble-the-parts failure
|
|
43
|
-
* this bundle exists to prevent.
|
|
32
|
+
* Email infrastructure is no longer sold in dollars (repricing 2026-09, S32/S33):
|
|
33
|
+
* a managed inbox is priced in credits, 400 for a Google mailbox plus the 100-credit
|
|
34
|
+
* mailbox connection every sending mailbox holds, warm-up included. A customer
|
|
35
|
+
* bringing their own mailbox pays the connection alone.
|
|
44
36
|
*/
|
|
45
|
-
export { MANAGED_INBOX_USD_CENTS, SENDING_DOMAIN_USD_CENTS };
|
|
46
37
|
export type { SendingSeatKey };
|
|
47
38
|
export declare const SENDING_SEAT_KEYS: readonly ["linkedin", "whatsapp", "phone_number", "email_sender"];
|
|
48
39
|
export type SendingSeatDefinition = {
|
|
@@ -64,6 +55,18 @@ export type SendingSeatDefinition = {
|
|
|
64
55
|
export declare const SENDING_SEAT_CATALOG: {
|
|
65
56
|
readonly [K in SendingSeatKey]: SendingSeatDefinition;
|
|
66
57
|
};
|
|
58
|
+
/**
|
|
59
|
+
* The credit reservation each sending-seat channel becomes once seats are
|
|
60
|
+
* retired (repricing 2026-09, decisions 2.7 and 2.9). A seat still held keeps
|
|
61
|
+
* covering accounts of its kind until the seat subscription ends at renewal, and
|
|
62
|
+
* only then does each account start reserving credits, so an account is never
|
|
63
|
+
* paid for twice.
|
|
64
|
+
*/
|
|
65
|
+
export declare const SEAT_RESERVATION_KIND: {
|
|
66
|
+
readonly [K in SendingSeatKey]: CreditGateKind;
|
|
67
|
+
};
|
|
68
|
+
/** The seat channel whose seats cover a reservation kind, or null for a kind no seat ever covered. */
|
|
69
|
+
export declare function seatKeyForReservationKind(kind: string): SendingSeatKey | null;
|
|
67
70
|
export declare const SENDING_SEAT_LEGACY_CHARGE_KEYS: readonly string[];
|
|
68
71
|
export declare function isSendingSeatKey(value: unknown): value is SendingSeatKey;
|
|
69
72
|
export declare function sendingSeatMonthlyPriceCents(key: SendingSeatKey): number;
|
|
@@ -25,24 +25,8 @@
|
|
|
25
25
|
* "skips the seat check", it is one that holds a legacy entitlement which
|
|
26
26
|
* SATISFIES the seat check. The gate always runs.
|
|
27
27
|
*/
|
|
28
|
-
import {
|
|
28
|
+
import { SENDING_SEAT_USD_CENTS, } from "./pricing-snapshot.generated.js";
|
|
29
29
|
export { SENDING_SEAT_USD_CENTS };
|
|
30
|
-
/**
|
|
31
|
-
* Email infrastructure, kept next to the seats because the relationship between
|
|
32
|
-
* them is the whole design.
|
|
33
|
-
*
|
|
34
|
-
* MANAGED_INBOX_USD_CENTS is ALL-IN: it already contains the sending seat and
|
|
35
|
-
* warm-up. A customer buying an Oxygen mailbox buys ONE thing at ONE price and
|
|
36
|
-
* can never end up holding a mailbox with no seat to send from. A customer
|
|
37
|
-
* bringing their own mailbox pays SENDING_SEAT_USD_CENTS.email_sender instead.
|
|
38
|
-
* The two are alternatives, never both for the same mailbox.
|
|
39
|
-
*
|
|
40
|
-
* The practical consequence for the connect gate: email capacity is
|
|
41
|
-
* (email_sender seats) + (managed inbox slots). Counting only seats would refuse
|
|
42
|
-
* a customer who bought inboxes, which is exactly the assemble-the-parts failure
|
|
43
|
-
* this bundle exists to prevent.
|
|
44
|
-
*/
|
|
45
|
-
export { MANAGED_INBOX_USD_CENTS, SENDING_DOMAIN_USD_CENTS };
|
|
46
30
|
export const SENDING_SEAT_KEYS = [
|
|
47
31
|
"linkedin",
|
|
48
32
|
"whatsapp",
|
|
@@ -88,6 +72,27 @@ export const SENDING_SEAT_CATALOG = {
|
|
|
88
72
|
stripePriceEnvVar: "STRIPE_PRICE_SEAT_EMAIL_USD",
|
|
89
73
|
},
|
|
90
74
|
};
|
|
75
|
+
/**
|
|
76
|
+
* The credit reservation each sending-seat channel becomes once seats are
|
|
77
|
+
* retired (repricing 2026-09, decisions 2.7 and 2.9). A seat still held keeps
|
|
78
|
+
* covering accounts of its kind until the seat subscription ends at renewal, and
|
|
79
|
+
* only then does each account start reserving credits, so an account is never
|
|
80
|
+
* paid for twice.
|
|
81
|
+
*/
|
|
82
|
+
export const SEAT_RESERVATION_KIND = {
|
|
83
|
+
linkedin: "linkedin_account",
|
|
84
|
+
whatsapp: "whatsapp_account",
|
|
85
|
+
phone_number: "phone_number",
|
|
86
|
+
email_sender: "sending_mailbox",
|
|
87
|
+
};
|
|
88
|
+
/** The seat channel whose seats cover a reservation kind, or null for a kind no seat ever covered. */
|
|
89
|
+
export function seatKeyForReservationKind(kind) {
|
|
90
|
+
for (const seatKey of SENDING_SEAT_KEYS) {
|
|
91
|
+
if (SEAT_RESERVATION_KIND[seatKey] === kind)
|
|
92
|
+
return seatKey;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
91
96
|
export const SENDING_SEAT_LEGACY_CHARGE_KEYS = SENDING_SEAT_KEYS
|
|
92
97
|
.map((key) => SENDING_SEAT_CATALOG[key].legacyChargeKey)
|
|
93
98
|
.filter((chargeKey) => chargeKey !== null);
|
|
@@ -172,8 +172,11 @@ function classifySequenceFailure(input) {
|
|
|
172
172
|
.toLowerCase();
|
|
173
173
|
// Ambiguous effects win every other classification. A 429-shaped code or a
|
|
174
174
|
// provider name must never make a possibly-applied write safe to replay.
|
|
175
|
+
// `write[_-]?ambiguous` is the LinkedIn client's `provider_write_ambiguous`:
|
|
176
|
+
// stored rows written before that error carried `effect_outcome` would
|
|
177
|
+
// otherwise read back as a recipient hard bounce.
|
|
175
178
|
if (input.effectOutcome === "unknown" ||
|
|
176
|
-
/(?:effect|outcome)[_-]?unknown|ambiguous[_-]?(?:effect|write)/i.test(codes)) {
|
|
179
|
+
/(?:effect|outcome)[_-]?unknown|ambiguous[_-]?(?:effect|write)|write[_-]?ambiguous/i.test(codes)) {
|
|
177
180
|
return "effect_unknown";
|
|
178
181
|
}
|
|
179
182
|
if ([input.code, input.providerSubtype].some((code) => isProviderFundingErrorCode(code)))
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { PlanTier } from "./billing.js";
|
|
2
|
+
import type { PlanBand } from "./plan-band.js";
|
|
2
3
|
import type { LimitsTier } from "./plan-limits.js";
|
|
4
|
+
import { type RepricingOptions } from "./repricing.js";
|
|
3
5
|
/**
|
|
4
6
|
* Spend-safety defaults: plan-scaled ceilings applied when an autonomous
|
|
5
7
|
* spend path was armed WITHOUT an explicit cap. They restore the ADR-0011
|
|
@@ -19,18 +21,29 @@ export declare const AUTONOMOUS_WORKFLOW_TRIGGER_TYPES: ReadonlySet<string>;
|
|
|
19
21
|
/**
|
|
20
22
|
* Per-run managed-credit ceiling for a LIVE workflow run fired by an autonomous
|
|
21
23
|
* trigger when neither run metadata nor the manifest declares `max_credits`.
|
|
24
|
+
*
|
|
25
|
+
* These are the ceilings each rung read BEFORE the 2026-09 repricing. The value
|
|
26
|
+
* in force is resolved per plan by `resolveDefaultTriggerRunCreditCeilingForPlan`
|
|
27
|
+
* (`plan-band.ts`): 10% of the plan's monthly credits (decision L5.1), where a
|
|
28
|
+
* larger default applies at once and a smaller one waits for the
|
|
29
|
+
* effective-date switch with this table as its old value. Read this table
|
|
30
|
+
* directly only to know the pre-repricing value.
|
|
22
31
|
*/
|
|
23
32
|
export declare const DEFAULT_TRIGGER_RUN_CREDIT_CEILING: Record<SpendSafetyPlanTier, number | null>;
|
|
24
33
|
/**
|
|
25
34
|
* Per-delivery credit ceiling stamped onto a standing/webhook table auto-run
|
|
26
|
-
* batch when the armed configuration carries no explicit `max_credits`.
|
|
35
|
+
* batch when the armed configuration carries no explicit `max_credits`. The
|
|
36
|
+
* pre-repricing values, like the trigger table above; resolve the value in force
|
|
37
|
+
* with `resolveDefaultAutoRunBatchCreditCeilingForPlan`.
|
|
27
38
|
*/
|
|
28
39
|
export declare const DEFAULT_AUTO_RUN_BATCH_CREDIT_CEILING: Record<SpendSafetyPlanTier, number | null>;
|
|
29
40
|
/**
|
|
30
41
|
* Row ceiling for a BYOK AI-column run whose caller gave no explicit row bound
|
|
31
42
|
* (no limit, no row_ids, selection "all"). BYOK bills the customer's own
|
|
32
|
-
* provider account, so
|
|
33
|
-
*
|
|
43
|
+
* provider account, so OXYGEN's credit ceiling bounds only its own platform fee
|
|
44
|
+
* (0.05 credits a call from the 2026-09 repricing instant, decision 6.3) and
|
|
45
|
+
* never the provider's bill — row count stays the enforceable unit for that.
|
|
46
|
+
* An explicit limit (up to the 500k platform row cap) wins.
|
|
34
47
|
*/
|
|
35
48
|
export declare const DEFAULT_BYOK_COLUMN_RUN_MAX_ROWS: Record<SpendSafetyPlanTier, number | null>;
|
|
36
49
|
/**
|
|
@@ -89,6 +102,12 @@ export declare function resolveByokProviderDailyCapEnforcementMode(configured?:
|
|
|
89
102
|
*
|
|
90
103
|
* This applies ONLY to the implicit default. An org that sets its own org-daily
|
|
91
104
|
* policy has chosen a real spend ceiling, and that ceiling counts everything.
|
|
105
|
+
*
|
|
106
|
+
* The monthly reservation renewals of the 2026-09 repricing (connected sending
|
|
107
|
+
* mailboxes, deliverability units, connected accounts and phone numbers, dedicated
|
|
108
|
+
* IPs) are the same shape and are excluded for the same reason (PROPOSED P-59).
|
|
109
|
+
* It matters most on the free plan, whose guard is a fixed 1,000 / 5,000 credits
|
|
110
|
+
* a day: one LinkedIn renewal would otherwise be most of a free day's warning.
|
|
92
111
|
*/
|
|
93
112
|
export declare const IMPLICIT_ORG_DAILY_GUARD_EXCLUDED_CATEGORIES: readonly string[];
|
|
94
113
|
export declare const DEFAULT_ORG_DAILY_SPEND_WARN_MULTIPLE = 5;
|
|
@@ -101,16 +120,46 @@ export type OrgDailySpendGuard = {
|
|
|
101
120
|
/**
|
|
102
121
|
* Resolve the implicit guard's thresholds from a plan's monthly grant. `null`
|
|
103
122
|
* means the guard is suppressed because the plan has no finite positive grant to
|
|
104
|
-
* scale from (
|
|
105
|
-
*
|
|
123
|
+
* scale from (enterprise/custom at null) — the single place that decision is
|
|
124
|
+
* made, so enforcement and the read surfaces cannot drift apart.
|
|
125
|
+
*
|
|
126
|
+
* The free plan has no monthly grant, so it gets a fixed guard instead (decision
|
|
127
|
+
* L5.2: 1,000 warn / 5,000 block a UTC day). It is a new bound on free, so it
|
|
128
|
+
* takes effect at the 2026-09 repricing instant; before then free has none.
|
|
106
129
|
*/
|
|
107
|
-
export declare function resolveOrgDailySpendGuard(monthlyCredits: number | null | undefined
|
|
130
|
+
export declare function resolveOrgDailySpendGuard(monthlyCredits: number | null | undefined, options?: RepricingOptions & {
|
|
131
|
+
freePlan?: boolean;
|
|
132
|
+
}): OrgDailySpendGuard | null;
|
|
108
133
|
/** Credits carry 3 decimals everywhere (control-DB numeric(18,3)). */
|
|
109
134
|
export declare function roundCredits(value: number): number;
|
|
110
135
|
/** Credit estimates that present to 2 decimals (cent-grained customer copy). */
|
|
111
136
|
export declare function roundCreditsToCents(value: number): number;
|
|
112
|
-
|
|
113
|
-
|
|
137
|
+
/**
|
|
138
|
+
* Platform-owned runaway ceiling for one attended Workspace Copilot turn: 3,000
|
|
139
|
+
* customer credits, $6 of upstream inference at the ratified 5x markup, the same
|
|
140
|
+
* on every plan. It is not a user budget or an authorization grant. It lives
|
|
141
|
+
* here rather than in `@oxygen/copilot` so the limits report can state it next
|
|
142
|
+
* to the approval-free allowance it must stay above; `@oxygen/copilot/config`
|
|
143
|
+
* re-exports it.
|
|
144
|
+
*/
|
|
145
|
+
export declare const COPILOT_ATTENDED_INFERENCE_CREDIT_CEILING = 3000;
|
|
146
|
+
/** How much paid work the attended Copilot may start without an approval card. */
|
|
147
|
+
export type CopilotApprovalFreeAllowance = {
|
|
148
|
+
/** The most one call may declare (its server-enforced `max_credits`). */
|
|
149
|
+
perCallCredits: number;
|
|
150
|
+
/** The most all such calls in one session may declare together. */
|
|
151
|
+
perSessionCredits: number;
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* The Copilot's approval-free spend allowance by plan size (decision L5.3). Only
|
|
155
|
+
* the two ends are ratified: free at 50 / 300 credits ($0.50 / $3) and $1,999 at
|
|
156
|
+
* 500 / 2,500 ($5 / $25). The middle sizes are PROPOSED (P-55). Every value is
|
|
157
|
+
* at least the 50 / 300 every plan had before, so the table ships live. The
|
|
158
|
+
* largest session allowance stays below the attended turn backstop above.
|
|
159
|
+
*/
|
|
160
|
+
export declare const COPILOT_APPROVAL_FREE_ALLOWANCE_BY_PLAN_BAND: Readonly<Record<PlanBand, CopilotApprovalFreeAllowance>>;
|
|
161
|
+
/** A plan size's approval-free Copilot allowance. */
|
|
162
|
+
export declare function copilotApprovalFreeAllowanceForBand(band: PlanBand): CopilotApprovalFreeAllowance;
|
|
114
163
|
export declare function resolveDefaultByokColumnRunMaxRows(tier: SpendSafetyPlanTier): number | null;
|
|
115
164
|
export declare function resolveDefaultByokProviderDailyCallCap(tier: SpendSafetyPlanTier): number | null;
|
|
116
165
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FREE_ORG_DAILY_GUARD_BLOCK_CREDITS, FREE_ORG_DAILY_GUARD_WARN_CREDITS, repricedValue, } from "./repricing.js";
|
|
1
2
|
/** Workflow run trigger types that fire without a human in the loop. */
|
|
2
3
|
export const AUTONOMOUS_WORKFLOW_TRIGGER_TYPES = new Set([
|
|
3
4
|
"cron",
|
|
@@ -7,6 +8,13 @@ export const AUTONOMOUS_WORKFLOW_TRIGGER_TYPES = new Set([
|
|
|
7
8
|
/**
|
|
8
9
|
* Per-run managed-credit ceiling for a LIVE workflow run fired by an autonomous
|
|
9
10
|
* trigger when neither run metadata nor the manifest declares `max_credits`.
|
|
11
|
+
*
|
|
12
|
+
* These are the ceilings each rung read BEFORE the 2026-09 repricing. The value
|
|
13
|
+
* in force is resolved per plan by `resolveDefaultTriggerRunCreditCeilingForPlan`
|
|
14
|
+
* (`plan-band.ts`): 10% of the plan's monthly credits (decision L5.1), where a
|
|
15
|
+
* larger default applies at once and a smaller one waits for the
|
|
16
|
+
* effective-date switch with this table as its old value. Read this table
|
|
17
|
+
* directly only to know the pre-repricing value.
|
|
10
18
|
*/
|
|
11
19
|
export const DEFAULT_TRIGGER_RUN_CREDIT_CEILING = {
|
|
12
20
|
free: 50,
|
|
@@ -18,7 +26,9 @@ export const DEFAULT_TRIGGER_RUN_CREDIT_CEILING = {
|
|
|
18
26
|
};
|
|
19
27
|
/**
|
|
20
28
|
* Per-delivery credit ceiling stamped onto a standing/webhook table auto-run
|
|
21
|
-
* batch when the armed configuration carries no explicit `max_credits`.
|
|
29
|
+
* batch when the armed configuration carries no explicit `max_credits`. The
|
|
30
|
+
* pre-repricing values, like the trigger table above; resolve the value in force
|
|
31
|
+
* with `resolveDefaultAutoRunBatchCreditCeilingForPlan`.
|
|
22
32
|
*/
|
|
23
33
|
export const DEFAULT_AUTO_RUN_BATCH_CREDIT_CEILING = {
|
|
24
34
|
free: 50,
|
|
@@ -31,8 +41,10 @@ export const DEFAULT_AUTO_RUN_BATCH_CREDIT_CEILING = {
|
|
|
31
41
|
/**
|
|
32
42
|
* Row ceiling for a BYOK AI-column run whose caller gave no explicit row bound
|
|
33
43
|
* (no limit, no row_ids, selection "all"). BYOK bills the customer's own
|
|
34
|
-
* provider account, so
|
|
35
|
-
*
|
|
44
|
+
* provider account, so OXYGEN's credit ceiling bounds only its own platform fee
|
|
45
|
+
* (0.05 credits a call from the 2026-09 repricing instant, decision 6.3) and
|
|
46
|
+
* never the provider's bill — row count stays the enforceable unit for that.
|
|
47
|
+
* An explicit limit (up to the 500k platform row cap) wins.
|
|
36
48
|
*/
|
|
37
49
|
export const DEFAULT_BYOK_COLUMN_RUN_MAX_ROWS = {
|
|
38
50
|
free: null,
|
|
@@ -106,20 +118,38 @@ export function resolveByokProviderDailyCapEnforcementMode(configured = process.
|
|
|
106
118
|
*
|
|
107
119
|
* This applies ONLY to the implicit default. An org that sets its own org-daily
|
|
108
120
|
* policy has chosen a real spend ceiling, and that ceiling counts everything.
|
|
121
|
+
*
|
|
122
|
+
* The monthly reservation renewals of the 2026-09 repricing (connected sending
|
|
123
|
+
* mailboxes, deliverability units, connected accounts and phone numbers, dedicated
|
|
124
|
+
* IPs) are the same shape and are excluded for the same reason (PROPOSED P-59).
|
|
125
|
+
* It matters most on the free plan, whose guard is a fixed 1,000 / 5,000 credits
|
|
126
|
+
* a day: one LinkedIn renewal would otherwise be most of a free day's warning.
|
|
109
127
|
*/
|
|
110
128
|
export const IMPLICIT_ORG_DAILY_GUARD_EXCLUDED_CATEGORIES = [
|
|
111
129
|
"managed_warmup",
|
|
112
130
|
"managed_inbox",
|
|
131
|
+
"managed_mailbox",
|
|
132
|
+
"managed_deliverability",
|
|
133
|
+
"managed_seat",
|
|
113
134
|
];
|
|
114
135
|
export const DEFAULT_ORG_DAILY_SPEND_WARN_MULTIPLE = 5;
|
|
115
136
|
export const DEFAULT_ORG_DAILY_SPEND_BLOCK_MULTIPLE = 20;
|
|
116
137
|
/**
|
|
117
138
|
* Resolve the implicit guard's thresholds from a plan's monthly grant. `null`
|
|
118
139
|
* means the guard is suppressed because the plan has no finite positive grant to
|
|
119
|
-
* scale from (
|
|
120
|
-
*
|
|
140
|
+
* scale from (enterprise/custom at null) — the single place that decision is
|
|
141
|
+
* made, so enforcement and the read surfaces cannot drift apart.
|
|
142
|
+
*
|
|
143
|
+
* The free plan has no monthly grant, so it gets a fixed guard instead (decision
|
|
144
|
+
* L5.2: 1,000 warn / 5,000 block a UTC day). It is a new bound on free, so it
|
|
145
|
+
* takes effect at the 2026-09 repricing instant; before then free has none.
|
|
121
146
|
*/
|
|
122
|
-
export function resolveOrgDailySpendGuard(monthlyCredits) {
|
|
147
|
+
export function resolveOrgDailySpendGuard(monthlyCredits, options = {}) {
|
|
148
|
+
if (options.freePlan) {
|
|
149
|
+
const warnCredits = repricedValue(FREE_ORG_DAILY_GUARD_WARN_CREDITS, options);
|
|
150
|
+
const blockCredits = repricedValue(FREE_ORG_DAILY_GUARD_BLOCK_CREDITS, options);
|
|
151
|
+
return warnCredits === null || blockCredits === null ? null : { warnCredits, blockCredits };
|
|
152
|
+
}
|
|
123
153
|
if (typeof monthlyCredits !== "number" || !Number.isFinite(monthlyCredits) || monthlyCredits <= 0) {
|
|
124
154
|
return null;
|
|
125
155
|
}
|
|
@@ -136,11 +166,34 @@ export function roundCredits(value) {
|
|
|
136
166
|
export function roundCreditsToCents(value) {
|
|
137
167
|
return Math.round((value + Number.EPSILON) * 100) / 100;
|
|
138
168
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
169
|
+
/**
|
|
170
|
+
* Platform-owned runaway ceiling for one attended Workspace Copilot turn: 3,000
|
|
171
|
+
* customer credits, $6 of upstream inference at the ratified 5x markup, the same
|
|
172
|
+
* on every plan. It is not a user budget or an authorization grant. It lives
|
|
173
|
+
* here rather than in `@oxygen/copilot` so the limits report can state it next
|
|
174
|
+
* to the approval-free allowance it must stay above; `@oxygen/copilot/config`
|
|
175
|
+
* re-exports it.
|
|
176
|
+
*/
|
|
177
|
+
export const COPILOT_ATTENDED_INFERENCE_CREDIT_CEILING = 3_000;
|
|
178
|
+
/**
|
|
179
|
+
* The Copilot's approval-free spend allowance by plan size (decision L5.3). Only
|
|
180
|
+
* the two ends are ratified: free at 50 / 300 credits ($0.50 / $3) and $1,999 at
|
|
181
|
+
* 500 / 2,500 ($5 / $25). The middle sizes are PROPOSED (P-55). Every value is
|
|
182
|
+
* at least the 50 / 300 every plan had before, so the table ships live. The
|
|
183
|
+
* largest session allowance stays below the attended turn backstop above.
|
|
184
|
+
*/
|
|
185
|
+
export const COPILOT_APPROVAL_FREE_ALLOWANCE_BY_PLAN_BAND = Object.freeze({
|
|
186
|
+
free: Object.freeze({ perCallCredits: 50, perSessionCredits: 300 }),
|
|
187
|
+
"49": Object.freeze({ perCallCredits: 100, perSessionCredits: 500 }),
|
|
188
|
+
"99": Object.freeze({ perCallCredits: 100, perSessionCredits: 500 }),
|
|
189
|
+
"199": Object.freeze({ perCallCredits: 200, perSessionCredits: 1_000 }),
|
|
190
|
+
"499": Object.freeze({ perCallCredits: 200, perSessionCredits: 1_000 }),
|
|
191
|
+
"999": Object.freeze({ perCallCredits: 300, perSessionCredits: 1_500 }),
|
|
192
|
+
"1999": Object.freeze({ perCallCredits: 500, perSessionCredits: 2_500 }),
|
|
193
|
+
});
|
|
194
|
+
/** A plan size's approval-free Copilot allowance. */
|
|
195
|
+
export function copilotApprovalFreeAllowanceForBand(band) {
|
|
196
|
+
return COPILOT_APPROVAL_FREE_ALLOWANCE_BY_PLAN_BAND[band];
|
|
144
197
|
}
|
|
145
198
|
export function resolveDefaultByokColumnRunMaxRows(tier) {
|
|
146
199
|
return DEFAULT_BYOK_COLUMN_RUN_MAX_ROWS[tier];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CONTACT_SALES_URL, PURCHASABLE_PLAN_KEYS, SELF_SERVE_PLAN_TIERS, normalizeBillingCurrency, type BillingCurrency, type OxygenPlanKey, type PricingPlanDefinition, type SelfServePlanTier } from "./billing.js";
|
|
1
|
+
import { CONTACT_SALES_URL, PURCHASABLE_PLAN_KEYS, SELF_SERVE_PLAN_TIERS, normalizeBillingCurrency, type BillingCurrency, type OxygenPlanKey, type PlanBillingInterval, type PricingPlanDefinition, type SelfServePlanTier } from "./billing.js";
|
|
2
2
|
export { CONTACT_SALES_URL, PURCHASABLE_PLAN_KEYS, SELF_SERVE_PLAN_TIERS, normalizeBillingCurrency, };
|
|
3
3
|
export type { BillingCurrency, OxygenPlanKey, SelfServePlanTier };
|
|
4
4
|
export type PricingPlan = PricingPlanDefinition & {
|
|
@@ -17,6 +17,32 @@ export declare const PRICING_PLANS: {
|
|
|
17
17
|
/** The six purchasable rungs with their Stripe price ids, cheapest first. */
|
|
18
18
|
export declare const PURCHASABLE_PLANS: Record<OxygenPlanKey, PricingPlan>;
|
|
19
19
|
export declare function getPlanPriceId(plan: PricingPlan, currency: BillingCurrency): string;
|
|
20
|
+
export type { PlanBillingInterval };
|
|
21
|
+
/**
|
|
22
|
+
* The configured Price for one rung and interval, or "" when that Price is not
|
|
23
|
+
* configured (every yearly Price, until S14 creates them).
|
|
24
|
+
*/
|
|
25
|
+
export declare function getPurchasablePlanPriceId(planKey: OxygenPlanKey, interval: PlanBillingInterval, currency?: BillingCurrency): string;
|
|
26
|
+
/**
|
|
27
|
+
* The billing interval of a recognized plan Price. "year" only for a configured
|
|
28
|
+
* yearly Oxygen Price; every other recognized Price (monthly rungs and every
|
|
29
|
+
* grandfathered or legacy plan) is "month"; an unrecognized id is null.
|
|
30
|
+
*
|
|
31
|
+
* This is the one definition the credit-grant paths read, so the Stripe webhook,
|
|
32
|
+
* the lazy grant on a balance read and the slice sweep can never disagree about
|
|
33
|
+
* whether a paid period is sliced.
|
|
34
|
+
*/
|
|
35
|
+
export declare function planBillingIntervalForPriceId(priceId: string | null | undefined): PlanBillingInterval | null;
|
|
36
|
+
/**
|
|
37
|
+
* Whether yearly billing can be sold: every purchasable rung has a configured
|
|
38
|
+
* yearly Price. False until S14 creates them, which is what keeps every annual
|
|
39
|
+
* control (the plan picker's Yearly switch, `--interval year`, the pricing
|
|
40
|
+
* page's yearly line) dark. All six or nothing, so no surface ever offers a
|
|
41
|
+
* yearly rung whose checkout would be refused.
|
|
42
|
+
*/
|
|
43
|
+
export declare function annualPlanBillingAvailable(currency?: BillingCurrency): boolean;
|
|
44
|
+
/** Every configured yearly plan Price id (empty until S14). */
|
|
45
|
+
export declare function configuredAnnualPlanPriceIds(): readonly string[];
|
|
20
46
|
/**
|
|
21
47
|
* Resolve a Stripe price id to the plan key stored in `subscriptions.tier`.
|
|
22
48
|
*
|
|
@@ -30,6 +56,12 @@ export declare function tierFromPriceId(priceId: string): string | null;
|
|
|
30
56
|
* Complete runtime-recognized Price set grouped by the current self-serve
|
|
31
57
|
* family. Catalog audit/migration code consumes this instead of maintaining a
|
|
32
58
|
* second env map that can silently omit grandfathered subscriptions.
|
|
59
|
+
*
|
|
60
|
+
* The configured yearly Prices are listed with the Oxygen family (slice S14):
|
|
61
|
+
* the migration planner compares the billing interval before it matches a
|
|
62
|
+
* rung by plan key or amount (S16, `stripeMigrationTargetFor`), so a yearly
|
|
63
|
+
* Price in the source set resolves to itself and can never become the target
|
|
64
|
+
* of a monthly subscription.
|
|
33
65
|
*/
|
|
34
66
|
export declare function recognizedStripePriceIdsBySelfServeTier(): Record<SelfServePlanTier, readonly string[]>;
|
|
35
67
|
export declare function formatMonthlyPrice(priceCents: number | null, currency?: BillingCurrency): string;
|
|
@@ -19,6 +19,25 @@ const STRIPE_PRICE_OXYGEN_IDS = {
|
|
|
19
19
|
oxygen_999: { usd: process.env.STRIPE_PRICE_OXYGEN_999_USD ?? "" },
|
|
20
20
|
oxygen_1999: { usd: process.env.STRIPE_PRICE_OXYGEN_1999_USD ?? "" },
|
|
21
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* The yearly Price of each rung (Repricing 2026-09, decision 1.2: pay 10 months,
|
|
24
|
+
* get 12, credits still granted monthly). A second Price on the rung's existing
|
|
25
|
+
* Product, since month and year are distinct intervals a Portal configuration
|
|
26
|
+
* accepts under one Product.
|
|
27
|
+
*
|
|
28
|
+
* DARK until the yearly Price ids are configured (`provision` creates the Prices
|
|
29
|
+
* and prints these names, slice S14): every env var is unset, an
|
|
30
|
+
* unset id is "", and tierFromPriceId refuses "" before any map is consulted, so
|
|
31
|
+
* no subscription resolves to a yearly interval and nothing is ever sliced.
|
|
32
|
+
*/
|
|
33
|
+
const STRIPE_PRICE_OXYGEN_ANNUAL_IDS = {
|
|
34
|
+
oxygen_49: { usd: process.env.STRIPE_PRICE_OXYGEN_49_ANNUAL_USD ?? "" },
|
|
35
|
+
oxygen_99: { usd: process.env.STRIPE_PRICE_OXYGEN_99_ANNUAL_USD ?? "" },
|
|
36
|
+
oxygen_199: { usd: process.env.STRIPE_PRICE_OXYGEN_199_ANNUAL_USD ?? "" },
|
|
37
|
+
oxygen_499: { usd: process.env.STRIPE_PRICE_OXYGEN_499_ANNUAL_USD ?? "" },
|
|
38
|
+
oxygen_999: { usd: process.env.STRIPE_PRICE_OXYGEN_999_ANNUAL_USD ?? "" },
|
|
39
|
+
oxygen_1999: { usd: process.env.STRIPE_PRICE_OXYGEN_1999_ANNUAL_USD ?? "" },
|
|
40
|
+
};
|
|
22
41
|
// Grandfathered on 2026-09-19. These three stay pointed at their LIVE prices
|
|
23
42
|
// (35 Starter + 2 Pro subscriptions bill against them) rather than moving into
|
|
24
43
|
// the legacy CSV lists: tierFromPriceId must keep returning "starter"/"pro"/
|
|
@@ -82,6 +101,49 @@ export function getPlanPriceId(plan, currency) {
|
|
|
82
101
|
return (plan.priceIds[currency]
|
|
83
102
|
|| (currency === DEFAULT_BILLING_CURRENCY ? plan.priceId : ""));
|
|
84
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* The configured Price for one rung and interval, or "" when that Price is not
|
|
106
|
+
* configured (every yearly Price, until S14 creates them).
|
|
107
|
+
*/
|
|
108
|
+
export function getPurchasablePlanPriceId(planKey, interval, currency = DEFAULT_BILLING_CURRENCY) {
|
|
109
|
+
const ids = interval === "year"
|
|
110
|
+
? STRIPE_PRICE_OXYGEN_ANNUAL_IDS[planKey]
|
|
111
|
+
: STRIPE_PRICE_OXYGEN_IDS[planKey];
|
|
112
|
+
return ids[currency] || "";
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The billing interval of a recognized plan Price. "year" only for a configured
|
|
116
|
+
* yearly Oxygen Price; every other recognized Price (monthly rungs and every
|
|
117
|
+
* grandfathered or legacy plan) is "month"; an unrecognized id is null.
|
|
118
|
+
*
|
|
119
|
+
* This is the one definition the credit-grant paths read, so the Stripe webhook,
|
|
120
|
+
* the lazy grant on a balance read and the slice sweep can never disagree about
|
|
121
|
+
* whether a paid period is sliced.
|
|
122
|
+
*/
|
|
123
|
+
export function planBillingIntervalForPriceId(priceId) {
|
|
124
|
+
if (!priceId)
|
|
125
|
+
return null;
|
|
126
|
+
for (const planKey of PURCHASABLE_PLAN_KEYS) {
|
|
127
|
+
if (Object.values(STRIPE_PRICE_OXYGEN_ANNUAL_IDS[planKey]).includes(priceId)) {
|
|
128
|
+
return "year";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return tierFromPriceId(priceId) === null ? null : "month";
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Whether yearly billing can be sold: every purchasable rung has a configured
|
|
135
|
+
* yearly Price. False until S14 creates them, which is what keeps every annual
|
|
136
|
+
* control (the plan picker's Yearly switch, `--interval year`, the pricing
|
|
137
|
+
* page's yearly line) dark. All six or nothing, so no surface ever offers a
|
|
138
|
+
* yearly rung whose checkout would be refused.
|
|
139
|
+
*/
|
|
140
|
+
export function annualPlanBillingAvailable(currency = DEFAULT_BILLING_CURRENCY) {
|
|
141
|
+
return PURCHASABLE_PLAN_KEYS.every((planKey) => getPurchasablePlanPriceId(planKey, "year", currency) !== "");
|
|
142
|
+
}
|
|
143
|
+
/** Every configured yearly plan Price id (empty until S14). */
|
|
144
|
+
export function configuredAnnualPlanPriceIds() {
|
|
145
|
+
return uniqueNonEmptyPriceIds(PURCHASABLE_PLAN_KEYS.flatMap((planKey) => Object.values(STRIPE_PRICE_OXYGEN_ANNUAL_IDS[planKey])));
|
|
146
|
+
}
|
|
85
147
|
/**
|
|
86
148
|
* Resolve a Stripe price id to the plan key stored in `subscriptions.tier`.
|
|
87
149
|
*
|
|
@@ -97,7 +159,8 @@ export function tierFromPriceId(priceId) {
|
|
|
97
159
|
if (!priceId)
|
|
98
160
|
return null;
|
|
99
161
|
for (const planKey of PURCHASABLE_PLAN_KEYS) {
|
|
100
|
-
if (Object.values(STRIPE_PRICE_OXYGEN_IDS[planKey]).includes(priceId)
|
|
162
|
+
if (Object.values(STRIPE_PRICE_OXYGEN_IDS[planKey]).includes(priceId)
|
|
163
|
+
|| Object.values(STRIPE_PRICE_OXYGEN_ANNUAL_IDS[planKey]).includes(priceId)) {
|
|
101
164
|
return planKey;
|
|
102
165
|
}
|
|
103
166
|
}
|
|
@@ -122,11 +185,18 @@ export function tierFromPriceId(priceId) {
|
|
|
122
185
|
* Complete runtime-recognized Price set grouped by the current self-serve
|
|
123
186
|
* family. Catalog audit/migration code consumes this instead of maintaining a
|
|
124
187
|
* second env map that can silently omit grandfathered subscriptions.
|
|
188
|
+
*
|
|
189
|
+
* The configured yearly Prices are listed with the Oxygen family (slice S14):
|
|
190
|
+
* the migration planner compares the billing interval before it matches a
|
|
191
|
+
* rung by plan key or amount (S16, `stripeMigrationTargetFor`), so a yearly
|
|
192
|
+
* Price in the source set resolves to itself and can never become the target
|
|
193
|
+
* of a monthly subscription.
|
|
125
194
|
*/
|
|
126
195
|
export function recognizedStripePriceIdsBySelfServeTier() {
|
|
127
196
|
const byTier = {
|
|
128
197
|
oxygen: [
|
|
129
198
|
...PURCHASABLE_PLAN_KEYS.flatMap((planKey) => Object.values(STRIPE_PRICE_OXYGEN_IDS[planKey])),
|
|
199
|
+
...configuredAnnualPlanPriceIds(),
|
|
130
200
|
...LEGACY_CURRENT_PLAN_PRICE_IDS.oxygen,
|
|
131
201
|
],
|
|
132
202
|
starter: [
|