@sylphx/sdk 0.3.7 → 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 +938 -140
- package/dist/index.d.ts +938 -140
- package/dist/index.js +810 -267
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +791 -269
- package/dist/index.mjs.map +1 -1
- package/dist/nextjs/index.js +44 -20
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/index.mjs +44 -20
- package/dist/nextjs/index.mjs.map +1 -1
- package/dist/react/index.d.cts +389 -32
- package/dist/react/index.d.ts +389 -32
- package/dist/react/index.js +1610 -1285
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +1284 -963
- 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 +559 -22
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +555 -22
- 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;
|
|
@@ -103,6 +197,13 @@ var SylphxError = class _SylphxError extends Error {
|
|
|
103
197
|
static isRateLimited(err) {
|
|
104
198
|
return err instanceof _SylphxError && err.code === "TOO_MANY_REQUESTS";
|
|
105
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Check if error is an account lockout error (too many failed login attempts).
|
|
202
|
+
* When true, `error.data?.lockoutUntil` contains the ISO 8601 timestamp when the lockout expires.
|
|
203
|
+
*/
|
|
204
|
+
static isAccountLocked(err) {
|
|
205
|
+
return err instanceof _SylphxError && err.code === "TOO_MANY_REQUESTS" && err.data?.code === "ACCOUNT_LOCKED";
|
|
206
|
+
}
|
|
106
207
|
/**
|
|
107
208
|
* Check if error is a quota exceeded error (plan limit reached)
|
|
108
209
|
*/
|
|
@@ -329,195 +430,153 @@ function exponentialBackoff(attempt, baseDelay = BASE_RETRY_DELAY_MS, maxDelay =
|
|
|
329
430
|
return Math.round(cappedDelay + jitter);
|
|
330
431
|
}
|
|
331
432
|
|
|
332
|
-
// src/
|
|
333
|
-
var
|
|
334
|
-
var
|
|
335
|
-
var
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
};
|
|
341
|
-
function detectKeyIssues(key) {
|
|
342
|
-
const issues = [];
|
|
343
|
-
if (key !== key.trim()) issues.push("whitespace");
|
|
344
|
-
if (key.includes("\n")) issues.push("newline");
|
|
345
|
-
if (key.includes("\r")) issues.push("carriage-return");
|
|
346
|
-
if (key.includes(" ")) issues.push("space");
|
|
347
|
-
if (key !== key.toLowerCase()) issues.push("uppercase-chars");
|
|
348
|
-
return issues;
|
|
349
|
-
}
|
|
350
|
-
function createSanitizationWarning(keyType, issues, envVarName) {
|
|
351
|
-
const keyTypeName = keyType === "appId" ? "App ID" : "Secret Key";
|
|
352
|
-
return `[Sylphx] ${keyTypeName} contains ${issues.join(", ")}. This is commonly caused by Vercel CLI's 'env pull' command.
|
|
353
|
-
|
|
354
|
-
To fix permanently:
|
|
355
|
-
1. Go to Vercel Dashboard \u2192 Your Project \u2192 Settings \u2192 Environment Variables
|
|
356
|
-
2. Edit ${envVarName}
|
|
357
|
-
3. Remove any trailing whitespace or newline characters
|
|
358
|
-
4. Redeploy your application
|
|
359
|
-
|
|
360
|
-
The SDK will automatically sanitize the key, but fixing the source is recommended.`;
|
|
361
|
-
}
|
|
362
|
-
function createInvalidKeyError(keyType, key, envVarName) {
|
|
363
|
-
const prefix = keyType === "appId" ? "app" : "sk";
|
|
364
|
-
const maskedKey = key.length > 20 ? `${key.slice(0, 20)}...` : key;
|
|
365
|
-
const formatHint = `${prefix}_(dev|stg|prod)_[identifier]`;
|
|
366
|
-
const keyTypeName = keyType === "appId" ? "App ID" : "Secret Key";
|
|
367
|
-
return `[Sylphx] Invalid ${keyTypeName} format.
|
|
368
|
-
|
|
369
|
-
Expected format: ${formatHint}
|
|
370
|
-
Received: "${maskedKey}"
|
|
371
|
-
|
|
372
|
-
Please check your ${envVarName} environment variable.
|
|
373
|
-
You can find your keys in the Sylphx Console \u2192 API Keys.
|
|
374
|
-
|
|
375
|
-
Common issues:
|
|
376
|
-
\u2022 Key has uppercase characters (must be lowercase)
|
|
377
|
-
\u2022 Key has wrong prefix (App ID: app_, Secret Key: sk_)
|
|
378
|
-
\u2022 Key has invalid environment (must be dev, stg, or prod)
|
|
379
|
-
\u2022 Key was copied with extra whitespace`;
|
|
380
|
-
}
|
|
381
|
-
function extractEnvironment(key) {
|
|
382
|
-
const match = key.match(/^(?:app|sk)_(dev|stg|prod)_/);
|
|
383
|
-
if (!match) return void 0;
|
|
384
|
-
return ENV_PREFIX_MAP[match[1]];
|
|
385
|
-
}
|
|
386
|
-
function validateKeyForType(key, keyType, pattern, envVarName) {
|
|
387
|
-
const keyTypeName = keyType === "appId" ? "App ID" : "Secret Key";
|
|
388
|
-
if (!key) {
|
|
389
|
-
return {
|
|
390
|
-
valid: false,
|
|
391
|
-
sanitizedKey: "",
|
|
392
|
-
error: `[Sylphx] ${keyTypeName} is required. Set ${envVarName} in your environment variables.`,
|
|
393
|
-
issues: ["missing"]
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
const issues = detectKeyIssues(key);
|
|
397
|
-
if (pattern.test(key)) {
|
|
398
|
-
return {
|
|
399
|
-
valid: true,
|
|
400
|
-
sanitizedKey: key,
|
|
401
|
-
keyType,
|
|
402
|
-
environment: extractEnvironment(key),
|
|
403
|
-
issues: []
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
const sanitized = key.trim().toLowerCase();
|
|
407
|
-
if (pattern.test(sanitized)) {
|
|
408
|
-
return {
|
|
409
|
-
valid: true,
|
|
410
|
-
sanitizedKey: sanitized,
|
|
411
|
-
keyType,
|
|
412
|
-
environment: extractEnvironment(sanitized),
|
|
413
|
-
warning: createSanitizationWarning(keyType, issues, envVarName),
|
|
414
|
-
issues
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
return {
|
|
418
|
-
valid: false,
|
|
419
|
-
sanitizedKey: "",
|
|
420
|
-
error: createInvalidKeyError(keyType, key, envVarName),
|
|
421
|
-
issues: [...issues, "invalid-format"]
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
function validatePublicKey(key) {
|
|
425
|
-
return validateKeyForType(key, "publicKey", PUBLIC_KEY_PATTERN, "NEXT_PUBLIC_SYLPHX_KEY");
|
|
426
|
-
}
|
|
427
|
-
function validateAppId(key) {
|
|
428
|
-
return validateKeyForType(key, "appId", APP_ID_PATTERN, "NEXT_PUBLIC_SYLPHX_APP_ID");
|
|
429
|
-
}
|
|
430
|
-
function validateSecretKey(key) {
|
|
431
|
-
return validateKeyForType(key, "secret", SECRET_KEY_PATTERN, "SYLPHX_SECRET_KEY");
|
|
432
|
-
}
|
|
433
|
-
function validateAndSanitizeSecretKey(key) {
|
|
434
|
-
const result = validateSecretKey(key);
|
|
435
|
-
if (!result.valid) {
|
|
436
|
-
throw new Error(result.error);
|
|
437
|
-
}
|
|
438
|
-
if (result.warning) {
|
|
439
|
-
console.warn(result.warning);
|
|
440
|
-
}
|
|
441
|
-
return result.sanitizedKey;
|
|
442
|
-
}
|
|
443
|
-
function detectKeyType(key) {
|
|
444
|
-
const sanitized = key.trim().toLowerCase();
|
|
445
|
-
if (sanitized.startsWith("pk_")) return "publicKey";
|
|
446
|
-
if (sanitized.startsWith("app_")) return "appId";
|
|
447
|
-
if (sanitized.startsWith("sk_")) return "secret";
|
|
448
|
-
return null;
|
|
449
|
-
}
|
|
450
|
-
function validateKey(key) {
|
|
451
|
-
const keyType = key ? detectKeyType(key) : null;
|
|
452
|
-
if (keyType === "publicKey") {
|
|
453
|
-
return validatePublicKey(key);
|
|
454
|
-
}
|
|
455
|
-
if (keyType === "appId") {
|
|
456
|
-
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" });
|
|
457
441
|
}
|
|
458
|
-
if (
|
|
459
|
-
|
|
442
|
+
if (LEGACY_EMBEDDED_REF_PATTERN.test(trimmed)) {
|
|
443
|
+
throw new SylphxError(`[Sylphx] ${MIGRATION_MESSAGE}`, { code: "BAD_REQUEST" });
|
|
460
444
|
}
|
|
461
|
-
return {
|
|
462
|
-
valid: false,
|
|
463
|
-
sanitizedKey: "",
|
|
464
|
-
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.",
|
|
465
|
-
issues: key ? ["invalid_format"] : ["missing"]
|
|
466
|
-
};
|
|
467
445
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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") {
|
|
473
469
|
throw new SylphxError(
|
|
474
|
-
"[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.",
|
|
475
471
|
{ code: "BAD_REQUEST" }
|
|
476
472
|
);
|
|
477
473
|
}
|
|
478
|
-
|
|
479
|
-
|
|
474
|
+
return config;
|
|
475
|
+
}
|
|
476
|
+
function createConfigFromUrl(url) {
|
|
477
|
+
if (!url || typeof url !== "string") {
|
|
480
478
|
throw new SylphxError(
|
|
481
|
-
|
|
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",
|
|
482
480
|
{ code: "BAD_REQUEST" }
|
|
483
481
|
);
|
|
484
482
|
}
|
|
485
|
-
const
|
|
486
|
-
|
|
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
|
+
}
|
|
487
492
|
throw new SylphxError(
|
|
488
|
-
`[Sylphx] Invalid
|
|
493
|
+
`[Sylphx] Invalid connection URL \u2014 must start with "sylphx://". Got: "${trimmed.slice(0, 30)}..."`,
|
|
489
494
|
{ code: "BAD_REQUEST" }
|
|
490
495
|
);
|
|
491
496
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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;
|
|
498
505
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
+
});
|
|
504
520
|
}
|
|
505
|
-
const
|
|
506
|
-
if (
|
|
507
|
-
throw new SylphxError(
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
);
|
|
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
|
+
});
|
|
511
526
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
+
});
|
|
520
578
|
}
|
|
579
|
+
var createConfig = createClient;
|
|
521
580
|
function httpStatusToErrorCode(status) {
|
|
522
581
|
switch (status) {
|
|
523
582
|
case 400:
|
|
@@ -550,79 +609,12 @@ function httpStatusToErrorCode(status) {
|
|
|
550
609
|
return status >= 500 ? "INTERNAL_SERVER_ERROR" : "BAD_REQUEST";
|
|
551
610
|
}
|
|
552
611
|
}
|
|
553
|
-
var REF_PATTERN = /^[a-z0-9]{12}$/;
|
|
554
|
-
function createConfig(input) {
|
|
555
|
-
const keyForParsing = input.secretKey || input.publicKey;
|
|
556
|
-
if (!keyForParsing) {
|
|
557
|
-
if (input.ref) {
|
|
558
|
-
const trimmedRef = input.ref.trim();
|
|
559
|
-
if (!REF_PATTERN.test(trimmedRef)) {
|
|
560
|
-
throw new SylphxError(
|
|
561
|
-
`[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.`,
|
|
562
|
-
{ code: "BAD_REQUEST" }
|
|
563
|
-
);
|
|
564
|
-
}
|
|
565
|
-
const baseUrl2 = `https://${trimmedRef}.${DEFAULT_SDK_API_HOST}${SDK_API_PATH}`;
|
|
566
|
-
console.warn(
|
|
567
|
-
"[Sylphx] Providing only ref without a key is deprecated. Provide secretKey or publicKey \u2014 the ref is now embedded in keys (ADR-021)."
|
|
568
|
-
);
|
|
569
|
-
return Object.freeze({ ref: trimmedRef, baseUrl: baseUrl2, accessToken: input.accessToken });
|
|
570
|
-
}
|
|
571
|
-
throw new SylphxError(
|
|
572
|
-
"[Sylphx] Either publicKey or secretKey must be provided to createConfig().",
|
|
573
|
-
{ code: "BAD_REQUEST" }
|
|
574
|
-
);
|
|
575
|
-
}
|
|
576
|
-
const parsed = parseKey(keyForParsing);
|
|
577
|
-
const ref = parsed.ref;
|
|
578
|
-
const baseUrl = parsed.baseUrl;
|
|
579
|
-
let secretKey;
|
|
580
|
-
if (input.secretKey) {
|
|
581
|
-
const result = validateKey(input.secretKey);
|
|
582
|
-
if (!result.valid) {
|
|
583
|
-
throw new SylphxError(result.error || "Invalid secret key", {
|
|
584
|
-
code: "BAD_REQUEST",
|
|
585
|
-
data: { issues: result.issues }
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
if (result.warning) console.warn(`[Sylphx] ${result.warning}`);
|
|
589
|
-
secretKey = result.sanitizedKey;
|
|
590
|
-
}
|
|
591
|
-
let publicKey;
|
|
592
|
-
if (input.publicKey) {
|
|
593
|
-
const result = validateKey(input.publicKey);
|
|
594
|
-
if (!result.valid) {
|
|
595
|
-
throw new SylphxError(result.error || "Invalid public key", {
|
|
596
|
-
code: "BAD_REQUEST",
|
|
597
|
-
data: { issues: result.issues }
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
if (result.warning) console.warn(`[Sylphx] ${result.warning}`);
|
|
601
|
-
publicKey = result.sanitizedKey;
|
|
602
|
-
}
|
|
603
|
-
return Object.freeze({
|
|
604
|
-
secretKey,
|
|
605
|
-
publicKey,
|
|
606
|
-
ref,
|
|
607
|
-
baseUrl,
|
|
608
|
-
accessToken: input.accessToken
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
function withToken(config, accessToken) {
|
|
612
|
-
return Object.freeze({
|
|
613
|
-
...config,
|
|
614
|
-
accessToken
|
|
615
|
-
// Preserve baseUrl and ref from original config
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
612
|
function buildHeaders(config) {
|
|
619
613
|
const headers = {
|
|
620
614
|
"Content-Type": "application/json"
|
|
621
615
|
};
|
|
622
|
-
if (config.
|
|
623
|
-
headers["x-app-secret"] = config.
|
|
624
|
-
} else if (config.publicKey) {
|
|
625
|
-
headers["x-app-secret"] = config.publicKey;
|
|
616
|
+
if (config.credential) {
|
|
617
|
+
headers["x-app-secret"] = config.credential;
|
|
626
618
|
}
|
|
627
619
|
if (config.accessToken) {
|
|
628
620
|
headers.Authorization = `Bearer ${config.accessToken}`;
|
|
@@ -641,7 +633,8 @@ async function callApi(config, path, options = {}) {
|
|
|
641
633
|
query,
|
|
642
634
|
timeout = DEFAULT_TIMEOUT_MS,
|
|
643
635
|
signal,
|
|
644
|
-
idempotencyKey
|
|
636
|
+
idempotencyKey,
|
|
637
|
+
headers: extraHeaders
|
|
645
638
|
} = options;
|
|
646
639
|
let url = buildApiUrl(config, path);
|
|
647
640
|
if (query) {
|
|
@@ -663,6 +656,11 @@ async function callApi(config, path, options = {}) {
|
|
|
663
656
|
if (idempotencyKey) {
|
|
664
657
|
headers["Idempotency-Key"] = idempotencyKey;
|
|
665
658
|
}
|
|
659
|
+
if (extraHeaders) {
|
|
660
|
+
for (const [k, v] of Object.entries(extraHeaders)) {
|
|
661
|
+
headers[k] = v;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
666
664
|
const fetchOptions = {
|
|
667
665
|
method,
|
|
668
666
|
headers,
|
|
@@ -739,7 +737,6 @@ async function callApi(config, path, options = {}) {
|
|
|
739
737
|
code: "PARSE_ERROR",
|
|
740
738
|
cause: error instanceof Error ? error : void 0,
|
|
741
739
|
data: { body: text.slice(0, 200) }
|
|
742
|
-
// Include snippet for debugging
|
|
743
740
|
});
|
|
744
741
|
}
|
|
745
742
|
}
|
|
@@ -848,7 +845,112 @@ function installGlobalDebugHelpers() {
|
|
|
848
845
|
}
|
|
849
846
|
|
|
850
847
|
// src/rest-client.ts
|
|
851
|
-
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
|
|
852
954
|
function createAuthMiddleware(config) {
|
|
853
955
|
return {
|
|
854
956
|
async onRequest({ request }) {
|
|
@@ -1191,7 +1293,7 @@ function validateClientConfig(config) {
|
|
|
1191
1293
|
}
|
|
1192
1294
|
function createRestClient(config) {
|
|
1193
1295
|
const { secretKey, baseUrl } = validateClientConfig(config);
|
|
1194
|
-
const client =
|
|
1296
|
+
const client = createClient2({
|
|
1195
1297
|
baseUrl: `${baseUrl}${SDK_API_PATH}`,
|
|
1196
1298
|
headers: {
|
|
1197
1299
|
"Content-Type": "application/json",
|
|
@@ -1217,7 +1319,7 @@ function createDynamicRestClient(config) {
|
|
|
1217
1319
|
secretKey,
|
|
1218
1320
|
platformUrl: baseUrl
|
|
1219
1321
|
};
|
|
1220
|
-
const client =
|
|
1322
|
+
const client = createClient2({
|
|
1221
1323
|
baseUrl: `${baseUrl}${SDK_API_PATH}`,
|
|
1222
1324
|
headers: {
|
|
1223
1325
|
"Content-Type": "application/json"
|
|
@@ -2068,6 +2170,73 @@ async function updatePushPreferences(config, preferences) {
|
|
|
2068
2170
|
});
|
|
2069
2171
|
}
|
|
2070
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
|
+
|
|
2071
2240
|
// src/lib/tasks/handler.ts
|
|
2072
2241
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
2073
2242
|
var StepCompleteSignal = class {
|
|
@@ -2084,6 +2253,14 @@ var StepSleepSignal = class {
|
|
|
2084
2253
|
}
|
|
2085
2254
|
_isStepSleepSignal = true;
|
|
2086
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
|
+
};
|
|
2087
2264
|
function createStepContext(completedSteps, resolvedWaits) {
|
|
2088
2265
|
return {
|
|
2089
2266
|
/**
|
|
@@ -2110,6 +2287,32 @@ function createStepContext(completedSteps, resolvedWaits) {
|
|
|
2110
2287
|
return;
|
|
2111
2288
|
}
|
|
2112
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);
|
|
2113
2316
|
}
|
|
2114
2317
|
};
|
|
2115
2318
|
}
|
|
@@ -2189,9 +2392,9 @@ function createTasksHandler(taskDefs, options = {}) {
|
|
|
2189
2392
|
for (const step of context?.steps ?? []) {
|
|
2190
2393
|
completedSteps.set(step.name, step.result);
|
|
2191
2394
|
}
|
|
2192
|
-
const resolvedWaits = /* @__PURE__ */ new
|
|
2395
|
+
const resolvedWaits = /* @__PURE__ */ new Map();
|
|
2193
2396
|
for (const wait of context?.waits ?? []) {
|
|
2194
|
-
resolvedWaits.
|
|
2397
|
+
resolvedWaits.set(wait.name, wait.result ?? void 0);
|
|
2195
2398
|
}
|
|
2196
2399
|
const stepCtx = createStepContext(completedSteps, resolvedWaits);
|
|
2197
2400
|
try {
|
|
@@ -2214,6 +2417,16 @@ function createTasksHandler(taskDefs, options = {}) {
|
|
|
2214
2417
|
duration: signal.duration
|
|
2215
2418
|
});
|
|
2216
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
|
+
}
|
|
2217
2430
|
const message = err instanceof Error ? err.message : String(err);
|
|
2218
2431
|
console.error(`[sylphx/tasks] Task "${taskName}" threw an error:`, err);
|
|
2219
2432
|
return Response.json(
|
|
@@ -2685,6 +2898,86 @@ function canDeleteOrganization(membership) {
|
|
|
2685
2898
|
return hasRole(membership, "super_admin");
|
|
2686
2899
|
}
|
|
2687
2900
|
|
|
2901
|
+
// src/permissions.ts
|
|
2902
|
+
async function listPermissions(config) {
|
|
2903
|
+
return callApi(config, "/permissions");
|
|
2904
|
+
}
|
|
2905
|
+
async function createPermission(config, input) {
|
|
2906
|
+
return callApi(config, "/permissions", {
|
|
2907
|
+
method: "POST",
|
|
2908
|
+
body: input
|
|
2909
|
+
});
|
|
2910
|
+
}
|
|
2911
|
+
async function deletePermission(config, permissionKey) {
|
|
2912
|
+
return callApi(config, `/permissions/${permissionKey}`, {
|
|
2913
|
+
method: "DELETE"
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2916
|
+
async function getMemberPermissions(config, orgIdOrSlug, memberId) {
|
|
2917
|
+
return callApi(
|
|
2918
|
+
config,
|
|
2919
|
+
`/orgs/${orgIdOrSlug}/members/${memberId}/permissions`
|
|
2920
|
+
);
|
|
2921
|
+
}
|
|
2922
|
+
function hasPermission(permissions, required) {
|
|
2923
|
+
return permissions.includes(required);
|
|
2924
|
+
}
|
|
2925
|
+
function hasAnyPermission(permissions, required) {
|
|
2926
|
+
return required.some((perm) => permissions.includes(perm));
|
|
2927
|
+
}
|
|
2928
|
+
function hasAllPermissions(permissions, required) {
|
|
2929
|
+
return required.every((perm) => permissions.includes(perm));
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
// src/roles.ts
|
|
2933
|
+
async function listRoles(config) {
|
|
2934
|
+
return callApi(config, "/roles");
|
|
2935
|
+
}
|
|
2936
|
+
async function getRole(config, roleKey) {
|
|
2937
|
+
return callApi(config, `/roles/${roleKey}`);
|
|
2938
|
+
}
|
|
2939
|
+
async function createRole(config, input) {
|
|
2940
|
+
const body = {
|
|
2941
|
+
key: input.key,
|
|
2942
|
+
name: input.name
|
|
2943
|
+
};
|
|
2944
|
+
if (input.description !== void 0) body.description = input.description;
|
|
2945
|
+
if (input.permissions !== void 0) body.permissionKeys = input.permissions;
|
|
2946
|
+
if (input.isDefault !== void 0) body.isDefault = input.isDefault;
|
|
2947
|
+
if (input.sortOrder !== void 0) body.sortOrder = input.sortOrder;
|
|
2948
|
+
return callApi(config, "/roles", {
|
|
2949
|
+
method: "POST",
|
|
2950
|
+
body
|
|
2951
|
+
});
|
|
2952
|
+
}
|
|
2953
|
+
async function updateRole(config, roleKey, input) {
|
|
2954
|
+
const body = {};
|
|
2955
|
+
if (input.name !== void 0) body.name = input.name;
|
|
2956
|
+
if (input.description !== void 0) body.description = input.description;
|
|
2957
|
+
if (input.permissions !== void 0) body.permissionKeys = input.permissions;
|
|
2958
|
+
if (input.isDefault !== void 0) body.isDefault = input.isDefault;
|
|
2959
|
+
if (input.sortOrder !== void 0) body.sortOrder = input.sortOrder;
|
|
2960
|
+
return callApi(config, `/roles/${roleKey}`, {
|
|
2961
|
+
method: "PUT",
|
|
2962
|
+
body
|
|
2963
|
+
});
|
|
2964
|
+
}
|
|
2965
|
+
async function deleteRole(config, roleKey) {
|
|
2966
|
+
return callApi(config, `/roles/${roleKey}`, {
|
|
2967
|
+
method: "DELETE"
|
|
2968
|
+
});
|
|
2969
|
+
}
|
|
2970
|
+
async function assignMemberRole(config, orgIdOrSlug, memberId, roleKey) {
|
|
2971
|
+
return callApi(
|
|
2972
|
+
config,
|
|
2973
|
+
`/orgs/${orgIdOrSlug}/members/${memberId}/assign-role`,
|
|
2974
|
+
{
|
|
2975
|
+
method: "PUT",
|
|
2976
|
+
body: { roleKey }
|
|
2977
|
+
}
|
|
2978
|
+
);
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2688
2981
|
// src/secrets.ts
|
|
2689
2982
|
async function getSecret(config, input) {
|
|
2690
2983
|
return callApi(config, "/secrets/get", {
|
|
@@ -3143,6 +3436,143 @@ var SandboxFiles = class {
|
|
|
3143
3436
|
return data.files;
|
|
3144
3437
|
}
|
|
3145
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
|
+
};
|
|
3146
3576
|
var SandboxClient = class _SandboxClient {
|
|
3147
3577
|
id;
|
|
3148
3578
|
config;
|
|
@@ -3152,12 +3582,18 @@ var SandboxClient = class _SandboxClient {
|
|
|
3152
3582
|
token;
|
|
3153
3583
|
/** File operations (direct to exec-server) */
|
|
3154
3584
|
files;
|
|
3585
|
+
/** Concurrent process management (direct to exec-server) */
|
|
3586
|
+
processes;
|
|
3587
|
+
/** Filesystem watch management (direct to exec-server) */
|
|
3588
|
+
watch;
|
|
3155
3589
|
constructor(id, config, endpoint, token) {
|
|
3156
3590
|
this.id = id;
|
|
3157
3591
|
this.config = config;
|
|
3158
3592
|
this.endpoint = endpoint;
|
|
3159
3593
|
this.token = token;
|
|
3160
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;
|
|
3161
3597
|
}
|
|
3162
3598
|
// ---------------------------------------------------------------------------
|
|
3163
3599
|
// Factory
|
|
@@ -3177,7 +3613,8 @@ var SandboxClient = class _SandboxClient {
|
|
|
3177
3613
|
idleTimeoutMs: options?.idleTimeoutMs ?? 3e5,
|
|
3178
3614
|
resources: options?.resources,
|
|
3179
3615
|
env: options?.env,
|
|
3180
|
-
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
|
|
3181
3618
|
}
|
|
3182
3619
|
});
|
|
3183
3620
|
return new _SandboxClient(record.id, config, record.endpoint, record.token);
|
|
@@ -3205,10 +3642,21 @@ var SandboxClient = class _SandboxClient {
|
|
|
3205
3642
|
// Exec — SSE streaming (primary)
|
|
3206
3643
|
// ---------------------------------------------------------------------------
|
|
3207
3644
|
/**
|
|
3208
|
-
* 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.
|
|
3209
3650
|
*
|
|
3210
|
-
*
|
|
3211
|
-
*
|
|
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
|
+
* ```
|
|
3212
3660
|
*
|
|
3213
3661
|
* @example
|
|
3214
3662
|
* ```typescript
|
|
@@ -3220,13 +3668,13 @@ var SandboxClient = class _SandboxClient {
|
|
|
3220
3668
|
*/
|
|
3221
3669
|
async *exec(command, options) {
|
|
3222
3670
|
this.assertDirect();
|
|
3223
|
-
const res = await fetch(`${this.endpoint}/exec
|
|
3671
|
+
const res = await fetch(`${this.endpoint}/exec`, {
|
|
3224
3672
|
method: "POST",
|
|
3225
3673
|
headers: {
|
|
3226
3674
|
Authorization: `Bearer ${this.token}`,
|
|
3227
3675
|
"Content-Type": "application/json"
|
|
3228
3676
|
},
|
|
3229
|
-
body: JSON.stringify({ command, ...options })
|
|
3677
|
+
body: JSON.stringify({ command, ...options, stateless: true, stream: true })
|
|
3230
3678
|
});
|
|
3231
3679
|
if (!res.ok) {
|
|
3232
3680
|
throw new Error(`exec failed (${res.status}): ${await res.text()}`);
|
|
@@ -3279,6 +3727,58 @@ var SandboxClient = class _SandboxClient {
|
|
|
3279
3727
|
return { stdout, stderr, exitCode, durationMs };
|
|
3280
3728
|
}
|
|
3281
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
|
+
// ---------------------------------------------------------------------------
|
|
3282
3782
|
// PTY — Interactive terminal (WebSocket)
|
|
3283
3783
|
// ---------------------------------------------------------------------------
|
|
3284
3784
|
/**
|
|
@@ -3324,7 +3824,7 @@ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
|
3324
3824
|
]);
|
|
3325
3825
|
var DEFAULT_POLL_INTERVAL_MS = 3e3;
|
|
3326
3826
|
var DEFAULT_WAIT_TIMEOUT_MS = 72e5;
|
|
3327
|
-
var
|
|
3827
|
+
var RunHandle = class {
|
|
3328
3828
|
id;
|
|
3329
3829
|
config;
|
|
3330
3830
|
constructor(id, config) {
|
|
@@ -3348,7 +3848,7 @@ var WorkerHandle = class {
|
|
|
3348
3848
|
*
|
|
3349
3849
|
* @param options.pollIntervalMs - How often to poll in ms (default: 3000)
|
|
3350
3850
|
* @param options.timeoutMs - Max time to wait before throwing (default: 7_200_000 = 2h)
|
|
3351
|
-
* @returns
|
|
3851
|
+
* @returns RunResult with exit code, status, stdout/stderr
|
|
3352
3852
|
* @throws Error if waitTimeout is exceeded
|
|
3353
3853
|
*
|
|
3354
3854
|
* @example
|
|
@@ -3416,7 +3916,7 @@ var WorkerHandle = class {
|
|
|
3416
3916
|
await callApi(this.config, `/workers/${this.id}`, { method: "DELETE" });
|
|
3417
3917
|
}
|
|
3418
3918
|
};
|
|
3419
|
-
var
|
|
3919
|
+
var RunsClient = {
|
|
3420
3920
|
// --------------------------------------------------------------------------
|
|
3421
3921
|
// Run
|
|
3422
3922
|
// --------------------------------------------------------------------------
|
|
@@ -3428,7 +3928,7 @@ var WorkersClient = {
|
|
|
3428
3928
|
*
|
|
3429
3929
|
* @example
|
|
3430
3930
|
* ```typescript
|
|
3431
|
-
* const
|
|
3931
|
+
* const run = await RunsClient.create(config, {
|
|
3432
3932
|
* image: 'registry.sylphx.com/sylphx/trainer:abc123',
|
|
3433
3933
|
* command: ['python', 'train.py', '--fold', '3'],
|
|
3434
3934
|
* resources: { requests: { cpu: '4', memory: '16Gi' } },
|
|
@@ -3438,7 +3938,7 @@ var WorkersClient = {
|
|
|
3438
3938
|
* ```
|
|
3439
3939
|
*/
|
|
3440
3940
|
async run(config, options) {
|
|
3441
|
-
const run = await callApi(config, "/
|
|
3941
|
+
const run = await callApi(config, "/runs", {
|
|
3442
3942
|
method: "POST",
|
|
3443
3943
|
body: {
|
|
3444
3944
|
image: options.image,
|
|
@@ -3449,25 +3949,25 @@ var WorkersClient = {
|
|
|
3449
3949
|
volumeMounts: options.volumeMounts
|
|
3450
3950
|
}
|
|
3451
3951
|
});
|
|
3452
|
-
return new
|
|
3952
|
+
return new RunHandle(run.id, config);
|
|
3453
3953
|
},
|
|
3454
3954
|
// --------------------------------------------------------------------------
|
|
3455
3955
|
// Get
|
|
3456
3956
|
// --------------------------------------------------------------------------
|
|
3457
3957
|
/**
|
|
3458
|
-
* Get a
|
|
3958
|
+
* Get a RunHandle for an existing run by ID.
|
|
3459
3959
|
*
|
|
3460
3960
|
* Useful for resuming monitoring across requests.
|
|
3461
3961
|
*
|
|
3462
3962
|
* @example
|
|
3463
3963
|
* ```typescript
|
|
3464
3964
|
* // Store the worker ID, retrieve later
|
|
3465
|
-
* const handle =
|
|
3965
|
+
* const handle = RunsClient.fromId(config, storedWorkerId)
|
|
3466
3966
|
* const result = await handle.wait()
|
|
3467
3967
|
* ```
|
|
3468
3968
|
*/
|
|
3469
3969
|
fromId(config, workerId) {
|
|
3470
|
-
return new
|
|
3970
|
+
return new RunHandle(workerId, config);
|
|
3471
3971
|
},
|
|
3472
3972
|
// --------------------------------------------------------------------------
|
|
3473
3973
|
// List
|
|
@@ -3477,12 +3977,12 @@ var WorkersClient = {
|
|
|
3477
3977
|
*
|
|
3478
3978
|
* @example
|
|
3479
3979
|
* ```typescript
|
|
3480
|
-
* const { workers } = await
|
|
3980
|
+
* const { workers } = await RunsClient.list(config, { status: 'running' })
|
|
3481
3981
|
* console.log(`${workers.length} workers currently running`)
|
|
3482
3982
|
* ```
|
|
3483
3983
|
*/
|
|
3484
3984
|
async list(config, options) {
|
|
3485
|
-
return callApi(config, "/
|
|
3985
|
+
return callApi(config, "/runs", {
|
|
3486
3986
|
method: "GET",
|
|
3487
3987
|
query: options?.status ? { status: options.status } : void 0
|
|
3488
3988
|
});
|
|
@@ -3493,11 +3993,11 @@ var WorkersClient = {
|
|
|
3493
3993
|
/**
|
|
3494
3994
|
* Spawn a worker and wait for it to complete in one call.
|
|
3495
3995
|
*
|
|
3496
|
-
* Equivalent to `(await
|
|
3996
|
+
* Equivalent to `(await RunsClient.create(config, options)).wait(waitOptions)`.
|
|
3497
3997
|
*
|
|
3498
3998
|
* @example
|
|
3499
3999
|
* ```typescript
|
|
3500
|
-
* const result = await
|
|
4000
|
+
* const result = await RunsClient.runAndWait(config, {
|
|
3501
4001
|
* image: 'registry.sylphx.com/sylphx/process:abc',
|
|
3502
4002
|
* command: ['node', 'dist/process.js'],
|
|
3503
4003
|
* })
|
|
@@ -3505,33 +4005,42 @@ var WorkersClient = {
|
|
|
3505
4005
|
* ```
|
|
3506
4006
|
*/
|
|
3507
4007
|
async runAndWait(config, options, waitOptions) {
|
|
3508
|
-
const handle = await
|
|
4008
|
+
const handle = await RunsClient.run(config, options);
|
|
3509
4009
|
return handle.wait(waitOptions);
|
|
3510
4010
|
}
|
|
3511
4011
|
};
|
|
3512
4012
|
function sleep2(ms) {
|
|
3513
4013
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3514
4014
|
}
|
|
4015
|
+
var WorkersClient = RunsClient;
|
|
3515
4016
|
export {
|
|
3516
4017
|
ACHIEVEMENT_TIER_CONFIG,
|
|
3517
4018
|
AuthenticationError,
|
|
3518
4019
|
AuthorizationError,
|
|
3519
4020
|
CircuitBreakerOpenError,
|
|
3520
4021
|
ERROR_CODE_STATUS,
|
|
4022
|
+
InvalidConnectionUrlError,
|
|
3521
4023
|
NetworkError,
|
|
3522
4024
|
NotFoundError,
|
|
3523
4025
|
RETRYABLE_CODES,
|
|
3524
4026
|
RateLimitError,
|
|
4027
|
+
RunHandle,
|
|
4028
|
+
RunsClient,
|
|
3525
4029
|
SandboxClient,
|
|
4030
|
+
SandboxFiles,
|
|
4031
|
+
SandboxProcesses,
|
|
4032
|
+
SandboxWatch,
|
|
3526
4033
|
StepCompleteSignal,
|
|
3527
4034
|
StepSleepSignal,
|
|
3528
4035
|
SylphxError,
|
|
3529
4036
|
TimeoutError,
|
|
4037
|
+
TriggersClient,
|
|
3530
4038
|
ValidationError,
|
|
3531
|
-
WorkerHandle,
|
|
4039
|
+
RunHandle as WorkerHandle,
|
|
3532
4040
|
WorkersClient,
|
|
3533
4041
|
acceptAllConsents,
|
|
3534
4042
|
acceptOrganizationInvitation,
|
|
4043
|
+
assignMemberRole,
|
|
3535
4044
|
batchIndex,
|
|
3536
4045
|
canDeleteOrganization,
|
|
3537
4046
|
canManageMembers,
|
|
@@ -3546,12 +4055,16 @@ export {
|
|
|
3546
4055
|
checkFlag,
|
|
3547
4056
|
complete,
|
|
3548
4057
|
createCheckout,
|
|
4058
|
+
createClient,
|
|
3549
4059
|
createConfig,
|
|
3550
4060
|
createCron,
|
|
3551
4061
|
createDynamicRestClient,
|
|
3552
4062
|
createOrganization,
|
|
4063
|
+
createPermission,
|
|
3553
4064
|
createPortalSession,
|
|
3554
4065
|
createRestClient,
|
|
4066
|
+
createRole,
|
|
4067
|
+
createServerClient,
|
|
3555
4068
|
createServiceWorkerScript,
|
|
3556
4069
|
createStepContext,
|
|
3557
4070
|
createTasksHandler,
|
|
@@ -3566,6 +4079,8 @@ export {
|
|
|
3566
4079
|
deleteEnvVar,
|
|
3567
4080
|
deleteFile,
|
|
3568
4081
|
deleteOrganization,
|
|
4082
|
+
deletePermission,
|
|
4083
|
+
deleteRole,
|
|
3569
4084
|
deleteUser,
|
|
3570
4085
|
disableDebug,
|
|
3571
4086
|
embed,
|
|
@@ -3599,6 +4114,7 @@ export {
|
|
|
3599
4114
|
getFlagPayload,
|
|
3600
4115
|
getFlags,
|
|
3601
4116
|
getLeaderboard,
|
|
4117
|
+
getMemberPermissions,
|
|
3602
4118
|
getMyReferralCode,
|
|
3603
4119
|
getOrganization,
|
|
3604
4120
|
getOrganizationInvitations,
|
|
@@ -3610,6 +4126,7 @@ export {
|
|
|
3610
4126
|
getReferralLeaderboard,
|
|
3611
4127
|
getReferralStats,
|
|
3612
4128
|
getRestErrorMessage,
|
|
4129
|
+
getRole,
|
|
3613
4130
|
getScheduledEmail,
|
|
3614
4131
|
getScheduledEmailStats,
|
|
3615
4132
|
getSearchStats,
|
|
@@ -3629,8 +4146,11 @@ export {
|
|
|
3629
4146
|
getWebhookDeliveries,
|
|
3630
4147
|
getWebhookDelivery,
|
|
3631
4148
|
getWebhookStats,
|
|
4149
|
+
hasAllPermissions,
|
|
4150
|
+
hasAnyPermission,
|
|
3632
4151
|
hasConsent,
|
|
3633
4152
|
hasError,
|
|
4153
|
+
hasPermission,
|
|
3634
4154
|
hasRole,
|
|
3635
4155
|
hasSecret,
|
|
3636
4156
|
identify,
|
|
@@ -3667,12 +4187,13 @@ export {
|
|
|
3667
4187
|
leaveOrganization,
|
|
3668
4188
|
linkAnonymousConsents,
|
|
3669
4189
|
listEnvVars,
|
|
4190
|
+
listPermissions,
|
|
4191
|
+
listRoles,
|
|
3670
4192
|
listScheduledEmails,
|
|
3671
4193
|
listSecretKeys,
|
|
3672
4194
|
listTasks,
|
|
3673
4195
|
listUsers,
|
|
3674
4196
|
page,
|
|
3675
|
-
parseKey,
|
|
3676
4197
|
pauseCron,
|
|
3677
4198
|
realtimeEmit,
|
|
3678
4199
|
recordStreakActivity,
|
|
@@ -3719,6 +4240,7 @@ export {
|
|
|
3719
4240
|
updateOrganization,
|
|
3720
4241
|
updateOrganizationMemberRole,
|
|
3721
4242
|
updatePushPreferences,
|
|
4243
|
+
updateRole,
|
|
3722
4244
|
updateUser,
|
|
3723
4245
|
updateUserMetadata,
|
|
3724
4246
|
updateWebhookConfig,
|