@sylphx/sdk 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.cts +583 -144
- package/dist/index.d.ts +583 -144
- package/dist/index.js +697 -267
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +691 -269
- package/dist/index.mjs.map +1 -1
- package/dist/nextjs/index.js +43 -24
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/index.mjs +43 -24
- package/dist/nextjs/index.mjs.map +1 -1
- package/dist/react/index.d.cts +61 -33
- package/dist/react/index.d.ts +61 -33
- package/dist/react/index.js +907 -749
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +907 -749
- package/dist/react/index.mjs.map +1 -1
- package/dist/server/index.d.cts +355 -18
- package/dist/server/index.d.ts +355 -18
- package/dist/server/index.js +529 -11
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +525 -11
- package/dist/server/index.mjs.map +1 -1
- package/dist/web-analytics.js.map +1 -1
- package/dist/web-analytics.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/web-analytics.d.cts +0 -90
- package/dist/web-analytics.d.ts +0 -90
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,101 @@
|
|
|
1
|
+
// src/connection-url.ts
|
|
2
|
+
var SYLPHX_PROTOCOL = "sylphx:";
|
|
3
|
+
var DEFAULT_VERSION = "v1";
|
|
4
|
+
var CREDENTIAL_REGEX = /^(pk|sk)_(dev|stg|prod|prev)_[a-f0-9]{32,64}$/;
|
|
5
|
+
var VERSION_REGEX = /^v[0-9]+$/;
|
|
6
|
+
var SLUG_REGEX = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
7
|
+
var InvalidConnectionUrlError = class _InvalidConnectionUrlError extends Error {
|
|
8
|
+
code = "INVALID_CONNECTION_URL";
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "InvalidConnectionUrlError";
|
|
12
|
+
Object.setPrototypeOf(this, _InvalidConnectionUrlError.prototype);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
function fail(reason) {
|
|
16
|
+
throw new InvalidConnectionUrlError(`Invalid Sylphx connection URL: ${reason}`);
|
|
17
|
+
}
|
|
18
|
+
function parseCredential(raw) {
|
|
19
|
+
const match = CREDENTIAL_REGEX.exec(raw);
|
|
20
|
+
if (!match) {
|
|
21
|
+
fail(`credential must match (pk|sk)_(dev|stg|prod|prev)_[a-f0-9]{32,64}, got "${raw}"`);
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
credentialType: match[1],
|
|
25
|
+
env: match[2]
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function validateSlug(candidate) {
|
|
29
|
+
if (!candidate || candidate.length > 63 || !SLUG_REGEX.test(candidate)) {
|
|
30
|
+
fail(`slug "${candidate}" is not a valid DNS label (lowercase alnum + hyphens, 1-63 chars)`);
|
|
31
|
+
}
|
|
32
|
+
return candidate;
|
|
33
|
+
}
|
|
34
|
+
function parseConnectionUrl(url) {
|
|
35
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
36
|
+
fail("url must be a non-empty string");
|
|
37
|
+
}
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = new URL(url);
|
|
41
|
+
} catch {
|
|
42
|
+
fail(`not a valid URL: "${url}"`);
|
|
43
|
+
}
|
|
44
|
+
if (parsed.protocol !== SYLPHX_PROTOCOL) {
|
|
45
|
+
fail(`protocol must be "sylphx:", got "${parsed.protocol}"`);
|
|
46
|
+
}
|
|
47
|
+
const credential = decodeURIComponent(parsed.username);
|
|
48
|
+
if (!credential) {
|
|
49
|
+
fail("missing credential (expected `sylphx://<credential>@<host>`)");
|
|
50
|
+
}
|
|
51
|
+
if (parsed.password) {
|
|
52
|
+
fail("connection URL must not contain a password component");
|
|
53
|
+
}
|
|
54
|
+
const { credentialType, env } = parseCredential(credential);
|
|
55
|
+
const host = parsed.host;
|
|
56
|
+
if (!host) {
|
|
57
|
+
fail("missing host");
|
|
58
|
+
}
|
|
59
|
+
const hostname = parsed.hostname;
|
|
60
|
+
const firstDot = hostname.indexOf(".");
|
|
61
|
+
if (firstDot <= 0) {
|
|
62
|
+
fail(`host "${hostname}" must contain at least one dot (slug.domain)`);
|
|
63
|
+
}
|
|
64
|
+
const slugCandidate = hostname.slice(0, firstDot);
|
|
65
|
+
const domainSuffix = hostname.slice(firstDot + 1);
|
|
66
|
+
if (!domainSuffix) {
|
|
67
|
+
fail(`host "${hostname}" has empty domain suffix`);
|
|
68
|
+
}
|
|
69
|
+
const slug = validateSlug(slugCandidate);
|
|
70
|
+
const rawPath = parsed.pathname.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
71
|
+
let version = DEFAULT_VERSION;
|
|
72
|
+
if (rawPath !== "") {
|
|
73
|
+
if (!VERSION_REGEX.test(rawPath)) {
|
|
74
|
+
fail(`path "${parsed.pathname}" must be empty or match /v{N}`);
|
|
75
|
+
}
|
|
76
|
+
version = rawPath;
|
|
77
|
+
}
|
|
78
|
+
if (parsed.search) {
|
|
79
|
+
fail("connection URL must not contain a query string");
|
|
80
|
+
}
|
|
81
|
+
if (parsed.hash) {
|
|
82
|
+
fail("connection URL must not contain a fragment");
|
|
83
|
+
}
|
|
84
|
+
const apiBaseUrl = `https://${host}/${version}`;
|
|
85
|
+
return {
|
|
86
|
+
credential,
|
|
87
|
+
credentialType,
|
|
88
|
+
env,
|
|
89
|
+
slug,
|
|
90
|
+
host,
|
|
91
|
+
apiBaseUrl
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
1
95
|
// src/constants.ts
|
|
2
96
|
var SDK_API_PATH = `/v1`;
|
|
3
97
|
var DEFAULT_SDK_API_HOST = "api.sylphx.com";
|
|
4
|
-
var SDK_VERSION = "0.
|
|
98
|
+
var SDK_VERSION = "0.5.0";
|
|
5
99
|
var SDK_PLATFORM = typeof window !== "undefined" ? "browser" : typeof process !== "undefined" && process.versions?.node ? "node" : "unknown";
|
|
6
100
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
7
101
|
var SESSION_TOKEN_LIFETIME_SECONDS = 5 * 60;
|
|
@@ -336,195 +430,153 @@ function exponentialBackoff(attempt, baseDelay = BASE_RETRY_DELAY_MS, maxDelay =
|
|
|
336
430
|
return Math.round(cappedDelay + jitter);
|
|
337
431
|
}
|
|
338
432
|
|
|
339
|
-
// src/
|
|
340
|
-
var
|
|
341
|
-
var
|
|
342
|
-
var
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
};
|
|
348
|
-
function detectKeyIssues(key) {
|
|
349
|
-
const issues = [];
|
|
350
|
-
if (key !== key.trim()) issues.push("whitespace");
|
|
351
|
-
if (key.includes("\n")) issues.push("newline");
|
|
352
|
-
if (key.includes("\r")) issues.push("carriage-return");
|
|
353
|
-
if (key.includes(" ")) issues.push("space");
|
|
354
|
-
if (key !== key.toLowerCase()) issues.push("uppercase-chars");
|
|
355
|
-
return issues;
|
|
356
|
-
}
|
|
357
|
-
function createSanitizationWarning(keyType, issues, envVarName) {
|
|
358
|
-
const keyTypeName = keyType === "appId" ? "App ID" : "Secret Key";
|
|
359
|
-
return `[Sylphx] ${keyTypeName} contains ${issues.join(", ")}. This is commonly caused by Vercel CLI's 'env pull' command.
|
|
360
|
-
|
|
361
|
-
To fix permanently:
|
|
362
|
-
1. Go to Vercel Dashboard \u2192 Your Project \u2192 Settings \u2192 Environment Variables
|
|
363
|
-
2. Edit ${envVarName}
|
|
364
|
-
3. Remove any trailing whitespace or newline characters
|
|
365
|
-
4. Redeploy your application
|
|
366
|
-
|
|
367
|
-
The SDK will automatically sanitize the key, but fixing the source is recommended.`;
|
|
368
|
-
}
|
|
369
|
-
function createInvalidKeyError(keyType, key, envVarName) {
|
|
370
|
-
const prefix = keyType === "appId" ? "app" : "sk";
|
|
371
|
-
const maskedKey = key.length > 20 ? `${key.slice(0, 20)}...` : key;
|
|
372
|
-
const formatHint = `${prefix}_(dev|stg|prod)_[identifier]`;
|
|
373
|
-
const keyTypeName = keyType === "appId" ? "App ID" : "Secret Key";
|
|
374
|
-
return `[Sylphx] Invalid ${keyTypeName} format.
|
|
375
|
-
|
|
376
|
-
Expected format: ${formatHint}
|
|
377
|
-
Received: "${maskedKey}"
|
|
378
|
-
|
|
379
|
-
Please check your ${envVarName} environment variable.
|
|
380
|
-
You can find your keys in the Sylphx Console \u2192 API Keys.
|
|
381
|
-
|
|
382
|
-
Common issues:
|
|
383
|
-
\u2022 Key has uppercase characters (must be lowercase)
|
|
384
|
-
\u2022 Key has wrong prefix (App ID: app_, Secret Key: sk_)
|
|
385
|
-
\u2022 Key has invalid environment (must be dev, stg, or prod)
|
|
386
|
-
\u2022 Key was copied with extra whitespace`;
|
|
387
|
-
}
|
|
388
|
-
function extractEnvironment(key) {
|
|
389
|
-
const match = key.match(/^(?:app|sk)_(dev|stg|prod)_/);
|
|
390
|
-
if (!match) return void 0;
|
|
391
|
-
return ENV_PREFIX_MAP[match[1]];
|
|
392
|
-
}
|
|
393
|
-
function validateKeyForType(key, keyType, pattern, envVarName) {
|
|
394
|
-
const keyTypeName = keyType === "appId" ? "App ID" : "Secret Key";
|
|
395
|
-
if (!key) {
|
|
396
|
-
return {
|
|
397
|
-
valid: false,
|
|
398
|
-
sanitizedKey: "",
|
|
399
|
-
error: `[Sylphx] ${keyTypeName} is required. Set ${envVarName} in your environment variables.`,
|
|
400
|
-
issues: ["missing"]
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
const issues = detectKeyIssues(key);
|
|
404
|
-
if (pattern.test(key)) {
|
|
405
|
-
return {
|
|
406
|
-
valid: true,
|
|
407
|
-
sanitizedKey: key,
|
|
408
|
-
keyType,
|
|
409
|
-
environment: extractEnvironment(key),
|
|
410
|
-
issues: []
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
const sanitized = key.trim().toLowerCase();
|
|
414
|
-
if (pattern.test(sanitized)) {
|
|
415
|
-
return {
|
|
416
|
-
valid: true,
|
|
417
|
-
sanitizedKey: sanitized,
|
|
418
|
-
keyType,
|
|
419
|
-
environment: extractEnvironment(sanitized),
|
|
420
|
-
warning: createSanitizationWarning(keyType, issues, envVarName),
|
|
421
|
-
issues
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
return {
|
|
425
|
-
valid: false,
|
|
426
|
-
sanitizedKey: "",
|
|
427
|
-
error: createInvalidKeyError(keyType, key, envVarName),
|
|
428
|
-
issues: [...issues, "invalid-format"]
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
function validatePublicKey(key) {
|
|
432
|
-
return validateKeyForType(key, "publicKey", PUBLIC_KEY_PATTERN, "NEXT_PUBLIC_SYLPHX_KEY");
|
|
433
|
-
}
|
|
434
|
-
function validateAppId(key) {
|
|
435
|
-
return validateKeyForType(key, "appId", APP_ID_PATTERN, "NEXT_PUBLIC_SYLPHX_APP_ID");
|
|
436
|
-
}
|
|
437
|
-
function validateSecretKey(key) {
|
|
438
|
-
return validateKeyForType(key, "secret", SECRET_KEY_PATTERN, "SYLPHX_SECRET_KEY");
|
|
439
|
-
}
|
|
440
|
-
function validateAndSanitizeSecretKey(key) {
|
|
441
|
-
const result = validateSecretKey(key);
|
|
442
|
-
if (!result.valid) {
|
|
443
|
-
throw new Error(result.error);
|
|
444
|
-
}
|
|
445
|
-
if (result.warning) {
|
|
446
|
-
console.warn(result.warning);
|
|
447
|
-
}
|
|
448
|
-
return result.sanitizedKey;
|
|
449
|
-
}
|
|
450
|
-
function detectKeyType(key) {
|
|
451
|
-
const sanitized = key.trim().toLowerCase();
|
|
452
|
-
if (sanitized.startsWith("pk_")) return "publicKey";
|
|
453
|
-
if (sanitized.startsWith("app_")) return "appId";
|
|
454
|
-
if (sanitized.startsWith("sk_")) return "secret";
|
|
455
|
-
return null;
|
|
456
|
-
}
|
|
457
|
-
function validateKey(key) {
|
|
458
|
-
const keyType = key ? detectKeyType(key) : null;
|
|
459
|
-
if (keyType === "publicKey") {
|
|
460
|
-
return validatePublicKey(key);
|
|
461
|
-
}
|
|
462
|
-
if (keyType === "appId") {
|
|
463
|
-
return validateAppId(key);
|
|
433
|
+
// src/config.ts
|
|
434
|
+
var LEGACY_EMBEDDED_REF_PATTERN = /^(pk|sk)_(dev|stg|prod|prev)_[a-z0-9]{12}_[a-f0-9]+$/;
|
|
435
|
+
var LEGACY_APP_KEY_PATTERN = /^app_(dev|stg|prod|prev)_/;
|
|
436
|
+
var MIGRATION_MESSAGE = "API key format has changed. Use a sylphx:// connection URL instead.\n\nNew format: sylphx://pk_prod_{hex}@your-slug.sylphx.com\n\nGenerate new credentials from the Sylphx Console \u2192 Your App \u2192 Environments.\nSee https://docs.sylphx.com/migration for details.";
|
|
437
|
+
function rejectLegacyKeyFormat(input) {
|
|
438
|
+
const trimmed = input.trim().toLowerCase();
|
|
439
|
+
if (LEGACY_APP_KEY_PATTERN.test(trimmed)) {
|
|
440
|
+
throw new SylphxError(`[Sylphx] ${MIGRATION_MESSAGE}`, { code: "BAD_REQUEST" });
|
|
464
441
|
}
|
|
465
|
-
if (
|
|
466
|
-
|
|
442
|
+
if (LEGACY_EMBEDDED_REF_PATTERN.test(trimmed)) {
|
|
443
|
+
throw new SylphxError(`[Sylphx] ${MIGRATION_MESSAGE}`, { code: "BAD_REQUEST" });
|
|
467
444
|
}
|
|
468
|
-
return {
|
|
469
|
-
valid: false,
|
|
470
|
-
sanitizedKey: "",
|
|
471
|
-
error: key ? `Invalid key format. Keys must start with 'pk_' (publishable), 'app_' (legacy), or 'sk_' (secret), followed by environment (dev/stg/prod). Got: ${key.slice(0, 20)}...` : "API key is required but was not provided.",
|
|
472
|
-
issues: key ? ["invalid_format"] : ["missing"]
|
|
473
|
-
};
|
|
474
445
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
446
|
+
function freezeConfig(opts) {
|
|
447
|
+
return Object.freeze({
|
|
448
|
+
credential: opts.credential,
|
|
449
|
+
credentialType: opts.credentialType,
|
|
450
|
+
env: opts.env,
|
|
451
|
+
slug: opts.slug,
|
|
452
|
+
baseUrl: opts.baseUrl,
|
|
453
|
+
accessToken: opts.accessToken,
|
|
454
|
+
// Backward-compat aliases
|
|
455
|
+
secretKey: opts.credentialType === "sk" ? opts.credential : void 0,
|
|
456
|
+
publicKey: opts.credentialType === "pk" ? opts.credential : void 0,
|
|
457
|
+
ref: opts.slug
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
function createClient(input) {
|
|
461
|
+
if (typeof input === "string") {
|
|
462
|
+
return createConfigFromUrl(input);
|
|
463
|
+
}
|
|
464
|
+
return createConfigFromComponents(input);
|
|
465
|
+
}
|
|
466
|
+
function createServerClient(input) {
|
|
467
|
+
const config = createClient(input);
|
|
468
|
+
if (config.credentialType !== "sk") {
|
|
480
469
|
throw new SylphxError(
|
|
481
|
-
"[Sylphx]
|
|
470
|
+
"[Sylphx] createServerClient() requires a secret key (sk_*). Use a SYLPHX_SECRET_URL with an sk_ credential, or pass { secretKey } in the components object.",
|
|
482
471
|
{ code: "BAD_REQUEST" }
|
|
483
472
|
);
|
|
484
473
|
}
|
|
485
|
-
|
|
486
|
-
|
|
474
|
+
return config;
|
|
475
|
+
}
|
|
476
|
+
function createConfigFromUrl(url) {
|
|
477
|
+
if (!url || typeof url !== "string") {
|
|
487
478
|
throw new SylphxError(
|
|
488
|
-
|
|
479
|
+
"[Sylphx] Connection URL is required. Set SYLPHX_URL or NEXT_PUBLIC_SYLPHX_URL environment variable.\n\nFormat: sylphx://pk_prod_{hex}@your-slug.sylphx.com",
|
|
489
480
|
{ code: "BAD_REQUEST" }
|
|
490
481
|
);
|
|
491
482
|
}
|
|
492
|
-
const
|
|
493
|
-
|
|
483
|
+
const trimmed = url.trim();
|
|
484
|
+
rejectLegacyKeyFormat(trimmed);
|
|
485
|
+
if (!trimmed.startsWith("sylphx://")) {
|
|
486
|
+
if (CREDENTIAL_REGEX.test(trimmed)) {
|
|
487
|
+
throw new SylphxError(
|
|
488
|
+
"[Sylphx] Received a bare credential instead of a connection URL.\n\nWrap it in a connection URL: sylphx://<credential>@<slug>.sylphx.com\nOr use createClient({ slug, publicKey }) for explicit components.",
|
|
489
|
+
{ code: "BAD_REQUEST" }
|
|
490
|
+
);
|
|
491
|
+
}
|
|
494
492
|
throw new SylphxError(
|
|
495
|
-
`[Sylphx] Invalid
|
|
493
|
+
`[Sylphx] Invalid connection URL \u2014 must start with "sylphx://". Got: "${trimmed.slice(0, 30)}..."`,
|
|
496
494
|
{ code: "BAD_REQUEST" }
|
|
497
495
|
);
|
|
498
496
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
497
|
+
let parsed;
|
|
498
|
+
try {
|
|
499
|
+
parsed = parseConnectionUrl(trimmed);
|
|
500
|
+
} catch (err) {
|
|
501
|
+
if (err instanceof InvalidConnectionUrlError) {
|
|
502
|
+
throw new SylphxError(err.message, { code: "BAD_REQUEST", cause: err });
|
|
503
|
+
}
|
|
504
|
+
throw err;
|
|
505
505
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
506
|
+
return freezeConfig({
|
|
507
|
+
credential: parsed.credential,
|
|
508
|
+
credentialType: parsed.credentialType,
|
|
509
|
+
env: parsed.env,
|
|
510
|
+
slug: parsed.slug,
|
|
511
|
+
baseUrl: parsed.apiBaseUrl
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
function createConfigFromComponents(input) {
|
|
515
|
+
const credential = input.secretKey || input.publicKey;
|
|
516
|
+
if (!credential) {
|
|
517
|
+
throw new SylphxError("[Sylphx] Either publicKey or secretKey must be provided.", {
|
|
518
|
+
code: "BAD_REQUEST"
|
|
519
|
+
});
|
|
511
520
|
}
|
|
512
|
-
const
|
|
513
|
-
if (
|
|
514
|
-
throw new SylphxError(
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
);
|
|
521
|
+
const resolvedSlug = input.slug || input.ref;
|
|
522
|
+
if (!resolvedSlug) {
|
|
523
|
+
throw new SylphxError("[Sylphx] slug is required when using explicit components.", {
|
|
524
|
+
code: "BAD_REQUEST"
|
|
525
|
+
});
|
|
518
526
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
+
const trimmedCred = credential.trim().toLowerCase();
|
|
528
|
+
if (CREDENTIAL_REGEX.test(trimmedCred)) {
|
|
529
|
+
const match = CREDENTIAL_REGEX.exec(trimmedCred);
|
|
530
|
+
const credentialType = match[1];
|
|
531
|
+
const env = match[2];
|
|
532
|
+
const slug = resolvedSlug.trim().toLowerCase();
|
|
533
|
+
const domain = input.domain?.trim() || "sylphx.com";
|
|
534
|
+
const baseUrl = `https://${slug}.${domain}/v1`;
|
|
535
|
+
return freezeConfig({
|
|
536
|
+
credential: trimmedCred,
|
|
537
|
+
credentialType,
|
|
538
|
+
env,
|
|
539
|
+
slug,
|
|
540
|
+
baseUrl,
|
|
541
|
+
accessToken: input.accessToken
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
const parts = trimmedCred.split("_");
|
|
545
|
+
const prefix = parts[0];
|
|
546
|
+
if ((prefix === "pk" || prefix === "sk") && parts.length >= 3) {
|
|
547
|
+
const envSegment = parts[1];
|
|
548
|
+
const validEnvs = ["dev", "stg", "prod", "prev"];
|
|
549
|
+
const env = validEnvs.includes(envSegment) ? envSegment : "prod";
|
|
550
|
+
const slug = resolvedSlug.trim().toLowerCase();
|
|
551
|
+
let baseUrl;
|
|
552
|
+
if (input.platformUrl) {
|
|
553
|
+
const platform = input.platformUrl.trim().replace(/\/$/, "");
|
|
554
|
+
baseUrl = platform.includes("/v1") ? platform : `${platform}/v1`;
|
|
555
|
+
} else {
|
|
556
|
+
const domain = input.domain?.trim() || "api.sylphx.com";
|
|
557
|
+
baseUrl = `https://${slug}.${domain}/v1`;
|
|
558
|
+
}
|
|
559
|
+
return freezeConfig({
|
|
560
|
+
credential: trimmedCred,
|
|
561
|
+
credentialType: prefix,
|
|
562
|
+
env,
|
|
563
|
+
slug,
|
|
564
|
+
baseUrl,
|
|
565
|
+
accessToken: input.accessToken
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
throw new SylphxError(
|
|
569
|
+
`[Sylphx] Invalid credential format. Expected (pk|sk)_(dev|stg|prod|prev)_[a-f0-9]{32,64}. Got: "${trimmedCred.slice(0, 30)}..."`,
|
|
570
|
+
{ code: "BAD_REQUEST" }
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
function withToken(config, accessToken) {
|
|
574
|
+
return Object.freeze({
|
|
575
|
+
...config,
|
|
576
|
+
accessToken
|
|
577
|
+
});
|
|
527
578
|
}
|
|
579
|
+
var createConfig = createClient;
|
|
528
580
|
function httpStatusToErrorCode(status) {
|
|
529
581
|
switch (status) {
|
|
530
582
|
case 400:
|
|
@@ -557,79 +609,12 @@ function httpStatusToErrorCode(status) {
|
|
|
557
609
|
return status >= 500 ? "INTERNAL_SERVER_ERROR" : "BAD_REQUEST";
|
|
558
610
|
}
|
|
559
611
|
}
|
|
560
|
-
var REF_PATTERN = /^[a-z0-9]{12}$/;
|
|
561
|
-
function createConfig(input) {
|
|
562
|
-
const keyForParsing = input.secretKey || input.publicKey;
|
|
563
|
-
if (!keyForParsing) {
|
|
564
|
-
if (input.ref) {
|
|
565
|
-
const trimmedRef = input.ref.trim();
|
|
566
|
-
if (!REF_PATTERN.test(trimmedRef)) {
|
|
567
|
-
throw new SylphxError(
|
|
568
|
-
`[Sylphx] Invalid project ref format: "${input.ref}". Expected a 12-character lowercase alphanumeric string (e.g. "abc123def456"). Get your ref from Platform Console \u2192 Projects \u2192 Your Project \u2192 Overview.`,
|
|
569
|
-
{ code: "BAD_REQUEST" }
|
|
570
|
-
);
|
|
571
|
-
}
|
|
572
|
-
const baseUrl2 = `https://${trimmedRef}.${DEFAULT_SDK_API_HOST}${SDK_API_PATH}`;
|
|
573
|
-
console.warn(
|
|
574
|
-
"[Sylphx] Providing only ref without a key is deprecated. Provide secretKey or publicKey \u2014 the ref is now embedded in keys (ADR-021)."
|
|
575
|
-
);
|
|
576
|
-
return Object.freeze({ ref: trimmedRef, baseUrl: baseUrl2, accessToken: input.accessToken });
|
|
577
|
-
}
|
|
578
|
-
throw new SylphxError(
|
|
579
|
-
"[Sylphx] Either publicKey or secretKey must be provided to createConfig().",
|
|
580
|
-
{ code: "BAD_REQUEST" }
|
|
581
|
-
);
|
|
582
|
-
}
|
|
583
|
-
const parsed = parseKey(keyForParsing);
|
|
584
|
-
const ref = parsed.ref;
|
|
585
|
-
const baseUrl = parsed.baseUrl;
|
|
586
|
-
let secretKey;
|
|
587
|
-
if (input.secretKey) {
|
|
588
|
-
const result = validateKey(input.secretKey);
|
|
589
|
-
if (!result.valid) {
|
|
590
|
-
throw new SylphxError(result.error || "Invalid secret key", {
|
|
591
|
-
code: "BAD_REQUEST",
|
|
592
|
-
data: { issues: result.issues }
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
if (result.warning) console.warn(`[Sylphx] ${result.warning}`);
|
|
596
|
-
secretKey = result.sanitizedKey;
|
|
597
|
-
}
|
|
598
|
-
let publicKey;
|
|
599
|
-
if (input.publicKey) {
|
|
600
|
-
const result = validateKey(input.publicKey);
|
|
601
|
-
if (!result.valid) {
|
|
602
|
-
throw new SylphxError(result.error || "Invalid public key", {
|
|
603
|
-
code: "BAD_REQUEST",
|
|
604
|
-
data: { issues: result.issues }
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
if (result.warning) console.warn(`[Sylphx] ${result.warning}`);
|
|
608
|
-
publicKey = result.sanitizedKey;
|
|
609
|
-
}
|
|
610
|
-
return Object.freeze({
|
|
611
|
-
secretKey,
|
|
612
|
-
publicKey,
|
|
613
|
-
ref,
|
|
614
|
-
baseUrl,
|
|
615
|
-
accessToken: input.accessToken
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
function withToken(config, accessToken) {
|
|
619
|
-
return Object.freeze({
|
|
620
|
-
...config,
|
|
621
|
-
accessToken
|
|
622
|
-
// Preserve baseUrl and ref from original config
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
612
|
function buildHeaders(config) {
|
|
626
613
|
const headers = {
|
|
627
614
|
"Content-Type": "application/json"
|
|
628
615
|
};
|
|
629
|
-
if (config.
|
|
630
|
-
headers["x-app-secret"] = config.
|
|
631
|
-
} else if (config.publicKey) {
|
|
632
|
-
headers["x-app-secret"] = config.publicKey;
|
|
616
|
+
if (config.credential) {
|
|
617
|
+
headers["x-app-secret"] = config.credential;
|
|
633
618
|
}
|
|
634
619
|
if (config.accessToken) {
|
|
635
620
|
headers.Authorization = `Bearer ${config.accessToken}`;
|
|
@@ -648,7 +633,8 @@ async function callApi(config, path, options = {}) {
|
|
|
648
633
|
query,
|
|
649
634
|
timeout = DEFAULT_TIMEOUT_MS,
|
|
650
635
|
signal,
|
|
651
|
-
idempotencyKey
|
|
636
|
+
idempotencyKey,
|
|
637
|
+
headers: extraHeaders
|
|
652
638
|
} = options;
|
|
653
639
|
let url = buildApiUrl(config, path);
|
|
654
640
|
if (query) {
|
|
@@ -670,6 +656,11 @@ async function callApi(config, path, options = {}) {
|
|
|
670
656
|
if (idempotencyKey) {
|
|
671
657
|
headers["Idempotency-Key"] = idempotencyKey;
|
|
672
658
|
}
|
|
659
|
+
if (extraHeaders) {
|
|
660
|
+
for (const [k, v] of Object.entries(extraHeaders)) {
|
|
661
|
+
headers[k] = v;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
673
664
|
const fetchOptions = {
|
|
674
665
|
method,
|
|
675
666
|
headers,
|
|
@@ -746,7 +737,6 @@ async function callApi(config, path, options = {}) {
|
|
|
746
737
|
code: "PARSE_ERROR",
|
|
747
738
|
cause: error instanceof Error ? error : void 0,
|
|
748
739
|
data: { body: text.slice(0, 200) }
|
|
749
|
-
// Include snippet for debugging
|
|
750
740
|
});
|
|
751
741
|
}
|
|
752
742
|
}
|
|
@@ -855,7 +845,112 @@ function installGlobalDebugHelpers() {
|
|
|
855
845
|
}
|
|
856
846
|
|
|
857
847
|
// src/rest-client.ts
|
|
858
|
-
import
|
|
848
|
+
import createClient2 from "openapi-fetch";
|
|
849
|
+
|
|
850
|
+
// src/key-validation.ts
|
|
851
|
+
var SECRET_KEY_PATTERN = /^sk_(dev|stg|prod)_[a-z0-9_-]+$/;
|
|
852
|
+
var ENV_PREFIX_MAP = {
|
|
853
|
+
dev: "development",
|
|
854
|
+
stg: "staging",
|
|
855
|
+
prod: "production"
|
|
856
|
+
};
|
|
857
|
+
function detectKeyIssues(key) {
|
|
858
|
+
const issues = [];
|
|
859
|
+
if (key !== key.trim()) issues.push("whitespace");
|
|
860
|
+
if (key.includes("\n")) issues.push("newline");
|
|
861
|
+
if (key.includes("\r")) issues.push("carriage-return");
|
|
862
|
+
if (key.includes(" ")) issues.push("space");
|
|
863
|
+
if (key !== key.toLowerCase()) issues.push("uppercase-chars");
|
|
864
|
+
return issues;
|
|
865
|
+
}
|
|
866
|
+
function createSanitizationWarning(keyType, issues, envVarName) {
|
|
867
|
+
const keyTypeName = keyType === "appId" ? "App ID" : "Secret Key";
|
|
868
|
+
return `[Sylphx] ${keyTypeName} contains ${issues.join(", ")}. This is commonly caused by Vercel CLI's 'env pull' command.
|
|
869
|
+
|
|
870
|
+
To fix permanently:
|
|
871
|
+
1. Go to Vercel Dashboard \u2192 Your Project \u2192 Settings \u2192 Environment Variables
|
|
872
|
+
2. Edit ${envVarName}
|
|
873
|
+
3. Remove any trailing whitespace or newline characters
|
|
874
|
+
4. Redeploy your application
|
|
875
|
+
|
|
876
|
+
The SDK will automatically sanitize the key, but fixing the source is recommended.`;
|
|
877
|
+
}
|
|
878
|
+
function createInvalidKeyError(keyType, key, envVarName) {
|
|
879
|
+
const maskedKey = key.length > 20 ? `${key.slice(0, 20)}...` : key;
|
|
880
|
+
const formatHint = keyType === "appId" ? "pk_(dev|stg|prod)_{ref}_{hex} or app_(dev|stg|prod)_[id]" : "sk_(dev|stg|prod)_{ref}_{hex}";
|
|
881
|
+
const keyTypeName = keyType === "appId" ? "App ID" : "Secret Key";
|
|
882
|
+
return `[Sylphx] Invalid ${keyTypeName} format.
|
|
883
|
+
|
|
884
|
+
Expected format: ${formatHint}
|
|
885
|
+
Received: "${maskedKey}"
|
|
886
|
+
|
|
887
|
+
Please check your ${envVarName} environment variable.
|
|
888
|
+
You can find your keys in the Sylphx Console \u2192 API Keys.
|
|
889
|
+
|
|
890
|
+
Common issues:
|
|
891
|
+
\u2022 Key has uppercase characters (must be lowercase)
|
|
892
|
+
\u2022 Key has wrong prefix (App ID: pk_ or app_, Secret Key: sk_)
|
|
893
|
+
\u2022 Key has invalid environment (must be dev, stg, or prod)
|
|
894
|
+
\u2022 Key was copied with extra whitespace`;
|
|
895
|
+
}
|
|
896
|
+
function extractEnvironment(key) {
|
|
897
|
+
const match = key.match(/^(?:app|pk|sk)_(dev|stg|prod|prev)_/);
|
|
898
|
+
if (!match) return void 0;
|
|
899
|
+
return ENV_PREFIX_MAP[match[1]];
|
|
900
|
+
}
|
|
901
|
+
function validateKeyForType(key, keyType, pattern, envVarName) {
|
|
902
|
+
const keyTypeName = keyType === "appId" ? "App ID" : "Secret Key";
|
|
903
|
+
if (!key) {
|
|
904
|
+
return {
|
|
905
|
+
valid: false,
|
|
906
|
+
sanitizedKey: "",
|
|
907
|
+
error: `[Sylphx] ${keyTypeName} is required. Set ${envVarName} in your environment variables.`,
|
|
908
|
+
issues: ["missing"]
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
const issues = detectKeyIssues(key);
|
|
912
|
+
if (pattern.test(key)) {
|
|
913
|
+
return {
|
|
914
|
+
valid: true,
|
|
915
|
+
sanitizedKey: key,
|
|
916
|
+
keyType,
|
|
917
|
+
environment: extractEnvironment(key),
|
|
918
|
+
issues: []
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
const sanitized = key.trim().toLowerCase();
|
|
922
|
+
if (pattern.test(sanitized)) {
|
|
923
|
+
return {
|
|
924
|
+
valid: true,
|
|
925
|
+
sanitizedKey: sanitized,
|
|
926
|
+
keyType,
|
|
927
|
+
environment: extractEnvironment(sanitized),
|
|
928
|
+
warning: createSanitizationWarning(keyType, issues, envVarName),
|
|
929
|
+
issues
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
return {
|
|
933
|
+
valid: false,
|
|
934
|
+
sanitizedKey: "",
|
|
935
|
+
error: createInvalidKeyError(keyType, key, envVarName),
|
|
936
|
+
issues: [...issues, "invalid-format"]
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
function validateSecretKey(key) {
|
|
940
|
+
return validateKeyForType(key, "secret", SECRET_KEY_PATTERN, "SYLPHX_SECRET_KEY");
|
|
941
|
+
}
|
|
942
|
+
function validateAndSanitizeSecretKey(key) {
|
|
943
|
+
const result = validateSecretKey(key);
|
|
944
|
+
if (!result.valid) {
|
|
945
|
+
throw new Error(result.error);
|
|
946
|
+
}
|
|
947
|
+
if (result.warning) {
|
|
948
|
+
console.warn(result.warning);
|
|
949
|
+
}
|
|
950
|
+
return result.sanitizedKey;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// src/rest-client.ts
|
|
859
954
|
function createAuthMiddleware(config) {
|
|
860
955
|
return {
|
|
861
956
|
async onRequest({ request }) {
|
|
@@ -1198,7 +1293,7 @@ function validateClientConfig(config) {
|
|
|
1198
1293
|
}
|
|
1199
1294
|
function createRestClient(config) {
|
|
1200
1295
|
const { secretKey, baseUrl } = validateClientConfig(config);
|
|
1201
|
-
const client =
|
|
1296
|
+
const client = createClient2({
|
|
1202
1297
|
baseUrl: `${baseUrl}${SDK_API_PATH}`,
|
|
1203
1298
|
headers: {
|
|
1204
1299
|
"Content-Type": "application/json",
|
|
@@ -1224,7 +1319,7 @@ function createDynamicRestClient(config) {
|
|
|
1224
1319
|
secretKey,
|
|
1225
1320
|
platformUrl: baseUrl
|
|
1226
1321
|
};
|
|
1227
|
-
const client =
|
|
1322
|
+
const client = createClient2({
|
|
1228
1323
|
baseUrl: `${baseUrl}${SDK_API_PATH}`,
|
|
1229
1324
|
headers: {
|
|
1230
1325
|
"Content-Type": "application/json"
|
|
@@ -2075,6 +2170,73 @@ async function updatePushPreferences(config, preferences) {
|
|
|
2075
2170
|
});
|
|
2076
2171
|
}
|
|
2077
2172
|
|
|
2173
|
+
// src/lib/triggers/index.ts
|
|
2174
|
+
var TriggersClient = class {
|
|
2175
|
+
/** Create a new trigger (cron or event source, task/run/http target) */
|
|
2176
|
+
static async create(config, options) {
|
|
2177
|
+
return callApi(config, "/triggers", {
|
|
2178
|
+
method: "POST",
|
|
2179
|
+
body: options
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
/** List all triggers for the project */
|
|
2183
|
+
static async list(config) {
|
|
2184
|
+
return callApi(config, "/triggers");
|
|
2185
|
+
}
|
|
2186
|
+
/** Get a trigger by ID */
|
|
2187
|
+
static async get(config, triggerId) {
|
|
2188
|
+
return callApi(config, `/triggers/${triggerId}`);
|
|
2189
|
+
}
|
|
2190
|
+
/** Update a trigger */
|
|
2191
|
+
static async update(config, triggerId, options) {
|
|
2192
|
+
return callApi(config, `/triggers/${triggerId}`, {
|
|
2193
|
+
method: "PATCH",
|
|
2194
|
+
body: options
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2197
|
+
/** Delete a trigger */
|
|
2198
|
+
static async delete(config, triggerId) {
|
|
2199
|
+
return callApi(config, `/triggers/${triggerId}`, { method: "DELETE" });
|
|
2200
|
+
}
|
|
2201
|
+
/** Pause a trigger */
|
|
2202
|
+
static async pause(config, triggerId) {
|
|
2203
|
+
return callApi(config, `/triggers/${triggerId}/pause`, { method: "POST" });
|
|
2204
|
+
}
|
|
2205
|
+
/** Resume a paused trigger */
|
|
2206
|
+
static async resume(config, triggerId) {
|
|
2207
|
+
return callApi(config, `/triggers/${triggerId}/resume`, { method: "POST" });
|
|
2208
|
+
}
|
|
2209
|
+
/** Fire a trigger immediately (one-shot, regardless of schedule) */
|
|
2210
|
+
static async fire(config, triggerId) {
|
|
2211
|
+
return callApi(config, `/triggers/${triggerId}/fire`, {
|
|
2212
|
+
method: "POST"
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
/**
|
|
2216
|
+
* Publish an event — dispatches all active event triggers matching the event name.
|
|
2217
|
+
*
|
|
2218
|
+
* @example
|
|
2219
|
+
* ```typescript
|
|
2220
|
+
* await TriggersClient.publishEvent(config, 'user.signup', { userId: '123', plan: 'pro' })
|
|
2221
|
+
* ```
|
|
2222
|
+
*/
|
|
2223
|
+
/**
|
|
2224
|
+
* Publish an event — dispatches all active event triggers matching the event name.
|
|
2225
|
+
* Endpoint: POST /triggers/events
|
|
2226
|
+
*
|
|
2227
|
+
* @example
|
|
2228
|
+
* ```typescript
|
|
2229
|
+
* await TriggersClient.publishEvent(config, 'user.signup', { userId: '123', plan: 'pro' })
|
|
2230
|
+
* ```
|
|
2231
|
+
*/
|
|
2232
|
+
static async publishEvent(config, eventName, payload) {
|
|
2233
|
+
return callApi(config, "/triggers/events", {
|
|
2234
|
+
method: "POST",
|
|
2235
|
+
body: { eventName, payload: payload ?? {} }
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
2238
|
+
};
|
|
2239
|
+
|
|
2078
2240
|
// src/lib/tasks/handler.ts
|
|
2079
2241
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
2080
2242
|
var StepCompleteSignal = class {
|
|
@@ -2091,6 +2253,14 @@ var StepSleepSignal = class {
|
|
|
2091
2253
|
}
|
|
2092
2254
|
_isStepSleepSignal = true;
|
|
2093
2255
|
};
|
|
2256
|
+
var StepWaitEventSignal = class {
|
|
2257
|
+
constructor(stepName, eventName, options = {}) {
|
|
2258
|
+
this.stepName = stepName;
|
|
2259
|
+
this.eventName = eventName;
|
|
2260
|
+
this.options = options;
|
|
2261
|
+
}
|
|
2262
|
+
_isStepWaitEventSignal = true;
|
|
2263
|
+
};
|
|
2094
2264
|
function createStepContext(completedSteps, resolvedWaits) {
|
|
2095
2265
|
return {
|
|
2096
2266
|
/**
|
|
@@ -2117,6 +2287,32 @@ function createStepContext(completedSteps, resolvedWaits) {
|
|
|
2117
2287
|
return;
|
|
2118
2288
|
}
|
|
2119
2289
|
throw new StepSleepSignal(name, duration);
|
|
2290
|
+
},
|
|
2291
|
+
/**
|
|
2292
|
+
* Pause execution until a named event is published via TriggersClient.publishEvent().
|
|
2293
|
+
*
|
|
2294
|
+
* - If event already arrived (platform re-dispatched with result): return event payload.
|
|
2295
|
+
* - If not yet arrived: throw StepWaitEventSignal to pause execution.
|
|
2296
|
+
*
|
|
2297
|
+
* @param name Step identifier (unique within handler).
|
|
2298
|
+
* @param eventName The event name to listen for (e.g. 'user.approved').
|
|
2299
|
+
* @param options Optional timeout ('24h', '7d') and payload filter.
|
|
2300
|
+
*
|
|
2301
|
+
* @example Human-in-the-loop approval
|
|
2302
|
+
* ```typescript
|
|
2303
|
+
* const approval = await step.waitForEvent('wait-approval', 'order.approved', {
|
|
2304
|
+
* timeout: '48h',
|
|
2305
|
+
* filter: { orderId: payload.orderId },
|
|
2306
|
+
* })
|
|
2307
|
+
* if (!approval) throw new Error('Approval timed out')
|
|
2308
|
+
* await sendConfirmation(approval.approvedBy)
|
|
2309
|
+
* ```
|
|
2310
|
+
*/
|
|
2311
|
+
async waitForEvent(name, eventName, options = {}) {
|
|
2312
|
+
if (resolvedWaits.has(name)) {
|
|
2313
|
+
return resolvedWaits.get(name) ?? null;
|
|
2314
|
+
}
|
|
2315
|
+
throw new StepWaitEventSignal(name, eventName, options);
|
|
2120
2316
|
}
|
|
2121
2317
|
};
|
|
2122
2318
|
}
|
|
@@ -2196,9 +2392,9 @@ function createTasksHandler(taskDefs, options = {}) {
|
|
|
2196
2392
|
for (const step of context?.steps ?? []) {
|
|
2197
2393
|
completedSteps.set(step.name, step.result);
|
|
2198
2394
|
}
|
|
2199
|
-
const resolvedWaits = /* @__PURE__ */ new
|
|
2395
|
+
const resolvedWaits = /* @__PURE__ */ new Map();
|
|
2200
2396
|
for (const wait of context?.waits ?? []) {
|
|
2201
|
-
resolvedWaits.
|
|
2397
|
+
resolvedWaits.set(wait.name, wait.result ?? void 0);
|
|
2202
2398
|
}
|
|
2203
2399
|
const stepCtx = createStepContext(completedSteps, resolvedWaits);
|
|
2204
2400
|
try {
|
|
@@ -2221,6 +2417,16 @@ function createTasksHandler(taskDefs, options = {}) {
|
|
|
2221
2417
|
duration: signal.duration
|
|
2222
2418
|
});
|
|
2223
2419
|
}
|
|
2420
|
+
if (err instanceof StepWaitEventSignal || err?._isStepWaitEventSignal) {
|
|
2421
|
+
const signal = err;
|
|
2422
|
+
return Response.json({
|
|
2423
|
+
status: "step_wait_event",
|
|
2424
|
+
stepName: signal.stepName,
|
|
2425
|
+
eventName: signal.eventName,
|
|
2426
|
+
timeout: signal.options.timeout ?? null,
|
|
2427
|
+
filter: signal.options.filter ?? null
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2224
2430
|
const message = err instanceof Error ? err.message : String(err);
|
|
2225
2431
|
console.error(`[sylphx/tasks] Task "${taskName}" threw an error:`, err);
|
|
2226
2432
|
return Response.json(
|
|
@@ -3230,6 +3436,143 @@ var SandboxFiles = class {
|
|
|
3230
3436
|
return data.files;
|
|
3231
3437
|
}
|
|
3232
3438
|
};
|
|
3439
|
+
var SandboxProcesses = class {
|
|
3440
|
+
constructor(endpoint, token) {
|
|
3441
|
+
this.endpoint = endpoint;
|
|
3442
|
+
this.token = token;
|
|
3443
|
+
}
|
|
3444
|
+
authHeader() {
|
|
3445
|
+
return { Authorization: `Bearer ${this.token}` };
|
|
3446
|
+
}
|
|
3447
|
+
/** Spawn a new tracked process. Returns processId + pid immediately. */
|
|
3448
|
+
async start(opts) {
|
|
3449
|
+
const res = await fetch(`${this.endpoint}/process/start`, {
|
|
3450
|
+
method: "POST",
|
|
3451
|
+
headers: { ...this.authHeader(), "Content-Type": "application/json" },
|
|
3452
|
+
body: JSON.stringify(opts)
|
|
3453
|
+
});
|
|
3454
|
+
if (!res.ok) throw new Error(`process.start failed: ${await res.text()}`);
|
|
3455
|
+
return await res.json();
|
|
3456
|
+
}
|
|
3457
|
+
/** List all tracked processes. */
|
|
3458
|
+
async list() {
|
|
3459
|
+
const res = await fetch(`${this.endpoint}/process/list`, {
|
|
3460
|
+
headers: this.authHeader()
|
|
3461
|
+
});
|
|
3462
|
+
if (!res.ok) throw new Error(`process.list failed: ${await res.text()}`);
|
|
3463
|
+
return await res.json();
|
|
3464
|
+
}
|
|
3465
|
+
/** Get full process info including buffered output. */
|
|
3466
|
+
async get(processId) {
|
|
3467
|
+
const res = await fetch(`${this.endpoint}/process/${processId}`, {
|
|
3468
|
+
headers: this.authHeader()
|
|
3469
|
+
});
|
|
3470
|
+
if (!res.ok) throw new Error(`process.get failed: ${await res.text()}`);
|
|
3471
|
+
return await res.json();
|
|
3472
|
+
}
|
|
3473
|
+
/** Send a signal to a process. */
|
|
3474
|
+
async kill(processId, signal = "SIGTERM") {
|
|
3475
|
+
const res = await fetch(`${this.endpoint}/process/${processId}/kill`, {
|
|
3476
|
+
method: "POST",
|
|
3477
|
+
headers: { ...this.authHeader(), "Content-Type": "application/json" },
|
|
3478
|
+
body: JSON.stringify({ signal })
|
|
3479
|
+
});
|
|
3480
|
+
if (!res.ok) throw new Error(`process.kill failed: ${await res.text()}`);
|
|
3481
|
+
}
|
|
3482
|
+
/** Write to process stdin. */
|
|
3483
|
+
async writeStdin(processId, data) {
|
|
3484
|
+
const res = await fetch(`${this.endpoint}/process/${processId}/input`, {
|
|
3485
|
+
method: "POST",
|
|
3486
|
+
headers: { ...this.authHeader(), "Content-Type": "application/json" },
|
|
3487
|
+
body: JSON.stringify({ data })
|
|
3488
|
+
});
|
|
3489
|
+
if (!res.ok) throw new Error(`process.writeStdin failed: ${await res.text()}`);
|
|
3490
|
+
}
|
|
3491
|
+
/**
|
|
3492
|
+
* Wait for a process to complete and return its final info.
|
|
3493
|
+
* Polls every 500ms until status is no longer 'running'.
|
|
3494
|
+
*
|
|
3495
|
+
* For real-time output, use stream() instead.
|
|
3496
|
+
*/
|
|
3497
|
+
async wait(processId, timeoutMs = 3e5) {
|
|
3498
|
+
const deadline = Date.now() + timeoutMs;
|
|
3499
|
+
while (Date.now() < deadline) {
|
|
3500
|
+
const info = await this.get(processId);
|
|
3501
|
+
if (info.status !== "running") return info;
|
|
3502
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
3503
|
+
}
|
|
3504
|
+
throw new Error(`Timed out waiting for process ${processId} to complete (${timeoutMs}ms)`);
|
|
3505
|
+
}
|
|
3506
|
+
/** Stream process output as async iterable SSE events. */
|
|
3507
|
+
async *stream(processId) {
|
|
3508
|
+
const res = await fetch(
|
|
3509
|
+
`${this.endpoint}/process/${processId}/stream`,
|
|
3510
|
+
{ headers: this.authHeader() }
|
|
3511
|
+
);
|
|
3512
|
+
if (!res.ok) throw new Error(`process.stream failed: ${await res.text()}`);
|
|
3513
|
+
if (!res.body) throw new Error("process.stream: no response body");
|
|
3514
|
+
const decoder = new TextDecoder();
|
|
3515
|
+
const reader = res.body.getReader();
|
|
3516
|
+
let buffer = "";
|
|
3517
|
+
try {
|
|
3518
|
+
while (true) {
|
|
3519
|
+
const { done, value } = await reader.read();
|
|
3520
|
+
if (done) break;
|
|
3521
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3522
|
+
const lines = buffer.split("\n");
|
|
3523
|
+
buffer = lines.pop() ?? "";
|
|
3524
|
+
for (const line of lines) {
|
|
3525
|
+
if (line.startsWith("data: ")) {
|
|
3526
|
+
try {
|
|
3527
|
+
const event = JSON.parse(line.slice(6));
|
|
3528
|
+
yield event;
|
|
3529
|
+
if (event.type === "exit") return;
|
|
3530
|
+
} catch {
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
} finally {
|
|
3536
|
+
reader.releaseLock();
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
};
|
|
3540
|
+
var SandboxWatch = class {
|
|
3541
|
+
constructor(endpoint, token) {
|
|
3542
|
+
this.endpoint = endpoint;
|
|
3543
|
+
this.token = token;
|
|
3544
|
+
}
|
|
3545
|
+
authHeader() {
|
|
3546
|
+
return { Authorization: `Bearer ${this.token}` };
|
|
3547
|
+
}
|
|
3548
|
+
/** Start watching a path. Events delivered via sandbox.events() SSE stream. */
|
|
3549
|
+
async add(opts) {
|
|
3550
|
+
const res = await fetch(`${this.endpoint}/watch`, {
|
|
3551
|
+
method: "POST",
|
|
3552
|
+
headers: { ...this.authHeader(), "Content-Type": "application/json" },
|
|
3553
|
+
body: JSON.stringify(opts)
|
|
3554
|
+
});
|
|
3555
|
+
if (!res.ok) throw new Error(`watch.add failed: ${await res.text()}`);
|
|
3556
|
+
return await res.json();
|
|
3557
|
+
}
|
|
3558
|
+
/** List active watches. */
|
|
3559
|
+
async list() {
|
|
3560
|
+
const res = await fetch(`${this.endpoint}/watch`, {
|
|
3561
|
+
headers: this.authHeader()
|
|
3562
|
+
});
|
|
3563
|
+
if (!res.ok) throw new Error(`watch.list failed: ${await res.text()}`);
|
|
3564
|
+
const data = await res.json();
|
|
3565
|
+
return data.watches;
|
|
3566
|
+
}
|
|
3567
|
+
/** Stop watching a path. */
|
|
3568
|
+
async remove(path) {
|
|
3569
|
+
const res = await fetch(`${this.endpoint}/watch?path=${encodeURIComponent(path)}`, {
|
|
3570
|
+
method: "DELETE",
|
|
3571
|
+
headers: this.authHeader()
|
|
3572
|
+
});
|
|
3573
|
+
if (!res.ok) throw new Error(`watch.remove failed: ${await res.text()}`);
|
|
3574
|
+
}
|
|
3575
|
+
};
|
|
3233
3576
|
var SandboxClient = class _SandboxClient {
|
|
3234
3577
|
id;
|
|
3235
3578
|
config;
|
|
@@ -3239,12 +3582,18 @@ var SandboxClient = class _SandboxClient {
|
|
|
3239
3582
|
token;
|
|
3240
3583
|
/** File operations (direct to exec-server) */
|
|
3241
3584
|
files;
|
|
3585
|
+
/** Concurrent process management (direct to exec-server) */
|
|
3586
|
+
processes;
|
|
3587
|
+
/** Filesystem watch management (direct to exec-server) */
|
|
3588
|
+
watch;
|
|
3242
3589
|
constructor(id, config, endpoint, token) {
|
|
3243
3590
|
this.id = id;
|
|
3244
3591
|
this.config = config;
|
|
3245
3592
|
this.endpoint = endpoint;
|
|
3246
3593
|
this.token = token;
|
|
3247
3594
|
this.files = endpoint && token ? new SandboxFiles(endpoint, token) : null;
|
|
3595
|
+
this.processes = endpoint && token ? new SandboxProcesses(endpoint, token) : null;
|
|
3596
|
+
this.watch = endpoint && token ? new SandboxWatch(endpoint, token) : null;
|
|
3248
3597
|
}
|
|
3249
3598
|
// ---------------------------------------------------------------------------
|
|
3250
3599
|
// Factory
|
|
@@ -3264,7 +3613,8 @@ var SandboxClient = class _SandboxClient {
|
|
|
3264
3613
|
idleTimeoutMs: options?.idleTimeoutMs ?? 3e5,
|
|
3265
3614
|
resources: options?.resources,
|
|
3266
3615
|
env: options?.env,
|
|
3267
|
-
storage: options?.storageGi !== void 0 ? { enabled: true, sizeGi: options.storageGi } : void 0
|
|
3616
|
+
storage: options?.storageGi !== void 0 ? { enabled: true, sizeGi: options.storageGi } : void 0,
|
|
3617
|
+
volumeMounts: options?.volumeMounts
|
|
3268
3618
|
}
|
|
3269
3619
|
});
|
|
3270
3620
|
return new _SandboxClient(record.id, config, record.endpoint, record.token);
|
|
@@ -3292,10 +3642,21 @@ var SandboxClient = class _SandboxClient {
|
|
|
3292
3642
|
// Exec — SSE streaming (primary)
|
|
3293
3643
|
// ---------------------------------------------------------------------------
|
|
3294
3644
|
/**
|
|
3295
|
-
* Execute a command and stream output as async iterable events.
|
|
3645
|
+
* Execute a command and stream output as async iterable SSE events.
|
|
3646
|
+
*
|
|
3647
|
+
* **Stateless mode**: each exec() call runs in an isolated bash invocation.
|
|
3648
|
+
* Shell state (CWD changes, exported env vars, functions) is NOT preserved
|
|
3649
|
+
* between calls.
|
|
3296
3650
|
*
|
|
3297
|
-
*
|
|
3298
|
-
*
|
|
3651
|
+
* For state-preserving execution (CWD, env), use `run()` which runs in the
|
|
3652
|
+
* persistent active shell and returns the result once complete.
|
|
3653
|
+
*
|
|
3654
|
+
* For streaming + state-preserving (advanced), combine `sandbox.events()` with `run()`:
|
|
3655
|
+
* ```typescript
|
|
3656
|
+
* const eventStream = sandbox.events({ type: 'stdout' })
|
|
3657
|
+
* sandbox.run(['npm', 'install']) // don't await yet
|
|
3658
|
+
* for await (const ev of eventStream) { ... }
|
|
3659
|
+
* ```
|
|
3299
3660
|
*
|
|
3300
3661
|
* @example
|
|
3301
3662
|
* ```typescript
|
|
@@ -3307,13 +3668,13 @@ var SandboxClient = class _SandboxClient {
|
|
|
3307
3668
|
*/
|
|
3308
3669
|
async *exec(command, options) {
|
|
3309
3670
|
this.assertDirect();
|
|
3310
|
-
const res = await fetch(`${this.endpoint}/exec
|
|
3671
|
+
const res = await fetch(`${this.endpoint}/exec`, {
|
|
3311
3672
|
method: "POST",
|
|
3312
3673
|
headers: {
|
|
3313
3674
|
Authorization: `Bearer ${this.token}`,
|
|
3314
3675
|
"Content-Type": "application/json"
|
|
3315
3676
|
},
|
|
3316
|
-
body: JSON.stringify({ command, ...options })
|
|
3677
|
+
body: JSON.stringify({ command, ...options, stateless: true, stream: true })
|
|
3317
3678
|
});
|
|
3318
3679
|
if (!res.ok) {
|
|
3319
3680
|
throw new Error(`exec failed (${res.status}): ${await res.text()}`);
|
|
@@ -3366,6 +3727,58 @@ var SandboxClient = class _SandboxClient {
|
|
|
3366
3727
|
return { stdout, stderr, exitCode, durationMs };
|
|
3367
3728
|
}
|
|
3368
3729
|
// ---------------------------------------------------------------------------
|
|
3730
|
+
// Events — Unified SSE stream
|
|
3731
|
+
// ---------------------------------------------------------------------------
|
|
3732
|
+
/**
|
|
3733
|
+
* Subscribe to the unified event stream (SSE).
|
|
3734
|
+
*
|
|
3735
|
+
* Receives all sandbox events: stdout, stderr, exit, port, file, shell, resource.
|
|
3736
|
+
* Filter by type/pid/shellId using query params.
|
|
3737
|
+
*
|
|
3738
|
+
* @example
|
|
3739
|
+
* ```typescript
|
|
3740
|
+
* for await (const event of sandbox.events({ type: 'file' })) {
|
|
3741
|
+
* console.log('File changed:', event.path, event.event)
|
|
3742
|
+
* }
|
|
3743
|
+
* ```
|
|
3744
|
+
*/
|
|
3745
|
+
async *events(filter) {
|
|
3746
|
+
this.assertDirect();
|
|
3747
|
+
const params = new URLSearchParams();
|
|
3748
|
+
if (filter?.type) params.set("type", filter.type);
|
|
3749
|
+
if (filter?.pid !== void 0) params.set("pid", String(filter.pid));
|
|
3750
|
+
if (filter?.shellId) params.set("shellId", filter.shellId);
|
|
3751
|
+
const qs = params.toString();
|
|
3752
|
+
const url = `${this.endpoint}/events${qs ? `?${qs}` : ""}`;
|
|
3753
|
+
const res = await fetch(url, {
|
|
3754
|
+
headers: { Authorization: `Bearer ${this.token}` }
|
|
3755
|
+
});
|
|
3756
|
+
if (!res.ok) throw new Error(`events failed (${res.status}): ${await res.text()}`);
|
|
3757
|
+
if (!res.body) throw new Error("events: no response body");
|
|
3758
|
+
const decoder = new TextDecoder();
|
|
3759
|
+
const reader = res.body.getReader();
|
|
3760
|
+
let buffer = "";
|
|
3761
|
+
try {
|
|
3762
|
+
while (true) {
|
|
3763
|
+
const { done, value } = await reader.read();
|
|
3764
|
+
if (done) break;
|
|
3765
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3766
|
+
const lines = buffer.split("\n");
|
|
3767
|
+
buffer = lines.pop() ?? "";
|
|
3768
|
+
for (const line of lines) {
|
|
3769
|
+
if (line.startsWith("data: ")) {
|
|
3770
|
+
try {
|
|
3771
|
+
yield JSON.parse(line.slice(6));
|
|
3772
|
+
} catch {
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
} finally {
|
|
3778
|
+
reader.releaseLock();
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3781
|
+
// ---------------------------------------------------------------------------
|
|
3369
3782
|
// PTY — Interactive terminal (WebSocket)
|
|
3370
3783
|
// ---------------------------------------------------------------------------
|
|
3371
3784
|
/**
|
|
@@ -3411,7 +3824,7 @@ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
|
3411
3824
|
]);
|
|
3412
3825
|
var DEFAULT_POLL_INTERVAL_MS = 3e3;
|
|
3413
3826
|
var DEFAULT_WAIT_TIMEOUT_MS = 72e5;
|
|
3414
|
-
var
|
|
3827
|
+
var RunHandle = class {
|
|
3415
3828
|
id;
|
|
3416
3829
|
config;
|
|
3417
3830
|
constructor(id, config) {
|
|
@@ -3435,7 +3848,7 @@ var WorkerHandle = class {
|
|
|
3435
3848
|
*
|
|
3436
3849
|
* @param options.pollIntervalMs - How often to poll in ms (default: 3000)
|
|
3437
3850
|
* @param options.timeoutMs - Max time to wait before throwing (default: 7_200_000 = 2h)
|
|
3438
|
-
* @returns
|
|
3851
|
+
* @returns RunResult with exit code, status, stdout/stderr
|
|
3439
3852
|
* @throws Error if waitTimeout is exceeded
|
|
3440
3853
|
*
|
|
3441
3854
|
* @example
|
|
@@ -3503,7 +3916,7 @@ var WorkerHandle = class {
|
|
|
3503
3916
|
await callApi(this.config, `/workers/${this.id}`, { method: "DELETE" });
|
|
3504
3917
|
}
|
|
3505
3918
|
};
|
|
3506
|
-
var
|
|
3919
|
+
var RunsClient = {
|
|
3507
3920
|
// --------------------------------------------------------------------------
|
|
3508
3921
|
// Run
|
|
3509
3922
|
// --------------------------------------------------------------------------
|
|
@@ -3515,7 +3928,7 @@ var WorkersClient = {
|
|
|
3515
3928
|
*
|
|
3516
3929
|
* @example
|
|
3517
3930
|
* ```typescript
|
|
3518
|
-
* const
|
|
3931
|
+
* const run = await RunsClient.create(config, {
|
|
3519
3932
|
* image: 'registry.sylphx.com/sylphx/trainer:abc123',
|
|
3520
3933
|
* command: ['python', 'train.py', '--fold', '3'],
|
|
3521
3934
|
* resources: { requests: { cpu: '4', memory: '16Gi' } },
|
|
@@ -3525,7 +3938,7 @@ var WorkersClient = {
|
|
|
3525
3938
|
* ```
|
|
3526
3939
|
*/
|
|
3527
3940
|
async run(config, options) {
|
|
3528
|
-
const run = await callApi(config, "/
|
|
3941
|
+
const run = await callApi(config, "/runs", {
|
|
3529
3942
|
method: "POST",
|
|
3530
3943
|
body: {
|
|
3531
3944
|
image: options.image,
|
|
@@ -3536,25 +3949,25 @@ var WorkersClient = {
|
|
|
3536
3949
|
volumeMounts: options.volumeMounts
|
|
3537
3950
|
}
|
|
3538
3951
|
});
|
|
3539
|
-
return new
|
|
3952
|
+
return new RunHandle(run.id, config);
|
|
3540
3953
|
},
|
|
3541
3954
|
// --------------------------------------------------------------------------
|
|
3542
3955
|
// Get
|
|
3543
3956
|
// --------------------------------------------------------------------------
|
|
3544
3957
|
/**
|
|
3545
|
-
* Get a
|
|
3958
|
+
* Get a RunHandle for an existing run by ID.
|
|
3546
3959
|
*
|
|
3547
3960
|
* Useful for resuming monitoring across requests.
|
|
3548
3961
|
*
|
|
3549
3962
|
* @example
|
|
3550
3963
|
* ```typescript
|
|
3551
3964
|
* // Store the worker ID, retrieve later
|
|
3552
|
-
* const handle =
|
|
3965
|
+
* const handle = RunsClient.fromId(config, storedWorkerId)
|
|
3553
3966
|
* const result = await handle.wait()
|
|
3554
3967
|
* ```
|
|
3555
3968
|
*/
|
|
3556
3969
|
fromId(config, workerId) {
|
|
3557
|
-
return new
|
|
3970
|
+
return new RunHandle(workerId, config);
|
|
3558
3971
|
},
|
|
3559
3972
|
// --------------------------------------------------------------------------
|
|
3560
3973
|
// List
|
|
@@ -3564,12 +3977,12 @@ var WorkersClient = {
|
|
|
3564
3977
|
*
|
|
3565
3978
|
* @example
|
|
3566
3979
|
* ```typescript
|
|
3567
|
-
* const { workers } = await
|
|
3980
|
+
* const { workers } = await RunsClient.list(config, { status: 'running' })
|
|
3568
3981
|
* console.log(`${workers.length} workers currently running`)
|
|
3569
3982
|
* ```
|
|
3570
3983
|
*/
|
|
3571
3984
|
async list(config, options) {
|
|
3572
|
-
return callApi(config, "/
|
|
3985
|
+
return callApi(config, "/runs", {
|
|
3573
3986
|
method: "GET",
|
|
3574
3987
|
query: options?.status ? { status: options.status } : void 0
|
|
3575
3988
|
});
|
|
@@ -3580,11 +3993,11 @@ var WorkersClient = {
|
|
|
3580
3993
|
/**
|
|
3581
3994
|
* Spawn a worker and wait for it to complete in one call.
|
|
3582
3995
|
*
|
|
3583
|
-
* Equivalent to `(await
|
|
3996
|
+
* Equivalent to `(await RunsClient.create(config, options)).wait(waitOptions)`.
|
|
3584
3997
|
*
|
|
3585
3998
|
* @example
|
|
3586
3999
|
* ```typescript
|
|
3587
|
-
* const result = await
|
|
4000
|
+
* const result = await RunsClient.runAndWait(config, {
|
|
3588
4001
|
* image: 'registry.sylphx.com/sylphx/process:abc',
|
|
3589
4002
|
* command: ['node', 'dist/process.js'],
|
|
3590
4003
|
* })
|
|
@@ -3592,30 +4005,38 @@ var WorkersClient = {
|
|
|
3592
4005
|
* ```
|
|
3593
4006
|
*/
|
|
3594
4007
|
async runAndWait(config, options, waitOptions) {
|
|
3595
|
-
const handle = await
|
|
4008
|
+
const handle = await RunsClient.run(config, options);
|
|
3596
4009
|
return handle.wait(waitOptions);
|
|
3597
4010
|
}
|
|
3598
4011
|
};
|
|
3599
4012
|
function sleep2(ms) {
|
|
3600
4013
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3601
4014
|
}
|
|
4015
|
+
var WorkersClient = RunsClient;
|
|
3602
4016
|
export {
|
|
3603
4017
|
ACHIEVEMENT_TIER_CONFIG,
|
|
3604
4018
|
AuthenticationError,
|
|
3605
4019
|
AuthorizationError,
|
|
3606
4020
|
CircuitBreakerOpenError,
|
|
3607
4021
|
ERROR_CODE_STATUS,
|
|
4022
|
+
InvalidConnectionUrlError,
|
|
3608
4023
|
NetworkError,
|
|
3609
4024
|
NotFoundError,
|
|
3610
4025
|
RETRYABLE_CODES,
|
|
3611
4026
|
RateLimitError,
|
|
4027
|
+
RunHandle,
|
|
4028
|
+
RunsClient,
|
|
3612
4029
|
SandboxClient,
|
|
4030
|
+
SandboxFiles,
|
|
4031
|
+
SandboxProcesses,
|
|
4032
|
+
SandboxWatch,
|
|
3613
4033
|
StepCompleteSignal,
|
|
3614
4034
|
StepSleepSignal,
|
|
3615
4035
|
SylphxError,
|
|
3616
4036
|
TimeoutError,
|
|
4037
|
+
TriggersClient,
|
|
3617
4038
|
ValidationError,
|
|
3618
|
-
WorkerHandle,
|
|
4039
|
+
RunHandle as WorkerHandle,
|
|
3619
4040
|
WorkersClient,
|
|
3620
4041
|
acceptAllConsents,
|
|
3621
4042
|
acceptOrganizationInvitation,
|
|
@@ -3634,6 +4055,7 @@ export {
|
|
|
3634
4055
|
checkFlag,
|
|
3635
4056
|
complete,
|
|
3636
4057
|
createCheckout,
|
|
4058
|
+
createClient,
|
|
3637
4059
|
createConfig,
|
|
3638
4060
|
createCron,
|
|
3639
4061
|
createDynamicRestClient,
|
|
@@ -3642,6 +4064,7 @@ export {
|
|
|
3642
4064
|
createPortalSession,
|
|
3643
4065
|
createRestClient,
|
|
3644
4066
|
createRole,
|
|
4067
|
+
createServerClient,
|
|
3645
4068
|
createServiceWorkerScript,
|
|
3646
4069
|
createStepContext,
|
|
3647
4070
|
createTasksHandler,
|
|
@@ -3771,7 +4194,6 @@ export {
|
|
|
3771
4194
|
listTasks,
|
|
3772
4195
|
listUsers,
|
|
3773
4196
|
page,
|
|
3774
|
-
parseKey,
|
|
3775
4197
|
pauseCron,
|
|
3776
4198
|
realtimeEmit,
|
|
3777
4199
|
recordStreakActivity,
|