@oxygen-agent/cli 1.1011.855 → 1.1011.1091
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/admin-lead-magnet-commands.d.ts +7 -0
- package/dist/admin-lead-magnet-commands.js +128 -0
- package/dist/command-manifest.js +29 -0
- package/dist/crm-support-flags.d.ts +1 -0
- package/dist/crm-support-flags.js +5 -1
- package/dist/customer-operation-commands.js +30 -1
- package/dist/functions-commands.js +1 -1
- package/dist/help.js +10 -2
- package/dist/http-client.d.ts +4 -0
- package/dist/http-client.js +21 -10
- package/dist/index.js +444 -208
- package/dist/sender-limits-flags.d.ts +7 -0
- package/dist/sender-limits-flags.js +13 -0
- package/dist/workspace-utility-flags.d.ts +7 -0
- package/dist/workspace-utility-flags.js +45 -0
- package/node_modules/@oxygen/shared/dist/attempt-provenance.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/attempt-provenance.js +83 -0
- package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/billing-anchors.js +7 -1
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +279 -13
- package/node_modules/@oxygen/shared/dist/cli-result.js +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +21 -9
- package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.d.ts +7 -1
- package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.js +5 -1
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.d.ts +14 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.js +4 -0
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +13 -11
- package/node_modules/@oxygen/shared/dist/contracts/limits-read.js +11 -1
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.js +1 -0
- package/node_modules/@oxygen/shared/dist/contracts/schema.js +10 -6
- package/node_modules/@oxygen/shared/dist/contracts/spintax.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/contracts/spintax.js +11 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.js +21 -0
- package/node_modules/@oxygen/shared/dist/contracts/workspace-utilities.d.ts +162 -0
- package/node_modules/@oxygen/shared/dist/contracts/workspace-utilities.js +46 -0
- package/node_modules/@oxygen/shared/dist/credit-topup-review.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/credit-topup-review.js +3 -0
- package/node_modules/@oxygen/shared/dist/emailguard-placement-probe.d.ts +70 -0
- package/node_modules/@oxygen/shared/dist/emailguard-placement-probe.js +161 -0
- package/node_modules/@oxygen/shared/dist/future-signup-events.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/future-signup-events.js +4 -0
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +2 -2
- package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.d.ts +48 -0
- package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.js +94 -0
- package/node_modules/@oxygen/shared/dist/partner-coupon-bonus.d.ts +45 -0
- package/node_modules/@oxygen/shared/dist/partner-coupon-bonus.js +57 -0
- package/node_modules/@oxygen/shared/dist/provider-balance-signal.js +3 -2
- package/node_modules/@oxygen/shared/dist/table-filter-operators.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/table-filter-operators.js +50 -0
- package/node_modules/@oxygen/shared/dist/telemetry.js +4 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- 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 +1 -1
- package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.d.ts +42 -0
- package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.js +68 -0
- package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.js +33 -0
- package/node_modules/@oxygen/shared/package.json +5 -5
- package/node_modules/@oxygen/workflows/dist/index.d.ts +1 -0
- package/node_modules/@oxygen/workflows/dist/index.js +1 -0
- package/node_modules/@oxygen/workflows/dist/recipe-timestamp.d.ts +22 -0
- package/node_modules/@oxygen/workflows/dist/recipe-timestamp.js +72 -0
- package/node_modules/@oxygen/workflows/dist/usage-estimate.js +9 -5
- package/package.json +1 -1
- package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.d.ts +0 -9
- package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.js +0 -33
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { arr, bool, enumOf, int, nullable, obj, str } from "./schema.js";
|
|
2
|
+
export const betaFeaturesUpdateContract = obj({
|
|
3
|
+
enabled: bool({ description: "Show beta features in this workspace's navigation. Admin or owner required." }),
|
|
4
|
+
}, { required: ["enabled"] });
|
|
5
|
+
export const providerLimitSelectorContract = obj({
|
|
6
|
+
provider: str({ minLength: 1, description: "Provider ID, e.g. hubspot or leadmagic." }),
|
|
7
|
+
operation: str({ description: "Exact operation; omit to target the provider-wide policy, not every operation policy." }),
|
|
8
|
+
}, { required: ["provider"] });
|
|
9
|
+
export const providerLimitUpdateContract = obj({
|
|
10
|
+
provider: providerLimitSelectorContract.properties.provider,
|
|
11
|
+
operation: nullable(providerLimitSelectorContract.properties.operation),
|
|
12
|
+
limit: int({ minimum: 1, description: "Enforced calls allowed per window." }),
|
|
13
|
+
window_seconds: int({ minimum: 1, description: "Window in seconds; 86400 sets an explicit daily policy." }),
|
|
14
|
+
}, { required: ["provider", "limit", "window_seconds"] });
|
|
15
|
+
export const SUPPRESSION_BULK_MAX_KEYS = 500;
|
|
16
|
+
export const utilityListWindowProperties = {
|
|
17
|
+
view: enumOf(["full", "summary"], { description: "Full keeps native fields; summary names omitted evidence fields in projection. HTTP/CLI default full." }),
|
|
18
|
+
max_chars: int({ minimum: 2_000, maximum: 40_000, description: "Native context window; MCP caps it at 10000 (external) or 4000 (built-in). Paging.next_offset follows the last exposed row." }),
|
|
19
|
+
};
|
|
20
|
+
export const utilityListWindowContract = obj(utilityListWindowProperties);
|
|
21
|
+
export const providerLimitListContract = obj({
|
|
22
|
+
provider: providerLimitSelectorContract.properties.provider,
|
|
23
|
+
limit: int({ minimum: 1, maximum: 500, description: "Saved policies per page; omit for the legacy full list." }),
|
|
24
|
+
offset: int({ minimum: 0, description: "Current-policy offset; repeat the same provider filter. Concurrent edits may shift rows." }),
|
|
25
|
+
...utilityListWindowProperties,
|
|
26
|
+
});
|
|
27
|
+
export const emailSuppressionListContract = obj({
|
|
28
|
+
reason: enumOf(["hard_bounce", "complaint", "unsubscribe", "manual"]),
|
|
29
|
+
search: str({ description: "Case-insensitive address substring." }),
|
|
30
|
+
limit: int({ minimum: 1, maximum: 500, clamp: true, description: "Page size, 1-500; API default 100." }),
|
|
31
|
+
offset: int({ minimum: 0, description: "Current-ledger offset; repeat the same filters. Concurrent changes may shift rows." }),
|
|
32
|
+
...utilityListWindowProperties,
|
|
33
|
+
});
|
|
34
|
+
export const emailDomainSuppressionListContract = obj({
|
|
35
|
+
...emailSuppressionListContract.properties,
|
|
36
|
+
reason: enumOf(["manual", "complaint", "policy"]),
|
|
37
|
+
search: str({ description: "Case-insensitive bare-domain substring." }),
|
|
38
|
+
});
|
|
39
|
+
export const emailSuppressionRemoveContract = obj({
|
|
40
|
+
email: str({ minLength: 1, description: "One full email address. Supply email or emails, never both." }),
|
|
41
|
+
emails: arr(str(), { minItems: 1, maxItems: SUPPRESSION_BULK_MAX_KEYS, description: "Addresses to unblock. Supply emails or email, never both." }),
|
|
42
|
+
});
|
|
43
|
+
export const emailDomainSuppressionRemoveContract = obj({
|
|
44
|
+
domain: str({ minLength: 1, description: "One bare domain, e.g. acme.com. Supply domain or domains, never both." }),
|
|
45
|
+
domains: arr(str(), { minItems: 1, maxItems: SUPPRESSION_BULK_MAX_KEYS, description: "Bare domains to unblock. Supply domains or domain, never both." }),
|
|
46
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Fixed customer outcome for a verified paid top-up awaiting manual review. */
|
|
2
|
+
export declare const CREDIT_TOPUP_REVIEW_CODE = "credit_topup_review_required";
|
|
3
|
+
export declare const CREDIT_TOPUP_REVIEW_MESSAGE = "Payment received. Credits need review. Check billing or contact support before buying again.";
|
|
4
|
+
export type CreditTopupReviewReason = "organization_missing" | "credits_invalid" | "quoted_amount_invalid" | "payment_intent_missing";
|
|
5
|
+
/** Contains no Stripe identifiers, invalid metadata or staff diagnostics. */
|
|
6
|
+
export type CreditTopupReviewNotice = {
|
|
7
|
+
code: typeof CREDIT_TOPUP_REVIEW_CODE;
|
|
8
|
+
message: typeof CREDIT_TOPUP_REVIEW_MESSAGE;
|
|
9
|
+
count: number;
|
|
10
|
+
web_url: string;
|
|
11
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Fixed customer outcome for a verified paid top-up awaiting manual review. */
|
|
2
|
+
export const CREDIT_TOPUP_REVIEW_CODE = "credit_topup_review_required";
|
|
3
|
+
export const CREDIT_TOPUP_REVIEW_MESSAGE = "Payment received. Credits need review. Check billing or contact support before buying again.";
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
type MailboxTransport = "zapbox" | "native_oauth" | "delegation" | "provisioning" | "ineligible";
|
|
2
|
+
export type EmailGuardSeedSendPlan = {
|
|
3
|
+
placement_test_id: string;
|
|
4
|
+
from: string;
|
|
5
|
+
transport: MailboxTransport;
|
|
6
|
+
recipients: string[];
|
|
7
|
+
to: string;
|
|
8
|
+
bcc: string[];
|
|
9
|
+
subject: string;
|
|
10
|
+
body: string;
|
|
11
|
+
test_phrase: string;
|
|
12
|
+
allow_paused_probe?: true;
|
|
13
|
+
plan_hash: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Build the exact external-write plan shown in preview and bound by plan_hash.
|
|
17
|
+
* EmailGuard requires one new message to its whole seed set containing the
|
|
18
|
+
* unique filter phrase. Oxygen uses one primary To recipient plus Bcc for the
|
|
19
|
+
* remaining seeds so every inbox receives the same MIME message without
|
|
20
|
+
* exposing EmailGuard's full seed set to each recipient.
|
|
21
|
+
*/
|
|
22
|
+
export declare function buildEmailGuardSeedSendPlan(input: {
|
|
23
|
+
placementTestId: string;
|
|
24
|
+
from: string;
|
|
25
|
+
transport: MailboxTransport;
|
|
26
|
+
seedAddresses: unknown;
|
|
27
|
+
testPhrase: string | null;
|
|
28
|
+
subject: string | null;
|
|
29
|
+
bodyText: string | null;
|
|
30
|
+
allowPausedProbe?: boolean;
|
|
31
|
+
}): EmailGuardSeedSendPlan;
|
|
32
|
+
/** One permanent delivery identity for the lifetime of this provider-created test. */
|
|
33
|
+
export declare function emailGuardProbeDeliveryKey(testId: string): string;
|
|
34
|
+
export declare function emailGuardSeedSendPayload(plan: EmailGuardSeedSendPlan): {
|
|
35
|
+
kind: "email_send";
|
|
36
|
+
to: string;
|
|
37
|
+
subject: string;
|
|
38
|
+
body: string;
|
|
39
|
+
bcc?: string[];
|
|
40
|
+
apply_mailbox_signature: boolean;
|
|
41
|
+
};
|
|
42
|
+
export declare function placementTestAllowsPausedProbe(results: unknown): boolean;
|
|
43
|
+
/** Untrusted job labels never supply pause authority: prove the durable exact approval. */
|
|
44
|
+
export declare function approvedPausedEmailGuardProbeMatches(input: {
|
|
45
|
+
organizationId: string;
|
|
46
|
+
mailboxId: string;
|
|
47
|
+
from: string;
|
|
48
|
+
transport: MailboxTransport;
|
|
49
|
+
job: {
|
|
50
|
+
id: string;
|
|
51
|
+
source: string;
|
|
52
|
+
idempotencyKey: string | null;
|
|
53
|
+
payload: Record<string, unknown>;
|
|
54
|
+
};
|
|
55
|
+
test: {
|
|
56
|
+
id: string;
|
|
57
|
+
provider: string;
|
|
58
|
+
status: string;
|
|
59
|
+
externalId: string | null;
|
|
60
|
+
mailboxIds: string[];
|
|
61
|
+
seedAddresses: unknown;
|
|
62
|
+
testPhrase: string | null;
|
|
63
|
+
subject: string | null;
|
|
64
|
+
bodyText: string | null;
|
|
65
|
+
results: unknown;
|
|
66
|
+
};
|
|
67
|
+
}): boolean;
|
|
68
|
+
/** Compare every externally meaningful queued field, permitting only private correlation beside it. */
|
|
69
|
+
export declare function emailGuardProbePayloadMatches(plan: EmailGuardSeedSendPlan, payload: Record<string, unknown>): boolean;
|
|
70
|
+
export {};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { CliHttpError } from "./cli-http-error.js";
|
|
3
|
+
const MAX_SEED_RECIPIENTS = 50;
|
|
4
|
+
const DEFAULT_SUBJECT = "Oxygen inbox-placement test";
|
|
5
|
+
const DEFAULT_BODY = "This message is an inbox-placement test sent from Oxygen.";
|
|
6
|
+
function singleEmail(value) {
|
|
7
|
+
if (typeof value !== "string")
|
|
8
|
+
return null;
|
|
9
|
+
const address = value.trim().toLowerCase();
|
|
10
|
+
if (!address || !address.includes("@") || /[\s\r\n,;]/.test(address)) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
return address;
|
|
14
|
+
}
|
|
15
|
+
function seedAddresses(value) {
|
|
16
|
+
if (!Array.isArray(value)) {
|
|
17
|
+
throw new CliHttpError(409, "emailguard_seed_set_missing", "EmailGuard did not return a usable seed-recipient list for this placement test.", { exitCode: 1 });
|
|
18
|
+
}
|
|
19
|
+
const recipients = [];
|
|
20
|
+
for (const raw of value) {
|
|
21
|
+
const address = singleEmail(raw);
|
|
22
|
+
if (!address) {
|
|
23
|
+
throw new CliHttpError(409, "emailguard_seed_set_invalid", "EmailGuard returned an invalid seed recipient; no email was sent.", {
|
|
24
|
+
details: { recipient: typeof raw === "string" ? raw : null },
|
|
25
|
+
exitCode: 1,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (!recipients.includes(address))
|
|
29
|
+
recipients.push(address);
|
|
30
|
+
}
|
|
31
|
+
if (recipients.length === 0 || recipients.length > MAX_SEED_RECIPIENTS) {
|
|
32
|
+
throw new CliHttpError(409, "emailguard_seed_set_invalid", `EmailGuard must return between 1 and ${MAX_SEED_RECIPIENTS} unique seed recipients; no email was sent.`, { details: { recipient_count: recipients.length }, exitCode: 1 });
|
|
33
|
+
}
|
|
34
|
+
return recipients;
|
|
35
|
+
}
|
|
36
|
+
function headerLine(value, fallback) {
|
|
37
|
+
const line = value?.trim() || fallback;
|
|
38
|
+
if (/[\r\n]/.test(line) || line.length > 998) {
|
|
39
|
+
throw new CliHttpError(400, "invalid_request", "The placement-test subject must be a single line no longer than 998 characters.", { exitCode: 1 });
|
|
40
|
+
}
|
|
41
|
+
return line;
|
|
42
|
+
}
|
|
43
|
+
function messageBody(value, phrase) {
|
|
44
|
+
const base = value?.trim() || DEFAULT_BODY;
|
|
45
|
+
if (base.includes(phrase))
|
|
46
|
+
return base;
|
|
47
|
+
return `${base}\n\nEmailGuard test phrase: ${phrase}`;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Build the exact external-write plan shown in preview and bound by plan_hash.
|
|
51
|
+
* EmailGuard requires one new message to its whole seed set containing the
|
|
52
|
+
* unique filter phrase. Oxygen uses one primary To recipient plus Bcc for the
|
|
53
|
+
* remaining seeds so every inbox receives the same MIME message without
|
|
54
|
+
* exposing EmailGuard's full seed set to each recipient.
|
|
55
|
+
*/
|
|
56
|
+
export function buildEmailGuardSeedSendPlan(input) {
|
|
57
|
+
const from = singleEmail(input.from);
|
|
58
|
+
if (!from) {
|
|
59
|
+
throw new CliHttpError(409, "mailbox_not_sendable", "The placement-test sender address is invalid.", {
|
|
60
|
+
exitCode: 1,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (input.transport === "ineligible") {
|
|
64
|
+
throw new CliHttpError(409, "mailbox_not_sendable", `Mailbox ${from} has no usable send transport. Connect Zapbox, mailbox OAuth, or deployment delegation before sending the deliverability-test probe.`, { details: { mailbox: from }, exitCode: 1 });
|
|
65
|
+
}
|
|
66
|
+
const phrase = input.testPhrase?.trim();
|
|
67
|
+
if (!phrase) {
|
|
68
|
+
throw new CliHttpError(409, "emailguard_test_phrase_missing", "EmailGuard did not return the mandatory filter phrase; no email was sent.", { exitCode: 1 });
|
|
69
|
+
}
|
|
70
|
+
const recipients = seedAddresses(input.seedAddresses);
|
|
71
|
+
const subject = headerLine(input.subject, DEFAULT_SUBJECT);
|
|
72
|
+
const body = messageBody(input.bodyText, phrase);
|
|
73
|
+
const planWithoutHash = {
|
|
74
|
+
placement_test_id: input.placementTestId,
|
|
75
|
+
from,
|
|
76
|
+
transport: input.transport,
|
|
77
|
+
recipients,
|
|
78
|
+
to: recipients[0],
|
|
79
|
+
bcc: recipients.slice(1),
|
|
80
|
+
subject,
|
|
81
|
+
body,
|
|
82
|
+
test_phrase: phrase,
|
|
83
|
+
...(input.allowPausedProbe ? { allow_paused_probe: true } : {}),
|
|
84
|
+
};
|
|
85
|
+
const planHash = createHash("sha256")
|
|
86
|
+
.update(JSON.stringify({ version: 1, ...planWithoutHash }))
|
|
87
|
+
.digest("hex");
|
|
88
|
+
return { ...planWithoutHash, plan_hash: planHash };
|
|
89
|
+
}
|
|
90
|
+
/** One permanent delivery identity for the lifetime of this provider-created test. */
|
|
91
|
+
export function emailGuardProbeDeliveryKey(testId) {
|
|
92
|
+
return `placement_test:${createHash("sha256").update(testId).digest("hex")}`;
|
|
93
|
+
}
|
|
94
|
+
export function emailGuardSeedSendPayload(plan) {
|
|
95
|
+
return {
|
|
96
|
+
kind: "email_send", to: plan.to, subject: plan.subject, body: plan.body,
|
|
97
|
+
...(plan.bcc.length ? { bcc: plan.bcc } : {}), apply_mailbox_signature: false,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function record(value) {
|
|
101
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
102
|
+
? value : {};
|
|
103
|
+
}
|
|
104
|
+
function equalJson(left, right) {
|
|
105
|
+
if (left === right)
|
|
106
|
+
return true;
|
|
107
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
108
|
+
return Array.isArray(left) && Array.isArray(right) && left.length === right.length
|
|
109
|
+
&& left.every((value, index) => equalJson(value, right[index]));
|
|
110
|
+
}
|
|
111
|
+
if (!left || !right || typeof left !== "object" || typeof right !== "object")
|
|
112
|
+
return false;
|
|
113
|
+
const a = record(left);
|
|
114
|
+
const b = record(right);
|
|
115
|
+
const keys = Object.keys(a);
|
|
116
|
+
return keys.length === Object.keys(b).length && keys.every(key => Object.hasOwn(b, key) && equalJson(a[key], b[key]));
|
|
117
|
+
}
|
|
118
|
+
export function placementTestAllowsPausedProbe(results) {
|
|
119
|
+
const ledger = record(results);
|
|
120
|
+
const create = record(ledger.create_operation);
|
|
121
|
+
return ledger.paused_probe_opt_in === true
|
|
122
|
+
&& ["succeeded", "recovered"].includes(String(create.phase))
|
|
123
|
+
&& typeof create.plan_hash === "string" && /^[a-f0-9]{64}$/.test(create.plan_hash);
|
|
124
|
+
}
|
|
125
|
+
/** Untrusted job labels never supply pause authority: prove the durable exact approval. */
|
|
126
|
+
export function approvedPausedEmailGuardProbeMatches(input) {
|
|
127
|
+
const { test, job } = input;
|
|
128
|
+
if (test.provider !== "emailguard" || test.status !== "sending" || !test.externalId
|
|
129
|
+
|| test.mailboxIds.length !== 1 || test.mailboxIds[0] !== input.mailboxId
|
|
130
|
+
|| job.source !== "placement_test" || !placementTestAllowsPausedProbe(test.results))
|
|
131
|
+
return false;
|
|
132
|
+
const receipt = record(record(test.results).seed_delivery);
|
|
133
|
+
const authorization = record(receipt.authorization);
|
|
134
|
+
const create = record(record(test.results).create_operation);
|
|
135
|
+
const correlation = record(job.payload.correlation);
|
|
136
|
+
if (!["claimed", "queued"].includes(String(receipt.status)) || authorization.version !== 1
|
|
137
|
+
|| authorization.organization_id !== input.organizationId || authorization.mailbox_id !== input.mailboxId
|
|
138
|
+
|| authorization.create_plan_hash !== create.plan_hash || authorization.allow_paused_probe !== true
|
|
139
|
+
|| authorization.idempotency_key !== emailGuardProbeDeliveryKey(test.id)
|
|
140
|
+
|| job.idempotencyKey !== authorization.idempotency_key
|
|
141
|
+
|| (receipt.job_id != null && receipt.job_id !== job.id)
|
|
142
|
+
|| correlation.source !== "placement_test" || correlation.placement_test_id !== test.id
|
|
143
|
+
|| correlation.organization_id !== input.organizationId
|
|
144
|
+
|| record(correlation.daily_slot).mailbox_id !== input.mailboxId)
|
|
145
|
+
return false;
|
|
146
|
+
try {
|
|
147
|
+
const plan = buildEmailGuardSeedSendPlan({ placementTestId: test.id, from: input.from, transport: input.transport,
|
|
148
|
+
seedAddresses: test.seedAddresses, testPhrase: test.testPhrase, subject: test.subject, bodyText: test.bodyText, allowPausedProbe: true });
|
|
149
|
+
const { correlation: _correlation, ...message } = job.payload;
|
|
150
|
+
return authorization.plan_hash === plan.plan_hash && equalJson(receipt.plan, plan)
|
|
151
|
+
&& equalJson(message, emailGuardSeedSendPayload(plan));
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/** Compare every externally meaningful queued field, permitting only private correlation beside it. */
|
|
158
|
+
export function emailGuardProbePayloadMatches(plan, payload) {
|
|
159
|
+
const { correlation: _correlation, ...message } = payload;
|
|
160
|
+
return equalJson(message, emailGuardSeedSendPayload(plan));
|
|
161
|
+
}
|
|
@@ -11,7 +11,7 @@ export declare const FUTURE_SIGNUP_LIFECYCLE_PROJECTION_CONTRACT_VERSION_V1: "fu
|
|
|
11
11
|
export declare const FUTURE_SIGNUP_LIFECYCLE_PROJECTION_CONTRACT_VERSION_V2: "future_signup_lifecycle_projection.v2";
|
|
12
12
|
export declare const FUTURE_SIGNUP_LIFECYCLE_PROJECTION_CONTRACT_VERSION: "future_signup_lifecycle_projection.v1";
|
|
13
13
|
export declare const FUTURE_SIGNUP_CRM_DESTINATION = "oxygen_company_crm";
|
|
14
|
-
export declare const FUTURE_SIGNUP_ACTIVATION_COMMANDS: readonly ["agent create", "crm activity log", "crm assert", "crm object add-attr", "crm object create", "crm relationships define", "crm relationships upsert", "knowledge page upsert", "publishing posts create", "sequences create", "tables create", "tables duplicate", "tables import-csv", "tables import-file", "tables import-staged", "tables import-url", "tables insert", "tables relate", "tables update", "tables upsert", "workflows apply"];
|
|
14
|
+
export declare const FUTURE_SIGNUP_ACTIVATION_COMMANDS: readonly ["agent create", "crm activity log", "crm assert", "crm object add-attr", "crm object create", "crm objects add-attr", "crm objects create", "crm relationships define", "crm relationships upsert", "knowledge page upsert", "publishing posts create", "sequences create", "tables create", "tables duplicate", "tables import-csv", "tables import-file", "tables import-staged", "tables import-url", "tables insert", "tables relate", "tables update", "tables upsert", "workflows apply"];
|
|
15
15
|
export declare function isFutureSignupActivationCommand(command: string): boolean;
|
|
16
16
|
export declare function futureSignupJourneyId(clerkUserId: string): string;
|
|
17
17
|
export declare function futureSignupFactId(clerkUserId: string): string;
|
|
@@ -15,8 +15,12 @@ export const FUTURE_SIGNUP_ACTIVATION_COMMANDS = [
|
|
|
15
15
|
"agent create",
|
|
16
16
|
"crm activity log",
|
|
17
17
|
"crm assert",
|
|
18
|
+
// Operations recorded before the label matched the real `crm objects`
|
|
19
|
+
// command keep the singular form; both name the same command.
|
|
18
20
|
"crm object add-attr",
|
|
19
21
|
"crm object create",
|
|
22
|
+
"crm objects add-attr",
|
|
23
|
+
"crm objects create",
|
|
20
24
|
"crm relationships define",
|
|
21
25
|
"crm relationships upsert",
|
|
22
26
|
"knowledge page upsert",
|
|
@@ -137,7 +137,7 @@ const ICP_BODY = `# ICP — ideal customer profile
|
|
|
137
137
|
`;
|
|
138
138
|
const POSITIONING_BODY = `# Positioning
|
|
139
139
|
|
|
140
|
-
> **Stub — fill me.** Non-canonical draft. Fill it, then
|
|
140
|
+
> **Stub — fill me.** Non-canonical draft. Fill it, then pin it (\`oxygen knowledge page pin positioning\`): the pin files a proposal a human approves, because canonical positioning grounds live copy. See [[conventions]].
|
|
141
141
|
|
|
142
142
|
## For whom
|
|
143
143
|
- Our [[icp]], and the moment they buy.
|
|
@@ -251,7 +251,7 @@ Banned hedges: just, hope this finds you well, wanted to reach out, circling bac
|
|
|
251
251
|
- **We would not send:** the same idea, wrong — and why it fails
|
|
252
252
|
|
|
253
253
|
## Canonical
|
|
254
|
-
Not pinned. Pinning it (\`oxygen knowledge page pin voice
|
|
254
|
+
Not pinned. Pinning it (\`oxygen knowledge page pin voice\`) files a proposal a human approves — canonical voice grounds live copy. Claims are [[brand]]; what we do is [[positioning]].
|
|
255
255
|
`;
|
|
256
256
|
const BRAND_BODY = `# Brand
|
|
257
257
|
|
|
@@ -31,3 +31,51 @@ export declare function linkedInQuotaBackoffUntil(error: unknown, now: Date): Da
|
|
|
31
31
|
* `sender_accounts.metadata` jsonb — expires; 0 when it is not parked.
|
|
32
32
|
*/
|
|
33
33
|
export declare function linkedInQuotaParkedUntil(metadata: unknown, key: string): number;
|
|
34
|
+
/**
|
|
35
|
+
* Where a sender's inbox sync is parked until, quota or failure backoff alike,
|
|
36
|
+
* in the freeform `sender_accounts.metadata` jsonb.
|
|
37
|
+
*/
|
|
38
|
+
export declare const INBOX_SYNC_BACKOFF_KEY = "inbox_sync_backoff_until";
|
|
39
|
+
/** The sender metadata key holding the latest non-quota inbox sync failure. */
|
|
40
|
+
export declare const INBOX_SYNC_FAILURE_KEY = "inbox_sync_failure";
|
|
41
|
+
export type InboxSyncFailureClass = "quota" | "authentication" | "transient" | "other";
|
|
42
|
+
export type InboxSyncFailureRecord = {
|
|
43
|
+
class: Exclude<InboxSyncFailureClass, "quota">;
|
|
44
|
+
code: string | null;
|
|
45
|
+
status: number | null;
|
|
46
|
+
/** Consecutive failed syncs, reset by a successful one. */
|
|
47
|
+
consecutive: number;
|
|
48
|
+
first_at: string;
|
|
49
|
+
last_at: string;
|
|
50
|
+
retry_at: string;
|
|
51
|
+
};
|
|
52
|
+
/** First park after a non-quota failure; it doubles per consecutive failure. */
|
|
53
|
+
export declare const INBOX_SYNC_FAILURE_BASE_BACKOFF_MS: number;
|
|
54
|
+
/** No non-quota failure parks a sender longer than this. */
|
|
55
|
+
export declare const INBOX_SYNC_FAILURE_MAX_BACKOFF_MS: number;
|
|
56
|
+
/** Quota, authentication (401/403), transient (capacity, 429, 5xx, retryable), else other. */
|
|
57
|
+
export declare function classifyInboxSyncFailure(error: unknown): InboxSyncFailureClass;
|
|
58
|
+
/** The latest non-quota inbox sync failure recorded on a sender, or null. */
|
|
59
|
+
export declare function readInboxSyncFailure(metadata: unknown): InboxSyncFailureRecord | null;
|
|
60
|
+
/**
|
|
61
|
+
* The park and record for a whole-sender inbox sync failure that is not a quota
|
|
62
|
+
* denial: 5 min, doubling per consecutive failure, capped at 2 h. Null for a
|
|
63
|
+
* quota denial, which keeps its own park (linkedInQuotaBackoffUntil).
|
|
64
|
+
*/
|
|
65
|
+
export declare function inboxSyncFailureBackoff(error: unknown, metadata: unknown, now: Date): {
|
|
66
|
+
until: Date;
|
|
67
|
+
record: InboxSyncFailureRecord;
|
|
68
|
+
} | null;
|
|
69
|
+
/**
|
|
70
|
+
* A sender's whole-inbox sync state for health reads: the last successful sync,
|
|
71
|
+
* the latest failure that was not a quota denial (cleared by the next success),
|
|
72
|
+
* and until when the background sync leaves the sender alone.
|
|
73
|
+
*/
|
|
74
|
+
export declare function inboxSyncHealth(sender: {
|
|
75
|
+
lastInboxSyncAt: Date | null;
|
|
76
|
+
metadata: unknown;
|
|
77
|
+
}, now?: Date): {
|
|
78
|
+
last_synced_at: string | null;
|
|
79
|
+
background_sync_paused_until: string | null;
|
|
80
|
+
last_failure: InboxSyncFailureRecord | null;
|
|
81
|
+
};
|
|
@@ -70,3 +70,97 @@ export function linkedInQuotaParkedUntil(metadata, key) {
|
|
|
70
70
|
const ms = Date.parse(raw);
|
|
71
71
|
return Number.isNaN(ms) ? 0 : ms;
|
|
72
72
|
}
|
|
73
|
+
// ===== Inbox sync failures that are not quota denials (OXP-1.11.72) =====
|
|
74
|
+
//
|
|
75
|
+
// A quota denial parks the sender until its budget resets (above). Every other
|
|
76
|
+
// whole-sender inbox sync failure used to leave the sender eligible on the next
|
|
77
|
+
// backstop pass: in the week to 2026-10-07 production retried one sender 45
|
|
78
|
+
// times through "Managed provider capacity is busy." and 24 senders every few
|
|
79
|
+
// minutes through a 27-minute LinkedIn 5xx incident, and nothing on the sender
|
|
80
|
+
// said so. These failures now park on a bounded, doubling backoff and leave an
|
|
81
|
+
// inspectable record that the next successful sync clears.
|
|
82
|
+
/**
|
|
83
|
+
* Where a sender's inbox sync is parked until, quota or failure backoff alike,
|
|
84
|
+
* in the freeform `sender_accounts.metadata` jsonb.
|
|
85
|
+
*/
|
|
86
|
+
export const INBOX_SYNC_BACKOFF_KEY = "inbox_sync_backoff_until";
|
|
87
|
+
/** The sender metadata key holding the latest non-quota inbox sync failure. */
|
|
88
|
+
export const INBOX_SYNC_FAILURE_KEY = "inbox_sync_failure";
|
|
89
|
+
// Refusals raised before the read leaves the process that clear on their own:
|
|
90
|
+
// the managed-provider limiter after waiting out its window, a runtime with no
|
|
91
|
+
// quota enforcer registered, and an unsizable worker fleet. The same set the
|
|
92
|
+
// LinkedIn ingestion drip defers on.
|
|
93
|
+
const TRANSIENT_CODES = new Set([
|
|
94
|
+
"rate_limit_exceeded",
|
|
95
|
+
"linkedin_quota_enforcement_unavailable",
|
|
96
|
+
"worker_fleet_discovery_unavailable",
|
|
97
|
+
]);
|
|
98
|
+
/** First park after a non-quota failure; it doubles per consecutive failure. */
|
|
99
|
+
export const INBOX_SYNC_FAILURE_BASE_BACKOFF_MS = 5 * 60 * 1000;
|
|
100
|
+
/** No non-quota failure parks a sender longer than this. */
|
|
101
|
+
export const INBOX_SYNC_FAILURE_MAX_BACKOFF_MS = 2 * 60 * 60 * 1000;
|
|
102
|
+
function errorStatus(error) {
|
|
103
|
+
const status = isRecord(error) ? error.status : undefined;
|
|
104
|
+
return typeof status === "number" && Number.isFinite(status) ? status : null;
|
|
105
|
+
}
|
|
106
|
+
/** Quota, authentication (401/403), transient (capacity, 429, 5xx, retryable), else other. */
|
|
107
|
+
export function classifyInboxSyncFailure(error) {
|
|
108
|
+
if (isLinkedInQuotaDenial(error))
|
|
109
|
+
return "quota";
|
|
110
|
+
const status = errorStatus(error);
|
|
111
|
+
if (status === 401 || status === 403)
|
|
112
|
+
return "authentication";
|
|
113
|
+
if (error instanceof OxygenError && TRANSIENT_CODES.has(error.code))
|
|
114
|
+
return "transient";
|
|
115
|
+
if (status === 429 || (status !== null && status >= 500))
|
|
116
|
+
return "transient";
|
|
117
|
+
if (isRecord(error) && error.retryable === true)
|
|
118
|
+
return "transient";
|
|
119
|
+
return "other";
|
|
120
|
+
}
|
|
121
|
+
/** The latest non-quota inbox sync failure recorded on a sender, or null. */
|
|
122
|
+
export function readInboxSyncFailure(metadata) {
|
|
123
|
+
const raw = isRecord(metadata) ? metadata[INBOX_SYNC_FAILURE_KEY] : undefined;
|
|
124
|
+
if (!isRecord(raw) || typeof raw.consecutive !== "number" || raw.consecutive < 1 || typeof raw.first_at !== "string")
|
|
125
|
+
return null;
|
|
126
|
+
return raw;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The park and record for a whole-sender inbox sync failure that is not a quota
|
|
130
|
+
* denial: 5 min, doubling per consecutive failure, capped at 2 h. Null for a
|
|
131
|
+
* quota denial, which keeps its own park (linkedInQuotaBackoffUntil).
|
|
132
|
+
*/
|
|
133
|
+
export function inboxSyncFailureBackoff(error, metadata, now) {
|
|
134
|
+
const failureClass = classifyInboxSyncFailure(error);
|
|
135
|
+
if (failureClass === "quota")
|
|
136
|
+
return null;
|
|
137
|
+
const previous = readInboxSyncFailure(metadata);
|
|
138
|
+
const consecutive = (previous?.consecutive ?? 0) + 1;
|
|
139
|
+
const delay = Math.min(INBOX_SYNC_FAILURE_BASE_BACKOFF_MS * 2 ** Math.min(consecutive - 1, 16), INBOX_SYNC_FAILURE_MAX_BACKOFF_MS);
|
|
140
|
+
const until = new Date(now.getTime() + delay);
|
|
141
|
+
return {
|
|
142
|
+
until,
|
|
143
|
+
record: {
|
|
144
|
+
class: failureClass,
|
|
145
|
+
code: error instanceof OxygenError ? error.code : null,
|
|
146
|
+
status: errorStatus(error),
|
|
147
|
+
consecutive,
|
|
148
|
+
first_at: previous?.first_at ?? now.toISOString(),
|
|
149
|
+
last_at: now.toISOString(),
|
|
150
|
+
retry_at: until.toISOString(),
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* A sender's whole-inbox sync state for health reads: the last successful sync,
|
|
156
|
+
* the latest failure that was not a quota denial (cleared by the next success),
|
|
157
|
+
* and until when the background sync leaves the sender alone.
|
|
158
|
+
*/
|
|
159
|
+
export function inboxSyncHealth(sender, now = new Date()) {
|
|
160
|
+
const parkedUntil = linkedInQuotaParkedUntil(sender.metadata, INBOX_SYNC_BACKOFF_KEY);
|
|
161
|
+
return {
|
|
162
|
+
last_synced_at: sender.lastInboxSyncAt ? sender.lastInboxSyncAt.toISOString() : null,
|
|
163
|
+
background_sync_paused_until: parkedUntil > now.getTime() ? new Date(parkedUntil).toISOString() : null,
|
|
164
|
+
last_failure: readInboxSyncFailure(sender.metadata),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type RepricingSwitchState } from "./repricing.js";
|
|
2
|
+
/** September 30 P3 ratification; both existing PARTNERS2026 coupon cohorts. */
|
|
3
|
+
export declare const PARTNER_COUPON_IDS: readonly ["jcQhT5r5", "0wTy2bAv"];
|
|
4
|
+
export declare const PARTNER_COUPON_BONUS_EXCLUSION_EFFECTIVE_AT_ENV = "OXYGEN_PARTNER_COUPON_BONUS_EXCLUSION_EFFECTIVE_AT";
|
|
5
|
+
export declare function readPartnerCouponBonusExclusion(options?: {
|
|
6
|
+
now?: Date;
|
|
7
|
+
env?: Record<string, string | undefined>;
|
|
8
|
+
}): RepricingSwitchState;
|
|
9
|
+
export type PartnerCouponBinding = {
|
|
10
|
+
organizationId: string;
|
|
11
|
+
subscriptionId: string;
|
|
12
|
+
customerId: string;
|
|
13
|
+
priceId: string;
|
|
14
|
+
planKey: string;
|
|
15
|
+
periodStart: string;
|
|
16
|
+
periodEnd: string;
|
|
17
|
+
};
|
|
18
|
+
export type PartnerCouponApplication = {
|
|
19
|
+
id: string;
|
|
20
|
+
layer: "subscription" | "plan_item" | "customer";
|
|
21
|
+
couponId: string | null;
|
|
22
|
+
start: number | null;
|
|
23
|
+
end: number | null;
|
|
24
|
+
deleted: boolean;
|
|
25
|
+
};
|
|
26
|
+
/** Trusted server capture, separate from externally supplied Stripe metadata. */
|
|
27
|
+
export type PartnerCouponAuthority = {
|
|
28
|
+
version: 1;
|
|
29
|
+
capturedAt: string;
|
|
30
|
+
binding: PartnerCouponBinding;
|
|
31
|
+
planItemId: string | null;
|
|
32
|
+
status: "complete" | "unknown";
|
|
33
|
+
reason: string | null;
|
|
34
|
+
applications: PartnerCouponApplication[];
|
|
35
|
+
};
|
|
36
|
+
export type PartnerCouponAuthorityStatus = {
|
|
37
|
+
state: "active_partner_coupon" | "no_active_partner_coupon" | "unknown";
|
|
38
|
+
capturedAt: string | null;
|
|
39
|
+
basis: "captured_stripe_discount_applications" | "unavailable";
|
|
40
|
+
reason: string | null;
|
|
41
|
+
};
|
|
42
|
+
/** Evaluate only the captured instant; a stored absence is never a live check. */
|
|
43
|
+
export declare function readCapturedPartnerCouponAuthority(authority: PartnerCouponAuthority | null | undefined, binding: PartnerCouponBinding | null): PartnerCouponAuthorityStatus;
|
|
44
|
+
/** Live issuance evaluates known application bounds at its clock, retaining capture provenance. */
|
|
45
|
+
export declare function readPartnerCouponAuthorityAt(authority: PartnerCouponAuthority | null | undefined, binding: PartnerCouponBinding | null, applicationInstant?: Date): PartnerCouponAuthorityStatus;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { parseRepricingEffectiveAt } from "./repricing.js";
|
|
2
|
+
/** September 30 P3 ratification; both existing PARTNERS2026 coupon cohorts. */
|
|
3
|
+
export const PARTNER_COUPON_IDS = ["jcQhT5r5", "0wTy2bAv"];
|
|
4
|
+
export const PARTNER_COUPON_BONUS_EXCLUSION_EFFECTIVE_AT_ENV = "OXYGEN_PARTNER_COUPON_BONUS_EXCLUSION_EFFECTIVE_AT";
|
|
5
|
+
export function readPartnerCouponBonusExclusion(options = {}) {
|
|
6
|
+
const raw = (options.env ?? process.env)[PARTNER_COUPON_BONUS_EXCLUSION_EFFECTIVE_AT_ENV];
|
|
7
|
+
const effectiveAt = parseRepricingEffectiveAt(raw);
|
|
8
|
+
if (effectiveAt === null)
|
|
9
|
+
return { status: "unset", effectiveAt: null };
|
|
10
|
+
if (effectiveAt === undefined)
|
|
11
|
+
return { status: "invalid", effectiveAt: null, raw: raw?.trim() ?? "" };
|
|
12
|
+
return (options.now ?? new Date()).getTime() >= effectiveAt.getTime()
|
|
13
|
+
? { status: "in_force", effectiveAt }
|
|
14
|
+
: { status: "scheduled", effectiveAt };
|
|
15
|
+
}
|
|
16
|
+
/** Evaluate only the captured instant; a stored absence is never a live check. */
|
|
17
|
+
export function readCapturedPartnerCouponAuthority(authority, binding) {
|
|
18
|
+
return readPartnerCouponAuthorityAt(authority, binding);
|
|
19
|
+
}
|
|
20
|
+
/** Live issuance evaluates known application bounds at its clock, retaining capture provenance. */
|
|
21
|
+
export function readPartnerCouponAuthorityAt(authority, binding, applicationInstant) {
|
|
22
|
+
const unknown = (reason) => ({
|
|
23
|
+
state: "unknown", capturedAt: authority?.capturedAt ?? null, basis: "unavailable", reason,
|
|
24
|
+
});
|
|
25
|
+
if (!authority || authority.version !== 1 || !binding)
|
|
26
|
+
return unknown("authority_missing");
|
|
27
|
+
const capturedAt = parseRepricingEffectiveAt(authority.capturedAt);
|
|
28
|
+
if (!capturedAt || !Number.isFinite(capturedAt.getTime()))
|
|
29
|
+
return unknown("capture_time_invalid");
|
|
30
|
+
if (!authority.binding || Object.keys(binding).some(key => authority.binding[key] !== binding[key])) {
|
|
31
|
+
return unknown("authority_binding_changed");
|
|
32
|
+
}
|
|
33
|
+
if (authority.status !== "complete" || !authority.planItemId || !Array.isArray(authority.applications)) {
|
|
34
|
+
return unknown(authority.reason ?? "authority_incomplete");
|
|
35
|
+
}
|
|
36
|
+
const instant = (applicationInstant ?? capturedAt).getTime() / 1000;
|
|
37
|
+
if (!Number.isFinite(instant))
|
|
38
|
+
return unknown("application_time_invalid");
|
|
39
|
+
for (const application of authority.applications) {
|
|
40
|
+
if (!application || !application.id || !["subscription", "plan_item", "customer"].includes(application.layer)
|
|
41
|
+
|| typeof application.deleted !== "boolean")
|
|
42
|
+
return unknown("application_invalid");
|
|
43
|
+
if (application.deleted)
|
|
44
|
+
continue;
|
|
45
|
+
if (!application.couponId || !Number.isSafeInteger(application.start) || application.start < 0
|
|
46
|
+
|| (application.end !== null && (!Number.isSafeInteger(application.end) || application.end < application.start))) {
|
|
47
|
+
return unknown("application_invalid");
|
|
48
|
+
}
|
|
49
|
+
if (PARTNER_COUPON_IDS.includes(application.couponId)
|
|
50
|
+
&& application.start <= instant && (application.end === null || instant < application.end)) {
|
|
51
|
+
return { state: "active_partner_coupon", capturedAt: authority.capturedAt,
|
|
52
|
+
basis: "captured_stripe_discount_applications", reason: null };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { state: "no_active_partner_coupon", capturedAt: authority.capturedAt,
|
|
56
|
+
basis: "captured_stripe_discount_applications", reason: null };
|
|
57
|
+
}
|
|
@@ -101,8 +101,9 @@ export const MANAGED_BALANCE_FLOORS = {
|
|
|
101
101
|
// to us, which sits at 0.00 in the normal case (it did on 2026-09-07) and goes
|
|
102
102
|
// negative after a refund. A floor here would page daily about nothing.
|
|
103
103
|
stripe: null,
|
|
104
|
-
//
|
|
105
|
-
//
|
|
104
|
+
// No balance concept at all. Neither is probed any more (Vercel left the stack
|
|
105
|
+
// on 2026-10-06); null keeps a historical row from ever reading as a low wallet
|
|
106
|
+
// under the generic default floor.
|
|
106
107
|
vercel: null,
|
|
107
108
|
neon: null,
|
|
108
109
|
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const FILTER_TREE_OPERATORS: readonly ["is", "is_not", "contains", "not_contains", "starts_with", "ends_with", "eq", "neq", "gt", "gte", "lt", "lte", "before", "after", "on_or_before", "on_or_after", "is_true", "is_false", "is_any_of", "is_none_of", "is_empty", "is_not_empty"];
|
|
2
|
+
export type FilterTreeOperator = (typeof FILTER_TREE_OPERATORS)[number];
|
|
3
|
+
/** The filter-tree operators a column whose SQL value has `dataType` accepts. */
|
|
4
|
+
export declare function filterTreeOperatorsFor(dataType: string): FilterTreeOperator[];
|
|
5
|
+
/** The filter-tree operator a rejected operator name most likely meant, if any. */
|
|
6
|
+
export declare function filterTreeOperatorSuggestion(operator: string): FilterTreeOperator | null;
|