@oxygen-agent/cli 1.285.8 → 1.285.53
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/browser-login.js +1 -3
- package/dist/credentials.d.ts +2 -2
- package/dist/credentials.js +26 -59
- package/dist/http-client.js +1 -10
- package/dist/index.js +45 -45
- package/dist/knowledge-mirror.d.ts +4 -7
- package/dist/knowledge-mirror.js +4 -4
- package/dist/local-custom-http-column.js +1 -3
- package/dist/runtime.d.ts +1 -0
- package/dist/runtime.js +1 -1
- package/dist/skills.js +1 -6
- package/dist/update.d.ts +1 -2
- package/dist/update.js +2 -4
- package/dist/util.d.ts +3 -0
- package/dist/util.js +9 -0
- package/node_modules/@oxygen/shared/dist/email-tracking-token.d.ts +1 -2
- package/node_modules/@oxygen/shared/dist/email-tracking-token.js +6 -29
- package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.d.ts +1 -2
- package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.js +5 -30
- package/node_modules/@oxygen/shared/dist/object-storage.js +7 -1
- package/node_modules/@oxygen/shared/dist/signed-token.d.ts +23 -0
- package/node_modules/@oxygen/shared/dist/signed-token.js +51 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/browser-login.js
CHANGED
|
@@ -2,6 +2,7 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { OxygenError } from "@oxygen/shared";
|
|
5
|
+
import { readErrorMessage } from "./util.js";
|
|
5
6
|
const CALLBACK_HOST = "127.0.0.1";
|
|
6
7
|
const CALLBACK_PATH = "/cli/login/callback";
|
|
7
8
|
const MAX_CALLBACK_BODY_BYTES = 32_000;
|
|
@@ -175,6 +176,3 @@ function closeServer(server) {
|
|
|
175
176
|
});
|
|
176
177
|
});
|
|
177
178
|
}
|
|
178
|
-
function readErrorMessage(error) {
|
|
179
|
-
return error instanceof Error ? error.message : "unknown error";
|
|
180
|
-
}
|
package/dist/credentials.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export type StoredCredentials = {
|
|
|
21
21
|
export type CredentialProfile = StoredCredentials & {
|
|
22
22
|
name: string;
|
|
23
23
|
};
|
|
24
|
-
|
|
24
|
+
type CredentialProfilesState = {
|
|
25
25
|
activeProfile: string | null;
|
|
26
26
|
profiles: CredentialProfile[];
|
|
27
27
|
};
|
|
@@ -46,7 +46,6 @@ export type ProfileResolution = {
|
|
|
46
46
|
exists: boolean;
|
|
47
47
|
};
|
|
48
48
|
export declare function resolveActiveProfile(env?: NodeJS.ProcessEnv): Promise<ProfileResolution>;
|
|
49
|
-
export declare function findProfileNameByOrganizationId(organizationId: string, env?: NodeJS.ProcessEnv): Promise<string | null>;
|
|
50
49
|
export declare function pickProfileNameForIdentity(organizationId: string, candidateSeed: string, env?: NodeJS.ProcessEnv): Promise<{
|
|
51
50
|
name: string;
|
|
52
51
|
renamed: boolean;
|
|
@@ -58,3 +57,4 @@ export declare function pickProfileNameForUserSession(userId: string, candidateS
|
|
|
58
57
|
export declare function switchCredentialProfile(profile: string, env?: NodeJS.ProcessEnv): Promise<CredentialProfile>;
|
|
59
58
|
export declare function updateActiveOrganizationForProfile(profile: string, organization: StoredOrganization, env?: NodeJS.ProcessEnv): Promise<CredentialProfile>;
|
|
60
59
|
export declare function normalizeApiUrl(value: string): string;
|
|
60
|
+
export {};
|
package/dist/credentials.js
CHANGED
|
@@ -43,17 +43,7 @@ export async function loadCredentials(env = process.env) {
|
|
|
43
43
|
return parseStoredCredentialsObject(document);
|
|
44
44
|
}
|
|
45
45
|
export async function saveCredentials(credentials, env = process.env, options = {}) {
|
|
46
|
-
const normalized =
|
|
47
|
-
token: credentials.token,
|
|
48
|
-
apiUrl: normalizeApiUrl(credentials.apiUrl),
|
|
49
|
-
};
|
|
50
|
-
if (credentials.authKind)
|
|
51
|
-
normalized.authKind = credentials.authKind;
|
|
52
|
-
if (credentials.activeOrganization !== undefined) {
|
|
53
|
-
normalized.activeOrganization = credentials.activeOrganization;
|
|
54
|
-
}
|
|
55
|
-
if (credentials.identity)
|
|
56
|
-
normalized.identity = credentials.identity;
|
|
46
|
+
const normalized = normalizeStoredCredentialsRecord(credentials);
|
|
57
47
|
const existing = await readSystemCredentialDocument(env);
|
|
58
48
|
const profile = normalizeProfileName(options.profile ?? readEnvProfile(env) ?? existing?.activeProfile ?? DEFAULT_PROFILE_NAME);
|
|
59
49
|
const profiles = existing ? readProfiles(existing) : {};
|
|
@@ -153,26 +143,12 @@ export async function resolveActiveProfile(env = process.env) {
|
|
|
153
143
|
: "default";
|
|
154
144
|
return { name: candidate, source, credentials, exists: Boolean(credentials) };
|
|
155
145
|
}
|
|
156
|
-
|
|
157
|
-
const document = await readSystemCredentialDocument(env);
|
|
158
|
-
if (!document)
|
|
159
|
-
return null;
|
|
160
|
-
const profiles = readProfiles(document);
|
|
161
|
-
for (const [name, credentials] of Object.entries(profiles)) {
|
|
162
|
-
if (credentials.identity?.organization?.id === organizationId)
|
|
163
|
-
return name;
|
|
164
|
-
if (credentials.activeOrganization?.id === organizationId)
|
|
165
|
-
return name;
|
|
166
|
-
}
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
export async function pickProfileNameForIdentity(organizationId, candidateSeed, env = process.env) {
|
|
146
|
+
async function pickProfileName(matches, candidateSeed, env) {
|
|
170
147
|
const document = await readSystemCredentialDocument(env);
|
|
171
148
|
const profiles = document ? readProfiles(document) : {};
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
return { name: existingForOrg[0], renamed: false };
|
|
149
|
+
const existing = Object.entries(profiles).find(([, credentials]) => matches(credentials));
|
|
150
|
+
if (existing)
|
|
151
|
+
return { name: existing[0], renamed: false };
|
|
176
152
|
const base = normalizeProfileName(candidateSeed);
|
|
177
153
|
if (!profiles[base])
|
|
178
154
|
return { name: base, renamed: false };
|
|
@@ -186,25 +162,13 @@ export async function pickProfileNameForIdentity(organizationId, candidateSeed,
|
|
|
186
162
|
exitCode: 1,
|
|
187
163
|
});
|
|
188
164
|
}
|
|
165
|
+
export async function pickProfileNameForIdentity(organizationId, candidateSeed, env = process.env) {
|
|
166
|
+
return pickProfileName((credentials) => credentials.identity?.organization?.id === organizationId ||
|
|
167
|
+
credentials.activeOrganization?.id === organizationId, candidateSeed, env);
|
|
168
|
+
}
|
|
189
169
|
export async function pickProfileNameForUserSession(userId, candidateSeed, env = process.env) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const existingForUser = Object.entries(profiles).find(([, credentials]) => credentials.authKind === "user_session" &&
|
|
193
|
-
credentials.identity?.user.id === userId);
|
|
194
|
-
if (existingForUser)
|
|
195
|
-
return { name: existingForUser[0], renamed: false };
|
|
196
|
-
const base = normalizeProfileName(candidateSeed);
|
|
197
|
-
if (!profiles[base])
|
|
198
|
-
return { name: base, renamed: false };
|
|
199
|
-
for (let suffix = 2; suffix < 1000; suffix++) {
|
|
200
|
-
const next = `${base}-${suffix}`;
|
|
201
|
-
if (!profiles[next])
|
|
202
|
-
return { name: next, renamed: true };
|
|
203
|
-
}
|
|
204
|
-
throw new OxygenError("profile_collision", "Unable to derive a unique CLI profile name.", {
|
|
205
|
-
details: { base },
|
|
206
|
-
exitCode: 1,
|
|
207
|
-
});
|
|
170
|
+
return pickProfileName((credentials) => credentials.authKind === "user_session" &&
|
|
171
|
+
credentials.identity?.user.id === userId, candidateSeed, env);
|
|
208
172
|
}
|
|
209
173
|
export async function switchCredentialProfile(profile, env = process.env) {
|
|
210
174
|
const document = await readSystemCredentialDocument(env);
|
|
@@ -696,22 +660,25 @@ function parseStoredOrganization(value) {
|
|
|
696
660
|
: null;
|
|
697
661
|
return { id, slug, name };
|
|
698
662
|
}
|
|
663
|
+
function normalizeStoredCredentialsRecord(credentials) {
|
|
664
|
+
const normalized = {
|
|
665
|
+
token: credentials.token,
|
|
666
|
+
apiUrl: normalizeApiUrl(credentials.apiUrl),
|
|
667
|
+
};
|
|
668
|
+
if (credentials.authKind)
|
|
669
|
+
normalized.authKind = credentials.authKind;
|
|
670
|
+
if (credentials.activeOrganization !== undefined) {
|
|
671
|
+
normalized.activeOrganization = credentials.activeOrganization;
|
|
672
|
+
}
|
|
673
|
+
if (credentials.identity)
|
|
674
|
+
normalized.identity = credentials.identity;
|
|
675
|
+
return normalized;
|
|
676
|
+
}
|
|
699
677
|
function readProfiles(document) {
|
|
700
678
|
const profiles = {};
|
|
701
679
|
const rawProfiles = document.profiles ?? {};
|
|
702
680
|
for (const [name, credentials] of Object.entries(rawProfiles)) {
|
|
703
|
-
|
|
704
|
-
token: credentials.token,
|
|
705
|
-
apiUrl: normalizeApiUrl(credentials.apiUrl),
|
|
706
|
-
};
|
|
707
|
-
if (credentials.authKind)
|
|
708
|
-
normalized.authKind = credentials.authKind;
|
|
709
|
-
if (credentials.activeOrganization !== undefined) {
|
|
710
|
-
normalized.activeOrganization = credentials.activeOrganization;
|
|
711
|
-
}
|
|
712
|
-
if (credentials.identity)
|
|
713
|
-
normalized.identity = credentials.identity;
|
|
714
|
-
profiles[normalizeProfileName(name)] = normalized;
|
|
681
|
+
profiles[normalizeProfileName(name)] = normalizeStoredCredentialsRecord(credentials);
|
|
715
682
|
}
|
|
716
683
|
if (Object.keys(profiles).length === 0) {
|
|
717
684
|
const defaultCredentials = parseStoredCredentialsObject(document);
|
package/dist/http-client.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { OXYGEN_VERSION, OxygenError, isCliResult, isVersionGreater, readEnvelopeCompatibility, vercelProtectionBypassHeaders, withRetryAfterDetails, } from "@oxygen/shared";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { defaultApiUrl, loadCredentials } from "./credentials.js";
|
|
4
|
-
import { resolveCliUpdateGuidance } from "./runtime.js";
|
|
4
|
+
import { isProdApiUrl, resolveCliUpdateGuidance } from "./runtime.js";
|
|
5
5
|
const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
|
|
6
6
|
const CLI_COMPATIBILITY_CHECK_TIMEOUT_MS = 5_000;
|
|
7
|
-
const PROD_API_HOSTNAME = "oxygen-agent.com";
|
|
8
7
|
const cliCompatibilityCheckKeys = new Set();
|
|
9
8
|
export async function requestOxygen(// skipcq: JS-R1005
|
|
10
9
|
path, options = {}) {
|
|
@@ -175,14 +174,6 @@ function warnIfCliIsOlderThanApi(serverVersion, apiUrl) {
|
|
|
175
174
|
process.stderr.write(`[${guidance.binaryName}] CLI version ${OXYGEN_VERSION} is older than Oxygen API version ${serverVersion}. `
|
|
176
175
|
+ `${guidance.warningInstruction}\n`);
|
|
177
176
|
}
|
|
178
|
-
function isProdApiUrl(apiUrl) {
|
|
179
|
-
try {
|
|
180
|
-
return new URL(apiUrl).hostname === PROD_API_HOSTNAME;
|
|
181
|
-
}
|
|
182
|
-
catch {
|
|
183
|
-
return false;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
177
|
function assertCliMeetsMinimumApiVersion(compatibility, options, apiUrl) {
|
|
187
178
|
if (options.enforceMinimumCliVersion === false)
|
|
188
179
|
return;
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import { addSessionOutput, addSessionStatus, getSessionUsage, startSession, upda
|
|
|
24
24
|
import { doctorAgentSkills, installAgentSkills, listAgentSkills, runAutomaticSkillsInstall, } from "./skills.js";
|
|
25
25
|
import { resolveCliBinaryName } from "./runtime.js";
|
|
26
26
|
import { updateCli } from "./update.js";
|
|
27
|
+
import { isRecord, readErrorMessage, readOption } from "./util.js";
|
|
27
28
|
function buildFindBody(capability, options) {
|
|
28
29
|
const body = { capability };
|
|
29
30
|
const set = (key, value) => {
|
|
@@ -795,13 +796,24 @@ function readPublishingPostText(options, required) {
|
|
|
795
796
|
if (inlineText && textFile) {
|
|
796
797
|
throw new OxygenError("conflicting_flags", "Pass either --text or --text-file, not both.", { exitCode: 1 });
|
|
797
798
|
}
|
|
798
|
-
const text = textFile ?
|
|
799
|
+
const text = textFile ? readPublishingTextFile(textFile) : inlineText;
|
|
799
800
|
if (text?.trim())
|
|
800
801
|
return text;
|
|
801
802
|
if (!required)
|
|
802
803
|
return null;
|
|
803
804
|
throw new OxygenError("invalid_request", "Pass --text or --text-file.", { exitCode: 1 });
|
|
804
805
|
}
|
|
806
|
+
// Read --text-file, sanitizing the OS error so a missing/unreadable file reports
|
|
807
|
+
// only the basename the user passed — never the resolved absolute filesystem path
|
|
808
|
+
// Node's ENOENT would otherwise leak into the CLI error output.
|
|
809
|
+
function readPublishingTextFile(textFile) {
|
|
810
|
+
try {
|
|
811
|
+
return readFileSync(resolve(textFile), "utf8").trimEnd();
|
|
812
|
+
}
|
|
813
|
+
catch {
|
|
814
|
+
throw new OxygenError("text_file_unreadable", `Couldn't read --text-file '${basename(textFile)}'. Check the path exists and is readable.`, { exitCode: 1 });
|
|
815
|
+
}
|
|
816
|
+
}
|
|
805
817
|
function buildCrmRelationshipUpsertBody(object, rowId, options) {
|
|
806
818
|
return {
|
|
807
819
|
object,
|
|
@@ -5727,6 +5739,23 @@ export function createProgram() {
|
|
|
5727
5739
|
.option("--json", "Print a JSON envelope.")
|
|
5728
5740
|
.action(async (id, options) => {
|
|
5729
5741
|
await handleAsyncAction("senders health", options, () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}/health`));
|
|
5742
|
+
}))
|
|
5743
|
+
.addCommand(new Command("inmail-balance")
|
|
5744
|
+
.description("Show remaining LinkedIn InMail credits for a sender (Premium / Sales Navigator / Recruiter each hold a separate pool). Run before enrolling an `inmail` sequencer step so a campaign does not stall on an account with zero credits. Read-only — not billed. <id> defaults to the org's primary sender.")
|
|
5745
|
+
.argument("[id]", "Sender account id, connection id, or Unipile account id. Defaults to the org's primary sender.")
|
|
5746
|
+
.option("--service <service>", "Narrow to one credit pool: classic | sales_navigator | recruiter. Defaults to every subscribed service.")
|
|
5747
|
+
.option("--json", "Print a JSON envelope.")
|
|
5748
|
+
.action(async (id, options) => {
|
|
5749
|
+
await handleAsyncAction("senders inmail-balance", options, () => {
|
|
5750
|
+
const query = new URLSearchParams();
|
|
5751
|
+
if (id)
|
|
5752
|
+
query.set("account", id);
|
|
5753
|
+
const service = readOption(options.service);
|
|
5754
|
+
if (service)
|
|
5755
|
+
query.set("service", service);
|
|
5756
|
+
const suffix = query.toString();
|
|
5757
|
+
return requestOxygen(`/api/cli/linkedin/inmail-balance${suffix ? `?${suffix}` : ""}`);
|
|
5758
|
+
});
|
|
5730
5759
|
}))
|
|
5731
5760
|
.addCommand(new Command("sync")
|
|
5732
5761
|
.description("Refresh LinkedIn account state (status, profile) from Unipile. Pass --connection-id to sync one account, or omit it to sync all.")
|
|
@@ -6267,7 +6296,7 @@ export function createProgram() {
|
|
|
6267
6296
|
.action(async (options) => {
|
|
6268
6297
|
await handleAsyncAction("linkedin invitations withdraw", options, () => {
|
|
6269
6298
|
const ids = (options.ids ?? "").split(",").map((id) => id.trim()).filter(Boolean);
|
|
6270
|
-
const olderThan =
|
|
6299
|
+
const olderThan = readPositiveInt(options.olderThan);
|
|
6271
6300
|
const account = readOption(options.account);
|
|
6272
6301
|
return requestOxygen("/api/cli/linkedin/connections/invite/withdraw", {
|
|
6273
6302
|
method: "POST",
|
|
@@ -6288,7 +6317,7 @@ export function createProgram() {
|
|
|
6288
6317
|
.action(async (options) => {
|
|
6289
6318
|
await handleAsyncAction("linkedin invitations list", options, () => {
|
|
6290
6319
|
const account = readOption(options.account);
|
|
6291
|
-
const limit =
|
|
6320
|
+
const limit = readPositiveInt(options.limit);
|
|
6292
6321
|
const params = new URLSearchParams();
|
|
6293
6322
|
if (account)
|
|
6294
6323
|
params.set("account", account);
|
|
@@ -6990,7 +7019,7 @@ export function createProgram() {
|
|
|
6990
7019
|
const senders = readCsvOption(options.senders);
|
|
6991
7020
|
const email = readCampaignEmailBinding(options);
|
|
6992
7021
|
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
6993
|
-
const maxLiveSends =
|
|
7022
|
+
const maxLiveSends = readPositiveInt(options.maxLiveSends);
|
|
6994
7023
|
const settings = readSequenceSettings(options);
|
|
6995
7024
|
return requestOxygen("/api/cli/sequences", {
|
|
6996
7025
|
method: "POST",
|
|
@@ -7057,7 +7086,7 @@ export function createProgram() {
|
|
|
7057
7086
|
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
7058
7087
|
if (maxCredits !== undefined)
|
|
7059
7088
|
body.max_credits = maxCredits;
|
|
7060
|
-
const maxLiveSends =
|
|
7089
|
+
const maxLiveSends = readPositiveInt(options.maxLiveSends);
|
|
7061
7090
|
if (maxLiveSends !== undefined)
|
|
7062
7091
|
body.max_live_sends = maxLiveSends;
|
|
7063
7092
|
const settings = readSequenceSettings(options);
|
|
@@ -7130,7 +7159,7 @@ export function createProgram() {
|
|
|
7130
7159
|
.action(async (sequence, options) => {
|
|
7131
7160
|
await handleAsyncAction("sequences start", options, () => {
|
|
7132
7161
|
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
7133
|
-
const maxLiveSends =
|
|
7162
|
+
const maxLiveSends = readPositiveInt(options.maxLiveSends);
|
|
7134
7163
|
return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/start`, {
|
|
7135
7164
|
method: "POST",
|
|
7136
7165
|
body: {
|
|
@@ -7561,10 +7590,10 @@ export function createProgram() {
|
|
|
7561
7590
|
body: { ramp_config: null },
|
|
7562
7591
|
});
|
|
7563
7592
|
}
|
|
7564
|
-
const startPerDay =
|
|
7565
|
-
const increaseEveryDays =
|
|
7566
|
-
const step =
|
|
7567
|
-
const cap =
|
|
7593
|
+
const startPerDay = readPositiveInt(options.startPerDay);
|
|
7594
|
+
const increaseEveryDays = readPositiveInt(options.increaseEveryDays);
|
|
7595
|
+
const step = readPositiveInt(options.step);
|
|
7596
|
+
const cap = readPositiveInt(options.cap);
|
|
7568
7597
|
if (startPerDay === undefined || increaseEveryDays === undefined || step === undefined || cap === undefined) {
|
|
7569
7598
|
throw new Error("Provide --start-per-day, --increase-every-days, --step, and --cap (or --clear to remove the override).");
|
|
7570
7599
|
}
|
|
@@ -9245,7 +9274,7 @@ function readCompaniesSearchPlanBody(options) {
|
|
|
9245
9274
|
}
|
|
9246
9275
|
function readCompaniesSearchRunBody(options) {
|
|
9247
9276
|
const prompt = options.prompt ? readFileIfPresent(options.prompt) : null;
|
|
9248
|
-
const plan = options.planJson ?
|
|
9277
|
+
const plan = options.planJson ? readSearchPlanJson(options.planJson) : null;
|
|
9249
9278
|
if (!prompt && !plan) {
|
|
9250
9279
|
throw new OxygenError("invalid_request", "Pass --prompt or --plan-json.", { exitCode: 1 });
|
|
9251
9280
|
}
|
|
@@ -9396,7 +9425,7 @@ function assertNotPeopleSearchKind(kind) {
|
|
|
9396
9425
|
});
|
|
9397
9426
|
}
|
|
9398
9427
|
}
|
|
9399
|
-
function
|
|
9428
|
+
function readSearchPlanJson(value) {
|
|
9400
9429
|
const parsed = parseJsonObject(readFileIfPresent(value));
|
|
9401
9430
|
const data = parsed.data;
|
|
9402
9431
|
return data && typeof data === "object" && !Array.isArray(data)
|
|
@@ -9418,7 +9447,7 @@ function readPeopleSearchPlanBody(options) {
|
|
|
9418
9447
|
}
|
|
9419
9448
|
function readPeopleSearchRunBody(options) {
|
|
9420
9449
|
const prompt = options.prompt ? readFileIfPresent(options.prompt) : null;
|
|
9421
|
-
const plan = options.planJson ?
|
|
9450
|
+
const plan = options.planJson ? readSearchPlanJson(options.planJson) : null;
|
|
9422
9451
|
if (!prompt && !plan) {
|
|
9423
9452
|
throw new OxygenError("invalid_request", "Pass --prompt or --plan-json.", { exitCode: 1 });
|
|
9424
9453
|
}
|
|
@@ -9504,13 +9533,6 @@ function readPeopleSearchFilters(options) {
|
|
|
9504
9533
|
}
|
|
9505
9534
|
return Object.keys(filters).length > 0 ? filters : null;
|
|
9506
9535
|
}
|
|
9507
|
-
function readPeopleSearchPlanJson(value) {
|
|
9508
|
-
const parsed = parseJsonObject(readFileIfPresent(value));
|
|
9509
|
-
const data = parsed.data;
|
|
9510
|
-
return data && typeof data === "object" && !Array.isArray(data)
|
|
9511
|
-
? data
|
|
9512
|
-
: parsed;
|
|
9513
|
-
}
|
|
9514
9536
|
function readCompaniesEnrichBody(table, options) {
|
|
9515
9537
|
const body = { table };
|
|
9516
9538
|
const fields = readCsvOption(options.missingFields);
|
|
@@ -10907,9 +10929,6 @@ function readRecord(value, key) {
|
|
|
10907
10929
|
const entry = value[key];
|
|
10908
10930
|
return isRecord(entry) ? entry : null;
|
|
10909
10931
|
}
|
|
10910
|
-
function isRecord(value) {
|
|
10911
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
10912
|
-
}
|
|
10913
10932
|
function tableWebUrl(tableIdOrSlug) {
|
|
10914
10933
|
return `https://oxygen-agent.com/tables/${encodeURIComponent(tableIdOrSlug)}`;
|
|
10915
10934
|
}
|
|
@@ -11706,9 +11725,6 @@ function createCliLoginUrl(apiUrl) {
|
|
|
11706
11725
|
url.searchParams.set("source", "oxygen_cli");
|
|
11707
11726
|
return url.toString();
|
|
11708
11727
|
}
|
|
11709
|
-
function readErrorMessage(error) {
|
|
11710
|
-
return error instanceof Error ? error.message : "unknown error";
|
|
11711
|
-
}
|
|
11712
11728
|
function startOxygenSpinner(message) {
|
|
11713
11729
|
if (!output.isTTY) {
|
|
11714
11730
|
output.write(`${message}...\n`);
|
|
@@ -12378,9 +12394,6 @@ function ansi(enabled) {
|
|
|
12378
12394
|
yellow: wrap(33, 39),
|
|
12379
12395
|
};
|
|
12380
12396
|
}
|
|
12381
|
-
function readOption(value) {
|
|
12382
|
-
return value?.trim() ? value.trim() : null;
|
|
12383
|
-
}
|
|
12384
12397
|
// Commander collector for a repeatable option (e.g. --attach <url> --attach <url>):
|
|
12385
12398
|
// accumulates each value into an array.
|
|
12386
12399
|
function collectRepeatable(value, previous) {
|
|
@@ -13470,19 +13483,6 @@ function readNonNegativeInt(value) {
|
|
|
13470
13483
|
}
|
|
13471
13484
|
return parsed;
|
|
13472
13485
|
}
|
|
13473
|
-
function readPositiveInteger(value) {
|
|
13474
|
-
const trimmed = value?.trim();
|
|
13475
|
-
if (!trimmed)
|
|
13476
|
-
return undefined;
|
|
13477
|
-
const parsed = Number(trimmed);
|
|
13478
|
-
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
13479
|
-
throw new OxygenError("invalid_number", "Expected a positive integer.", {
|
|
13480
|
-
details: { value },
|
|
13481
|
-
exitCode: 1,
|
|
13482
|
-
});
|
|
13483
|
-
}
|
|
13484
|
-
return parsed;
|
|
13485
|
-
}
|
|
13486
13486
|
// Folds the sequence-level email send controls (--max-emails-per-mailbox-per-day,
|
|
13487
13487
|
// --send-window-file) into a partial `settings` object. Returns undefined when
|
|
13488
13488
|
// neither flag is set so the field is omitted from the request body entirely.
|
|
@@ -13491,7 +13491,7 @@ function readPositiveInteger(value) {
|
|
|
13491
13491
|
// preserves other settings the server already holds, e.g. dispatch_mode/schedule.
|
|
13492
13492
|
function readSequenceSettings(options) {
|
|
13493
13493
|
const settings = {};
|
|
13494
|
-
const maxPerMailbox =
|
|
13494
|
+
const maxPerMailbox = readPositiveInt(options.maxEmailsPerMailboxPerDay);
|
|
13495
13495
|
if (maxPerMailbox !== undefined) {
|
|
13496
13496
|
settings.max_emails_per_mailbox_per_day = maxPerMailbox;
|
|
13497
13497
|
}
|
|
@@ -13512,10 +13512,10 @@ function readSequenceSettings(options) {
|
|
|
13512
13512
|
if (options.stopOnBounce === false) {
|
|
13513
13513
|
settings.stop_on_bounce = false;
|
|
13514
13514
|
}
|
|
13515
|
-
const maxEmailsPerDay =
|
|
13515
|
+
const maxEmailsPerDay = readPositiveInt(options.maxEmailsPerDay);
|
|
13516
13516
|
if (maxEmailsPerDay !== undefined)
|
|
13517
13517
|
settings.max_emails_per_day = maxEmailsPerDay;
|
|
13518
|
-
const maxNewEnrollmentsPerDay =
|
|
13518
|
+
const maxNewEnrollmentsPerDay = readPositiveInt(options.maxNewEnrollmentsPerDay);
|
|
13519
13519
|
if (maxNewEnrollmentsPerDay !== undefined)
|
|
13520
13520
|
settings.max_new_enrollments_per_day = maxNewEnrollmentsPerDay;
|
|
13521
13521
|
const prioritization = readOption(options.sequencePrioritization);
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
export declare const MIRROR_LOCK_STALE_MS: number;
|
|
2
|
-
export declare const MIRROR_SLUG_PATTERN: RegExp;
|
|
3
1
|
export declare const GENERATED_MIRROR_FILE_HEADER = "<!-- generated by oxygen knowledge sync \u2014 local edits are overwritten -->";
|
|
4
2
|
export type MirrorPageRecord = {
|
|
5
3
|
id: string;
|
|
@@ -16,7 +14,7 @@ export type MirrorState = {
|
|
|
16
14
|
last_sync_at: string | null;
|
|
17
15
|
pages: Record<string, MirrorPageRecord>;
|
|
18
16
|
};
|
|
19
|
-
|
|
17
|
+
type MirrorIndexPage = {
|
|
20
18
|
slug: string;
|
|
21
19
|
title?: string | null;
|
|
22
20
|
type?: string | null;
|
|
@@ -26,7 +24,7 @@ export type MirrorIndexPage = {
|
|
|
26
24
|
canonical?: boolean;
|
|
27
25
|
degree?: number;
|
|
28
26
|
};
|
|
29
|
-
|
|
27
|
+
type MirrorIndexSnapshot = {
|
|
30
28
|
pages: MirrorIndexPage[];
|
|
31
29
|
counts?: {
|
|
32
30
|
total?: number;
|
|
@@ -34,7 +32,7 @@ export type MirrorIndexSnapshot = {
|
|
|
34
32
|
byStatus?: Record<string, number>;
|
|
35
33
|
};
|
|
36
34
|
};
|
|
37
|
-
|
|
35
|
+
type MirrorLogEntry = {
|
|
38
36
|
seq?: string;
|
|
39
37
|
event?: string;
|
|
40
38
|
slug?: string | null;
|
|
@@ -49,14 +47,12 @@ export declare function resolveMirrorDir(input: {
|
|
|
49
47
|
apiHost: string;
|
|
50
48
|
orgId: string;
|
|
51
49
|
}): string;
|
|
52
|
-
export declare function mirrorSidecarDir(dir: string): string;
|
|
53
50
|
export declare function mirrorManifestPath(dir: string): string;
|
|
54
51
|
export declare function mirrorLockPath(dir: string): string;
|
|
55
52
|
export declare function mirrorConflictsDir(dir: string): string;
|
|
56
53
|
export declare function pageFilePath(dir: string, slug: string): string;
|
|
57
54
|
/** Cheap "does a mirror exist here" check for post-write staleness hooks. */
|
|
58
55
|
export declare function mirrorExists(dir: string): boolean;
|
|
59
|
-
export declare function assertSafeMirrorSlug(slug: string): void;
|
|
60
56
|
export declare function emptyMirrorState(input: {
|
|
61
57
|
apiHost: string;
|
|
62
58
|
orgId: string;
|
|
@@ -126,3 +122,4 @@ export declare function purgeMirror(dir: string): void;
|
|
|
126
122
|
export declare function resetMirrorForFullResync(dir: string): {
|
|
127
123
|
quarantined: number;
|
|
128
124
|
};
|
|
125
|
+
export {};
|
package/dist/knowledge-mirror.js
CHANGED
|
@@ -22,10 +22,10 @@ const MIRROR_MANIFEST_FILENAME = "manifest.json";
|
|
|
22
22
|
const MIRROR_LOCK_FILENAME = "lock";
|
|
23
23
|
const MIRROR_CONFLICTS_DIRNAME = "conflicts";
|
|
24
24
|
const MIRROR_SLUG_MAX_LENGTH = 200;
|
|
25
|
-
|
|
25
|
+
const MIRROR_LOCK_STALE_MS = 10 * 60 * 1000;
|
|
26
26
|
// Server page slugs are lowercase alphanumerics/dashes; anything else (path
|
|
27
27
|
// separators, dots, uppercase) is rejected before it can touch the filesystem.
|
|
28
|
-
|
|
28
|
+
const MIRROR_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
29
29
|
export const GENERATED_MIRROR_FILE_HEADER = "<!-- generated by oxygen knowledge sync — local edits are overwritten -->";
|
|
30
30
|
// ── Paths ───────────────────────────────────────────────────────────────────────
|
|
31
31
|
/** Same resolution as the credential/session stores: OXYGEN_CONFIG_DIR wins. */
|
|
@@ -38,7 +38,7 @@ export function resolveDefaultConfigDir(env = process.env) {
|
|
|
38
38
|
export function resolveMirrorDir(input) {
|
|
39
39
|
return join(input.configDir, "knowledge", safePathSegment(input.apiHost, "API host"), safePathSegment(input.orgId, "organization id"));
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
function mirrorSidecarDir(dir) {
|
|
42
42
|
return join(dir, MIRROR_SIDECAR_DIRNAME);
|
|
43
43
|
}
|
|
44
44
|
export function mirrorManifestPath(dir) {
|
|
@@ -68,7 +68,7 @@ function safePathSegment(value, label) {
|
|
|
68
68
|
}
|
|
69
69
|
return cleaned;
|
|
70
70
|
}
|
|
71
|
-
|
|
71
|
+
function assertSafeMirrorSlug(slug) {
|
|
72
72
|
if (slug.length > MIRROR_SLUG_MAX_LENGTH || !MIRROR_SLUG_PATTERN.test(slug)) {
|
|
73
73
|
throw new OxygenError("invalid_slug", "Mirror page slugs must be lowercase alphanumerics and dashes.", {
|
|
74
74
|
details: { slug },
|
|
@@ -2,6 +2,7 @@ import { Buffer } from "node:buffer";
|
|
|
2
2
|
import { OxygenError } from "@oxygen/shared";
|
|
3
3
|
import { CustomHttpUrlSafetyError, assertCustomHttpPublicUrlSyntax, assertCustomHttpResolvedHostAllowed, } from "@oxygen/shared/custom-http-safety";
|
|
4
4
|
import { requestOxygen } from "./http-client.js";
|
|
5
|
+
import { isRecord } from "./util.js";
|
|
5
6
|
const LOCAL_CUSTOM_HTTP_TIMEOUT_MS = 10_000;
|
|
6
7
|
const LOCAL_CUSTOM_HTTP_MAX_RESPONSE_BYTES = 1_000_000;
|
|
7
8
|
export async function runLocalCustomHttpColumn(table, columnKeyOrId, options) {
|
|
@@ -769,6 +770,3 @@ function isNonEmptyLocalValue(value) {
|
|
|
769
770
|
return Object.keys(value).length > 0;
|
|
770
771
|
return true;
|
|
771
772
|
}
|
|
772
|
-
function isRecord(value) {
|
|
773
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
774
|
-
}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -12,4 +12,5 @@ export type CliUpdateGuidance = {
|
|
|
12
12
|
};
|
|
13
13
|
export declare function resolveCliBinaryName(env?: NodeJS.ProcessEnv, argv?: readonly string[]): typeof DEV_CLI_BINARY | typeof PROD_CLI_BINARY;
|
|
14
14
|
export declare function resolveCliUpdateGuidance(apiUrl: string | undefined, env?: NodeJS.ProcessEnv, argv?: readonly string[]): CliUpdateGuidance;
|
|
15
|
+
export declare function isProdApiUrl(apiUrl: string): boolean;
|
|
15
16
|
export {};
|
package/dist/runtime.js
CHANGED
package/dist/skills.js
CHANGED
|
@@ -5,6 +5,7 @@ import { join, resolve } from "node:path";
|
|
|
5
5
|
import { OxygenError, toFailure } from "@oxygen/shared";
|
|
6
6
|
import { defaultApiUrl, loadCredentials, normalizeApiUrl } from "./credentials.js";
|
|
7
7
|
import { requestOxygen } from "./http-client.js";
|
|
8
|
+
import { isRecord, readOption } from "./util.js";
|
|
8
9
|
import { resolveCmdShimInvocation } from "./windows-shim.js";
|
|
9
10
|
const DEFAULT_SKILL_AGENTS = ["codex", "claude-code", "cursor"];
|
|
10
11
|
const SKILL_INDEX_PATH = "/.well-known/skills/index.json";
|
|
@@ -444,9 +445,3 @@ function readWords(value) {
|
|
|
444
445
|
.map((entry) => entry.trim())
|
|
445
446
|
.filter(Boolean);
|
|
446
447
|
}
|
|
447
|
-
function readOption(value) {
|
|
448
|
-
return value?.trim() ? value.trim() : null;
|
|
449
|
-
}
|
|
450
|
-
function isRecord(value) {
|
|
451
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
452
|
-
}
|
package/dist/update.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type UpdateOptions = {
|
|
|
13
13
|
package?: string;
|
|
14
14
|
dryRun?: boolean;
|
|
15
15
|
};
|
|
16
|
-
|
|
16
|
+
type UpdateResult = {
|
|
17
17
|
current_version: string;
|
|
18
18
|
package: string;
|
|
19
19
|
command: string;
|
|
@@ -28,7 +28,6 @@ export declare function updateCli(options: UpdateOptions, runtime?: {
|
|
|
28
28
|
spawnSync?: SpawnSyncLike;
|
|
29
29
|
execFileSync?: ExecFileSyncLike;
|
|
30
30
|
} & SkillsSourceRuntime): Promise<UpdateResult>;
|
|
31
|
-
export declare function detectCliInstallPrefix(modulePath?: string): string | null;
|
|
32
31
|
export declare function detectCliInstallPrefixFromPath(modulePath: string): string | null;
|
|
33
32
|
export declare function resolveNpmExecutables(platform?: NodeJS.Platform): string[];
|
|
34
33
|
export {};
|
package/dist/update.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { OXYGEN_VERSION, OxygenError } from "@oxygen/shared";
|
|
4
4
|
import { runAutomaticSkillsInstall, skippedAutomaticSkillsInstall, } from "./skills.js";
|
|
5
|
+
import { readOption } from "./util.js";
|
|
5
6
|
import { quoteWindowsCmdArg } from "./windows-shim.js";
|
|
6
7
|
const DEFAULT_CLI_PACKAGE_SPEC = "@oxygen-agent/cli@latest";
|
|
7
8
|
// skipcq: JS-R1005 — handles platform/install-mode/dry-run combinations end-to-end
|
|
@@ -76,7 +77,7 @@ export async function updateCli(options, runtime = {}) {
|
|
|
76
77
|
exitCode: 1,
|
|
77
78
|
});
|
|
78
79
|
}
|
|
79
|
-
|
|
80
|
+
function detectCliInstallPrefix(modulePath = fileURLToPath(import.meta.url)) {
|
|
80
81
|
return detectCliInstallPrefixFromPath(modulePath);
|
|
81
82
|
}
|
|
82
83
|
export function detectCliInstallPrefixFromPath(modulePath) {
|
|
@@ -99,9 +100,6 @@ export function detectCliInstallPrefixFromPath(modulePath) {
|
|
|
99
100
|
export function resolveNpmExecutables(platform = process.platform) {
|
|
100
101
|
return platform === "win32" ? ["cmd.exe"] : ["npm"];
|
|
101
102
|
}
|
|
102
|
-
function readOption(value) {
|
|
103
|
-
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
104
|
-
}
|
|
105
103
|
function isMissingExecutableError(error) {
|
|
106
104
|
return error.code === "ENOENT";
|
|
107
105
|
}
|
package/dist/util.d.ts
ADDED
package/dist/util.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function readOption(value) {
|
|
2
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
3
|
+
}
|
|
4
|
+
export function isRecord(value) {
|
|
5
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
export function readErrorMessage(error) {
|
|
8
|
+
return error instanceof Error ? error.message : "unknown error";
|
|
9
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type EnvLike } from "./signed-token.js";
|
|
1
2
|
/**
|
|
2
3
|
* Signed token for native open/click tracking on cold email. The dispatcher MINTS
|
|
3
4
|
* the token at send time (one per tracked action) and bakes it into a tracking
|
|
@@ -37,7 +38,6 @@ export type EmailTrackingTokenPayload = {
|
|
|
37
38
|
/** Issued-at (epoch ms), recorded as provenance. */
|
|
38
39
|
iat: number;
|
|
39
40
|
};
|
|
40
|
-
type EnvLike = Record<string, string | undefined>;
|
|
41
41
|
/** SHA-256 hex of a destination URL — the value bound into a click token's `uh`. */
|
|
42
42
|
export declare function hashTrackingUrl(url: string): string;
|
|
43
43
|
/**
|
|
@@ -79,4 +79,3 @@ export declare function buildOpenPixelUrl(token: string, baseUrl: string): strin
|
|
|
79
79
|
* in the `u` query param (the route recomputes its hash against the token's `uh`).
|
|
80
80
|
*/
|
|
81
81
|
export declare function buildClickTrackingUrl(token: string, destinationUrl: string, baseUrl: string): string;
|
|
82
|
-
export {};
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { createHash,
|
|
2
|
-
|
|
3
|
-
return Buffer.from(value, "utf8").toString("base64url");
|
|
4
|
-
}
|
|
1
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { resolveAppBaseUrl, signPayload, splitAndVerifyToken } from "./signed-token.js";
|
|
5
3
|
/** SHA-256 hex of a destination URL — the value bound into a click token's `uh`. */
|
|
6
4
|
export function hashTrackingUrl(url) {
|
|
7
5
|
return createHash("sha256").update(url, "utf8").digest("hex");
|
|
@@ -11,21 +9,7 @@ export function hashTrackingUrl(url) {
|
|
|
11
9
|
* computed over the encoded body, so any tamper invalidates the signature.
|
|
12
10
|
*/
|
|
13
11
|
export function signEmailTrackingToken(payload, secret) {
|
|
14
|
-
|
|
15
|
-
const signature = createHmac("sha256", secret).update(body).digest("base64url");
|
|
16
|
-
return `${body}.${signature}`;
|
|
17
|
-
}
|
|
18
|
-
/** Constant-time HMAC-SHA256 check; false on any error (bad base64, length mismatch). */
|
|
19
|
-
function verifySignature(body, providedSig, secret) {
|
|
20
|
-
const expected = createHmac("sha256", secret).update(body).digest();
|
|
21
|
-
let provided;
|
|
22
|
-
try {
|
|
23
|
-
provided = Buffer.from(providedSig, "base64url");
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
return provided.length === expected.length && timingSafeEqual(provided, expected);
|
|
12
|
+
return signPayload(payload, secret);
|
|
29
13
|
}
|
|
30
14
|
/** Decode + validate the base64url body; typed payload or null on any problem. */
|
|
31
15
|
function parseTokenPayload(body) {
|
|
@@ -71,14 +55,8 @@ function parseTokenPayload(body) {
|
|
|
71
55
|
* null -> benign gif / plain redirect with no recording.
|
|
72
56
|
*/
|
|
73
57
|
export function verifyEmailTrackingToken(token, secret) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const dot = token.indexOf(".");
|
|
77
|
-
if (dot <= 0 || dot === token.length - 1)
|
|
78
|
-
return null;
|
|
79
|
-
const body = token.slice(0, dot);
|
|
80
|
-
const providedSig = token.slice(dot + 1);
|
|
81
|
-
if (!verifySignature(body, providedSig, secret))
|
|
58
|
+
const body = splitAndVerifyToken(token, secret);
|
|
59
|
+
if (body === null)
|
|
82
60
|
return null;
|
|
83
61
|
return parseTokenPayload(body);
|
|
84
62
|
}
|
|
@@ -114,8 +92,7 @@ export function emailTrackingSigningSecret(env = process.env) {
|
|
|
114
92
|
* fallback the token routes are always reachable at.)
|
|
115
93
|
*/
|
|
116
94
|
export function emailTrackingAppBaseUrl(env = process.env) {
|
|
117
|
-
|
|
118
|
-
return raw.replace(/\/+$/, "");
|
|
95
|
+
return resolveAppBaseUrl(env);
|
|
119
96
|
}
|
|
120
97
|
/** The open-pixel URL for a signed token: `<base>/api/t/o/<token>`. */
|
|
121
98
|
export function buildOpenPixelUrl(token, baseUrl) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type EnvLike } from "./signed-token.js";
|
|
1
2
|
/**
|
|
2
3
|
* Signed token for the RFC 8058 one-click List-Unsubscribe link on native cold
|
|
3
4
|
* email. The worker MINTS the token at send time and bakes the URL into the
|
|
@@ -28,7 +29,6 @@ export type UnsubscribeTokenPayload = {
|
|
|
28
29
|
/** Issued-at (epoch ms), recorded as provenance. */
|
|
29
30
|
iat: number;
|
|
30
31
|
};
|
|
31
|
-
type EnvLike = Record<string, string | undefined>;
|
|
32
32
|
/**
|
|
33
33
|
* Sign a payload into `<base64url(json)>.<base64url(hmac-sha256)>`. The HMAC is
|
|
34
34
|
* computed over the encoded body, so any tamper to the payload invalidates the
|
|
@@ -59,4 +59,3 @@ export declare function oxygenAppBaseUrl(env?: EnvLike): string;
|
|
|
59
59
|
* re-checks it against the token), token in the `token` query param.
|
|
60
60
|
*/
|
|
61
61
|
export declare function buildUnsubscribeUrl(orgId: string, token: string, baseUrl: string): string;
|
|
62
|
-
export {};
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
function base64urlEncode(value) {
|
|
3
|
-
return Buffer.from(value, "utf8").toString("base64url");
|
|
4
|
-
}
|
|
1
|
+
import { resolveAppBaseUrl, signPayload, splitAndVerifyToken } from "./signed-token.js";
|
|
5
2
|
/**
|
|
6
3
|
* Sign a payload into `<base64url(json)>.<base64url(hmac-sha256)>`. The HMAC is
|
|
7
4
|
* computed over the encoded body, so any tamper to the payload invalidates the
|
|
@@ -10,22 +7,7 @@ function base64urlEncode(value) {
|
|
|
10
7
|
*/
|
|
11
8
|
export function signUnsubscribeToken(payload, secret) {
|
|
12
9
|
const normalized = { ...payload, email: payload.email.trim().toLowerCase() };
|
|
13
|
-
|
|
14
|
-
const signature = createHmac("sha256", secret).update(body).digest("base64url");
|
|
15
|
-
return `${body}.${signature}`;
|
|
16
|
-
}
|
|
17
|
-
/** Constant-time HMAC-SHA256 check; false on any error (bad base64, length mismatch). */
|
|
18
|
-
function verifySignature(body, providedSig, secret) {
|
|
19
|
-
const expected = createHmac("sha256", secret).update(body).digest();
|
|
20
|
-
let provided;
|
|
21
|
-
try {
|
|
22
|
-
provided = Buffer.from(providedSig, "base64url");
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
// Equal-length guard before timingSafeEqual (it throws on length mismatch).
|
|
28
|
-
return provided.length === expected.length && timingSafeEqual(provided, expected);
|
|
10
|
+
return signPayload(normalized, secret);
|
|
29
11
|
}
|
|
30
12
|
/** Decode and validate the base64url body; returns typed payload or null on any problem. */
|
|
31
13
|
function parseTokenPayload(body) {
|
|
@@ -62,14 +44,8 @@ function parseTokenPayload(body) {
|
|
|
62
44
|
* secret, tampered body, replayed garbage). Pure: the route maps null -> 401.
|
|
63
45
|
*/
|
|
64
46
|
export function verifyUnsubscribeToken(token, secret) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const dot = token.indexOf(".");
|
|
68
|
-
if (dot <= 0 || dot === token.length - 1)
|
|
69
|
-
return null;
|
|
70
|
-
const body = token.slice(0, dot);
|
|
71
|
-
const providedSig = token.slice(dot + 1);
|
|
72
|
-
if (!verifySignature(body, providedSig, secret))
|
|
47
|
+
const body = splitAndVerifyToken(token, secret);
|
|
48
|
+
if (body === null)
|
|
73
49
|
return null;
|
|
74
50
|
return parseTokenPayload(body);
|
|
75
51
|
}
|
|
@@ -87,8 +63,7 @@ export function unsubscribeSigningSecret(env = process.env) {
|
|
|
87
63
|
* stripped. Prefers NEXT_PUBLIC_APP_URL, then OXYGEN_APP_URL, then the prod host.
|
|
88
64
|
*/
|
|
89
65
|
export function oxygenAppBaseUrl(env = process.env) {
|
|
90
|
-
|
|
91
|
-
return raw.replace(/\/+$/, "");
|
|
66
|
+
return resolveAppBaseUrl(env);
|
|
92
67
|
}
|
|
93
68
|
/**
|
|
94
69
|
* The canonical one-click unsubscribe URL: org id in the path (the route
|
|
@@ -8,6 +8,12 @@ import { OxygenError } from "./cli-result.js";
|
|
|
8
8
|
// it. Configured against Hetzner Object Storage (S3-compatible) via the
|
|
9
9
|
// OXYGEN_IMPORT_S3_* env vars; works with any S3-compatible endpoint.
|
|
10
10
|
const PRESIGN_EXPIRY_SECONDS = 900;
|
|
11
|
+
// The Fly worker publishes tenants serially, so a hung S3 GET on one tenant's
|
|
12
|
+
// publishing media would stall every other tenant's publishing tick. Bound the
|
|
13
|
+
// media download the same way the native-provider fetches are bounded
|
|
14
|
+
// (AbortSignal.timeout); 120s matches the worker's other media-transfer budgets
|
|
15
|
+
// and is generous for the bounded image/short-video assets publishing uploads.
|
|
16
|
+
const PUBLISHING_MEDIA_DOWNLOAD_TIMEOUT_MS = 120_000;
|
|
11
17
|
function readImportStorageConfig() {
|
|
12
18
|
const endpoint = process.env.OXYGEN_IMPORT_S3_ENDPOINT?.trim();
|
|
13
19
|
const bucket = process.env.OXYGEN_IMPORT_S3_BUCKET?.trim();
|
|
@@ -138,7 +144,7 @@ export async function openImportObjectStream(input) {
|
|
|
138
144
|
}
|
|
139
145
|
export async function openPublishingMediaObjectStream(input) {
|
|
140
146
|
const { client, config } = resolveClient();
|
|
141
|
-
const result = await client.send(new GetObjectCommand({ Bucket: config.bucket, Key: input.storageKey }));
|
|
147
|
+
const result = await client.send(new GetObjectCommand({ Bucket: config.bucket, Key: input.storageKey }), { abortSignal: AbortSignal.timeout(PUBLISHING_MEDIA_DOWNLOAD_TIMEOUT_MS) });
|
|
142
148
|
const body = result.Body;
|
|
143
149
|
if (!body) {
|
|
144
150
|
throw new OxygenError("publishing_media_object_missing", "Publishing media object had no body.", {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HMAC-signed-token core shared by the email tracking + unsubscribe tokens:
|
|
3
|
+
* `<base64url(json)>.<base64url(hmac-sha256)>` minting/verification plus the
|
|
4
|
+
* public app base-URL resolver. Internal to @oxygen/shared (not exported from
|
|
5
|
+
* the package index); each token module owns its payload shape + parsing.
|
|
6
|
+
*/
|
|
7
|
+
export type EnvLike = Record<string, string | undefined>;
|
|
8
|
+
/**
|
|
9
|
+
* Sign a payload into `<base64url(json)>.<base64url(hmac-sha256)>`. The HMAC is
|
|
10
|
+
* computed over the encoded body, so any tamper invalidates the signature.
|
|
11
|
+
*/
|
|
12
|
+
export declare function signPayload(payload: unknown, secret: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Split a `<body>.<signature>` token at the first dot and verify the signature
|
|
15
|
+
* (constant-time). Returns the still-encoded body on a valid signature; null on
|
|
16
|
+
* ANY problem (bad shape, blank/wrong secret, tampered body).
|
|
17
|
+
*/
|
|
18
|
+
export declare function splitAndVerifyToken(token: string, secret: string): string | null;
|
|
19
|
+
/**
|
|
20
|
+
* The public app base URL, trailing slash stripped. Prefers NEXT_PUBLIC_APP_URL,
|
|
21
|
+
* then OXYGEN_APP_URL, then the prod host.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveAppBaseUrl(env?: EnvLike): string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
function base64urlEncode(value) {
|
|
3
|
+
return Buffer.from(value, "utf8").toString("base64url");
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Sign a payload into `<base64url(json)>.<base64url(hmac-sha256)>`. The HMAC is
|
|
7
|
+
* computed over the encoded body, so any tamper invalidates the signature.
|
|
8
|
+
*/
|
|
9
|
+
export function signPayload(payload, secret) {
|
|
10
|
+
const body = base64urlEncode(JSON.stringify(payload));
|
|
11
|
+
const signature = createHmac("sha256", secret).update(body).digest("base64url");
|
|
12
|
+
return `${body}.${signature}`;
|
|
13
|
+
}
|
|
14
|
+
/** Constant-time HMAC-SHA256 check; false on any error (bad base64, length mismatch). */
|
|
15
|
+
function verifySignature(body, providedSig, secret) {
|
|
16
|
+
const expected = createHmac("sha256", secret).update(body).digest();
|
|
17
|
+
let provided;
|
|
18
|
+
try {
|
|
19
|
+
provided = Buffer.from(providedSig, "base64url");
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
// Equal-length guard before timingSafeEqual (it throws on length mismatch).
|
|
25
|
+
return provided.length === expected.length && timingSafeEqual(provided, expected);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Split a `<body>.<signature>` token at the first dot and verify the signature
|
|
29
|
+
* (constant-time). Returns the still-encoded body on a valid signature; null on
|
|
30
|
+
* ANY problem (bad shape, blank/wrong secret, tampered body).
|
|
31
|
+
*/
|
|
32
|
+
export function splitAndVerifyToken(token, secret) {
|
|
33
|
+
if (typeof token !== "string" || typeof secret !== "string" || !secret)
|
|
34
|
+
return null;
|
|
35
|
+
const dot = token.indexOf(".");
|
|
36
|
+
if (dot <= 0 || dot === token.length - 1)
|
|
37
|
+
return null;
|
|
38
|
+
const body = token.slice(0, dot);
|
|
39
|
+
const providedSig = token.slice(dot + 1);
|
|
40
|
+
if (!verifySignature(body, providedSig, secret))
|
|
41
|
+
return null;
|
|
42
|
+
return body;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The public app base URL, trailing slash stripped. Prefers NEXT_PUBLIC_APP_URL,
|
|
46
|
+
* then OXYGEN_APP_URL, then the prod host.
|
|
47
|
+
*/
|
|
48
|
+
export function resolveAppBaseUrl(env = process.env) {
|
|
49
|
+
const raw = (env.NEXT_PUBLIC_APP_URL || env.OXYGEN_APP_URL || "https://oxygen-agent.com").trim();
|
|
50
|
+
return raw.replace(/\/+$/, "");
|
|
51
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const OXYGEN_VERSION = "1.285.
|
|
1
|
+
export declare const OXYGEN_VERSION = "1.285.53";
|
|
2
2
|
export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.181.0";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const OXYGEN_VERSION = "1.285.
|
|
1
|
+
export const OXYGEN_VERSION = "1.285.53";
|
|
2
2
|
// Bump this only when deployed CLI/API contracts require a newer CLI.
|
|
3
3
|
// 1.181.0: paid table action runs and background columns run require
|
|
4
4
|
// approved=true in addition to max_credits; older CLIs cannot send the flag.
|