@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.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.1.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/key-validation.ts
340
- var PUBLIC_KEY_PATTERN = /^pk_(dev|stg|prod)_[a-z0-9]{12}_[a-f0-9]{32}$/;
341
- var APP_ID_PATTERN = /^app_(dev|stg|prod)_[a-z0-9_-]+$/;
342
- var SECRET_KEY_PATTERN = /^sk_(dev|stg|prod)_[a-z0-9_-]+$/;
343
- var ENV_PREFIX_MAP = {
344
- dev: "development",
345
- stg: "staging",
346
- prod: "production"
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 (keyType === "secret") {
466
- return validateSecretKey(key);
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
- // src/config.ts
477
- function parseKey(key) {
478
- const sanitized = key.trim().toLowerCase();
479
- if (sanitized.startsWith("app_")) {
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] API key format has changed (ADR-021). Keys now embed the project ref.\n\nOld format: app_prod_xxx\nNew format: pk_prod_{ref}_xxx\n\nPlease regenerate your API keys from the Sylphx Console \u2192 Your App \u2192 Environments.\nOr contact support@sylphx.com for assistance.",
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
- const prefix = sanitized.startsWith("pk_") ? "pk" : sanitized.startsWith("sk_") ? "sk" : null;
486
- if (!prefix) {
474
+ return config;
475
+ }
476
+ function createConfigFromUrl(url) {
477
+ if (!url || typeof url !== "string") {
487
478
  throw new SylphxError(
488
- `[Sylphx] Invalid key format. Keys must start with 'pk_' (publishable) or 'sk_' (secret). Got: "${sanitized.slice(0, 15)}..."`,
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 parts = sanitized.split("_");
493
- if (parts.length !== 4) {
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 key structure. Expected format: ${prefix}_{env}_{ref}_{token} (4 segments separated by '_'). Got ${parts.length} segment(s).`,
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
- const [, env, ref, token] = parts;
500
- if (env !== "prod" && env !== "dev" && env !== "stg" && env !== "prev") {
501
- throw new SylphxError(
502
- `[Sylphx] Invalid key environment "${env}". Must be 'prod', 'dev', 'stg', or 'prev'.`,
503
- { code: "BAD_REQUEST" }
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
- if (!/^[a-z0-9]{12}$/.test(ref)) {
507
- throw new SylphxError(
508
- `[Sylphx] Invalid project ref in key: "${ref}". Must be a 12-character lowercase alphanumeric string.`,
509
- { code: "BAD_REQUEST" }
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 expectedTokenLen = prefix === "pk" ? 32 : 64;
513
- if (token.length !== expectedTokenLen || !/^[a-f0-9]+$/.test(token)) {
514
- throw new SylphxError(
515
- `[Sylphx] Invalid key token. ${prefix === "pk" ? "Publishable" : "Secret"} keys must have a ${expectedTokenLen}-char hex token.`,
516
- { code: "BAD_REQUEST" }
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
- return {
520
- type: prefix,
521
- env,
522
- ref,
523
- token,
524
- isPublic: prefix === "pk",
525
- baseUrl: `https://${ref}.${DEFAULT_SDK_API_HOST}${SDK_API_PATH}`
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.secretKey) {
630
- headers["x-app-secret"] = config.secretKey;
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 createClient from "openapi-fetch";
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 = createClient({
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 = createClient({
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 Set();
2395
+ const resolvedWaits = /* @__PURE__ */ new Map();
2200
2396
  for (const wait of context?.waits ?? []) {
2201
- resolvedWaits.add(wait.name);
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
- * Uses Server-Sent Events (SSE) for real-time stdout/stderr streaming.
3298
- * Communicates DIRECTLY with exec-server (Platform not in data path).
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/stream`, {
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 WorkerHandle = class {
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 WorkerResult with exit code, status, stdout/stderr
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 WorkersClient = {
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 worker = await WorkersClient.run(config, {
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, "/workers", {
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 WorkerHandle(run.id, config);
3952
+ return new RunHandle(run.id, config);
3540
3953
  },
3541
3954
  // --------------------------------------------------------------------------
3542
3955
  // Get
3543
3956
  // --------------------------------------------------------------------------
3544
3957
  /**
3545
- * Get a WorkerHandle for an existing run by ID.
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 = WorkersClient.fromId(config, storedWorkerId)
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 WorkerHandle(workerId, config);
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 WorkersClient.list(config, { status: 'running' })
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, "/workers", {
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 WorkersClient.run(config, options)).wait(waitOptions)`.
3996
+ * Equivalent to `(await RunsClient.create(config, options)).wait(waitOptions)`.
3584
3997
  *
3585
3998
  * @example
3586
3999
  * ```typescript
3587
- * const result = await WorkersClient.runAndWait(config, {
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 WorkersClient.run(config, options);
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,