@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.js CHANGED
@@ -35,17 +35,24 @@ __export(index_exports, {
35
35
  AuthorizationError: () => AuthorizationError,
36
36
  CircuitBreakerOpenError: () => CircuitBreakerOpenError,
37
37
  ERROR_CODE_STATUS: () => ERROR_CODE_STATUS,
38
+ InvalidConnectionUrlError: () => InvalidConnectionUrlError,
38
39
  NetworkError: () => NetworkError,
39
40
  NotFoundError: () => NotFoundError,
40
41
  RETRYABLE_CODES: () => RETRYABLE_CODES,
41
42
  RateLimitError: () => RateLimitError,
43
+ RunHandle: () => RunHandle,
44
+ RunsClient: () => RunsClient,
42
45
  SandboxClient: () => SandboxClient,
46
+ SandboxFiles: () => SandboxFiles,
47
+ SandboxProcesses: () => SandboxProcesses,
48
+ SandboxWatch: () => SandboxWatch,
43
49
  StepCompleteSignal: () => StepCompleteSignal,
44
50
  StepSleepSignal: () => StepSleepSignal,
45
51
  SylphxError: () => SylphxError,
46
52
  TimeoutError: () => TimeoutError,
53
+ TriggersClient: () => TriggersClient,
47
54
  ValidationError: () => ValidationError,
48
- WorkerHandle: () => WorkerHandle,
55
+ WorkerHandle: () => RunHandle,
49
56
  WorkersClient: () => WorkersClient,
50
57
  acceptAllConsents: () => acceptAllConsents,
51
58
  acceptOrganizationInvitation: () => acceptOrganizationInvitation,
@@ -64,6 +71,7 @@ __export(index_exports, {
64
71
  checkFlag: () => checkFlag,
65
72
  complete: () => complete,
66
73
  createCheckout: () => createCheckout,
74
+ createClient: () => createClient,
67
75
  createConfig: () => createConfig,
68
76
  createCron: () => createCron,
69
77
  createDynamicRestClient: () => createDynamicRestClient,
@@ -72,6 +80,7 @@ __export(index_exports, {
72
80
  createPortalSession: () => createPortalSession,
73
81
  createRestClient: () => createRestClient,
74
82
  createRole: () => createRole,
83
+ createServerClient: () => createServerClient,
75
84
  createServiceWorkerScript: () => createServiceWorkerScript,
76
85
  createStepContext: () => createStepContext,
77
86
  createTasksHandler: () => createTasksHandler,
@@ -201,7 +210,6 @@ __export(index_exports, {
201
210
  listTasks: () => listTasks,
202
211
  listUsers: () => listUsers,
203
212
  page: () => page,
204
- parseKey: () => parseKey,
205
213
  pauseCron: () => pauseCron,
206
214
  realtimeEmit: () => realtimeEmit,
207
215
  recordStreakActivity: () => recordStreakActivity,
@@ -262,10 +270,104 @@ __export(index_exports, {
262
270
  });
263
271
  module.exports = __toCommonJS(index_exports);
264
272
 
273
+ // src/connection-url.ts
274
+ var SYLPHX_PROTOCOL = "sylphx:";
275
+ var DEFAULT_VERSION = "v1";
276
+ var CREDENTIAL_REGEX = /^(pk|sk)_(dev|stg|prod|prev)_[a-f0-9]{32,64}$/;
277
+ var VERSION_REGEX = /^v[0-9]+$/;
278
+ var SLUG_REGEX = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
279
+ var InvalidConnectionUrlError = class _InvalidConnectionUrlError extends Error {
280
+ code = "INVALID_CONNECTION_URL";
281
+ constructor(message) {
282
+ super(message);
283
+ this.name = "InvalidConnectionUrlError";
284
+ Object.setPrototypeOf(this, _InvalidConnectionUrlError.prototype);
285
+ }
286
+ };
287
+ function fail(reason) {
288
+ throw new InvalidConnectionUrlError(`Invalid Sylphx connection URL: ${reason}`);
289
+ }
290
+ function parseCredential(raw) {
291
+ const match = CREDENTIAL_REGEX.exec(raw);
292
+ if (!match) {
293
+ fail(`credential must match (pk|sk)_(dev|stg|prod|prev)_[a-f0-9]{32,64}, got "${raw}"`);
294
+ }
295
+ return {
296
+ credentialType: match[1],
297
+ env: match[2]
298
+ };
299
+ }
300
+ function validateSlug(candidate) {
301
+ if (!candidate || candidate.length > 63 || !SLUG_REGEX.test(candidate)) {
302
+ fail(`slug "${candidate}" is not a valid DNS label (lowercase alnum + hyphens, 1-63 chars)`);
303
+ }
304
+ return candidate;
305
+ }
306
+ function parseConnectionUrl(url) {
307
+ if (typeof url !== "string" || url.length === 0) {
308
+ fail("url must be a non-empty string");
309
+ }
310
+ let parsed;
311
+ try {
312
+ parsed = new URL(url);
313
+ } catch {
314
+ fail(`not a valid URL: "${url}"`);
315
+ }
316
+ if (parsed.protocol !== SYLPHX_PROTOCOL) {
317
+ fail(`protocol must be "sylphx:", got "${parsed.protocol}"`);
318
+ }
319
+ const credential = decodeURIComponent(parsed.username);
320
+ if (!credential) {
321
+ fail("missing credential (expected `sylphx://<credential>@<host>`)");
322
+ }
323
+ if (parsed.password) {
324
+ fail("connection URL must not contain a password component");
325
+ }
326
+ const { credentialType, env } = parseCredential(credential);
327
+ const host = parsed.host;
328
+ if (!host) {
329
+ fail("missing host");
330
+ }
331
+ const hostname = parsed.hostname;
332
+ const firstDot = hostname.indexOf(".");
333
+ if (firstDot <= 0) {
334
+ fail(`host "${hostname}" must contain at least one dot (slug.domain)`);
335
+ }
336
+ const slugCandidate = hostname.slice(0, firstDot);
337
+ const domainSuffix = hostname.slice(firstDot + 1);
338
+ if (!domainSuffix) {
339
+ fail(`host "${hostname}" has empty domain suffix`);
340
+ }
341
+ const slug = validateSlug(slugCandidate);
342
+ const rawPath = parsed.pathname.replace(/^\/+/, "").replace(/\/+$/, "");
343
+ let version = DEFAULT_VERSION;
344
+ if (rawPath !== "") {
345
+ if (!VERSION_REGEX.test(rawPath)) {
346
+ fail(`path "${parsed.pathname}" must be empty or match /v{N}`);
347
+ }
348
+ version = rawPath;
349
+ }
350
+ if (parsed.search) {
351
+ fail("connection URL must not contain a query string");
352
+ }
353
+ if (parsed.hash) {
354
+ fail("connection URL must not contain a fragment");
355
+ }
356
+ const apiBaseUrl = `https://${host}/${version}`;
357
+ return {
358
+ credential,
359
+ credentialType,
360
+ env,
361
+ slug,
362
+ host,
363
+ apiBaseUrl
364
+ };
365
+ }
366
+
265
367
  // src/constants.ts
266
368
  var SDK_API_PATH = `/v1`;
267
369
  var DEFAULT_SDK_API_HOST = "api.sylphx.com";
268
- var SDK_VERSION = "0.1.0";
370
+ var SDK_VERSION = "0.5.0";
269
371
  var SDK_PLATFORM = typeof window !== "undefined" ? "browser" : typeof process !== "undefined" && process.versions?.node ? "node" : "unknown";
270
372
  var DEFAULT_TIMEOUT_MS = 3e4;
271
373
  var SESSION_TOKEN_LIFETIME_SECONDS = 5 * 60;
@@ -600,195 +702,153 @@ function exponentialBackoff(attempt, baseDelay = BASE_RETRY_DELAY_MS, maxDelay =
600
702
  return Math.round(cappedDelay + jitter);
601
703
  }
602
704
 
603
- // src/key-validation.ts
604
- var PUBLIC_KEY_PATTERN = /^pk_(dev|stg|prod)_[a-z0-9]{12}_[a-f0-9]{32}$/;
605
- var APP_ID_PATTERN = /^app_(dev|stg|prod)_[a-z0-9_-]+$/;
606
- var SECRET_KEY_PATTERN = /^sk_(dev|stg|prod)_[a-z0-9_-]+$/;
607
- var ENV_PREFIX_MAP = {
608
- dev: "development",
609
- stg: "staging",
610
- prod: "production"
611
- };
612
- function detectKeyIssues(key) {
613
- const issues = [];
614
- if (key !== key.trim()) issues.push("whitespace");
615
- if (key.includes("\n")) issues.push("newline");
616
- if (key.includes("\r")) issues.push("carriage-return");
617
- if (key.includes(" ")) issues.push("space");
618
- if (key !== key.toLowerCase()) issues.push("uppercase-chars");
619
- return issues;
620
- }
621
- function createSanitizationWarning(keyType, issues, envVarName) {
622
- const keyTypeName = keyType === "appId" ? "App ID" : "Secret Key";
623
- return `[Sylphx] ${keyTypeName} contains ${issues.join(", ")}. This is commonly caused by Vercel CLI's 'env pull' command.
624
-
625
- To fix permanently:
626
- 1. Go to Vercel Dashboard \u2192 Your Project \u2192 Settings \u2192 Environment Variables
627
- 2. Edit ${envVarName}
628
- 3. Remove any trailing whitespace or newline characters
629
- 4. Redeploy your application
630
-
631
- The SDK will automatically sanitize the key, but fixing the source is recommended.`;
632
- }
633
- function createInvalidKeyError(keyType, key, envVarName) {
634
- const prefix = keyType === "appId" ? "app" : "sk";
635
- const maskedKey = key.length > 20 ? `${key.slice(0, 20)}...` : key;
636
- const formatHint = `${prefix}_(dev|stg|prod)_[identifier]`;
637
- const keyTypeName = keyType === "appId" ? "App ID" : "Secret Key";
638
- return `[Sylphx] Invalid ${keyTypeName} format.
639
-
640
- Expected format: ${formatHint}
641
- Received: "${maskedKey}"
642
-
643
- Please check your ${envVarName} environment variable.
644
- You can find your keys in the Sylphx Console \u2192 API Keys.
645
-
646
- Common issues:
647
- \u2022 Key has uppercase characters (must be lowercase)
648
- \u2022 Key has wrong prefix (App ID: app_, Secret Key: sk_)
649
- \u2022 Key has invalid environment (must be dev, stg, or prod)
650
- \u2022 Key was copied with extra whitespace`;
651
- }
652
- function extractEnvironment(key) {
653
- const match = key.match(/^(?:app|sk)_(dev|stg|prod)_/);
654
- if (!match) return void 0;
655
- return ENV_PREFIX_MAP[match[1]];
656
- }
657
- function validateKeyForType(key, keyType, pattern, envVarName) {
658
- const keyTypeName = keyType === "appId" ? "App ID" : "Secret Key";
659
- if (!key) {
660
- return {
661
- valid: false,
662
- sanitizedKey: "",
663
- error: `[Sylphx] ${keyTypeName} is required. Set ${envVarName} in your environment variables.`,
664
- issues: ["missing"]
665
- };
666
- }
667
- const issues = detectKeyIssues(key);
668
- if (pattern.test(key)) {
669
- return {
670
- valid: true,
671
- sanitizedKey: key,
672
- keyType,
673
- environment: extractEnvironment(key),
674
- issues: []
675
- };
676
- }
677
- const sanitized = key.trim().toLowerCase();
678
- if (pattern.test(sanitized)) {
679
- return {
680
- valid: true,
681
- sanitizedKey: sanitized,
682
- keyType,
683
- environment: extractEnvironment(sanitized),
684
- warning: createSanitizationWarning(keyType, issues, envVarName),
685
- issues
686
- };
687
- }
688
- return {
689
- valid: false,
690
- sanitizedKey: "",
691
- error: createInvalidKeyError(keyType, key, envVarName),
692
- issues: [...issues, "invalid-format"]
693
- };
694
- }
695
- function validatePublicKey(key) {
696
- return validateKeyForType(key, "publicKey", PUBLIC_KEY_PATTERN, "NEXT_PUBLIC_SYLPHX_KEY");
697
- }
698
- function validateAppId(key) {
699
- return validateKeyForType(key, "appId", APP_ID_PATTERN, "NEXT_PUBLIC_SYLPHX_APP_ID");
700
- }
701
- function validateSecretKey(key) {
702
- return validateKeyForType(key, "secret", SECRET_KEY_PATTERN, "SYLPHX_SECRET_KEY");
703
- }
704
- function validateAndSanitizeSecretKey(key) {
705
- const result = validateSecretKey(key);
706
- if (!result.valid) {
707
- throw new Error(result.error);
708
- }
709
- if (result.warning) {
710
- console.warn(result.warning);
711
- }
712
- return result.sanitizedKey;
713
- }
714
- function detectKeyType(key) {
715
- const sanitized = key.trim().toLowerCase();
716
- if (sanitized.startsWith("pk_")) return "publicKey";
717
- if (sanitized.startsWith("app_")) return "appId";
718
- if (sanitized.startsWith("sk_")) return "secret";
719
- return null;
720
- }
721
- function validateKey(key) {
722
- const keyType = key ? detectKeyType(key) : null;
723
- if (keyType === "publicKey") {
724
- return validatePublicKey(key);
725
- }
726
- if (keyType === "appId") {
727
- return validateAppId(key);
705
+ // src/config.ts
706
+ var LEGACY_EMBEDDED_REF_PATTERN = /^(pk|sk)_(dev|stg|prod|prev)_[a-z0-9]{12}_[a-f0-9]+$/;
707
+ var LEGACY_APP_KEY_PATTERN = /^app_(dev|stg|prod|prev)_/;
708
+ 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.";
709
+ function rejectLegacyKeyFormat(input) {
710
+ const trimmed = input.trim().toLowerCase();
711
+ if (LEGACY_APP_KEY_PATTERN.test(trimmed)) {
712
+ throw new SylphxError(`[Sylphx] ${MIGRATION_MESSAGE}`, { code: "BAD_REQUEST" });
728
713
  }
729
- if (keyType === "secret") {
730
- return validateSecretKey(key);
714
+ if (LEGACY_EMBEDDED_REF_PATTERN.test(trimmed)) {
715
+ throw new SylphxError(`[Sylphx] ${MIGRATION_MESSAGE}`, { code: "BAD_REQUEST" });
731
716
  }
732
- return {
733
- valid: false,
734
- sanitizedKey: "",
735
- 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.",
736
- issues: key ? ["invalid_format"] : ["missing"]
737
- };
738
717
  }
739
-
740
- // src/config.ts
741
- function parseKey(key) {
742
- const sanitized = key.trim().toLowerCase();
743
- if (sanitized.startsWith("app_")) {
718
+ function freezeConfig(opts) {
719
+ return Object.freeze({
720
+ credential: opts.credential,
721
+ credentialType: opts.credentialType,
722
+ env: opts.env,
723
+ slug: opts.slug,
724
+ baseUrl: opts.baseUrl,
725
+ accessToken: opts.accessToken,
726
+ // Backward-compat aliases
727
+ secretKey: opts.credentialType === "sk" ? opts.credential : void 0,
728
+ publicKey: opts.credentialType === "pk" ? opts.credential : void 0,
729
+ ref: opts.slug
730
+ });
731
+ }
732
+ function createClient(input) {
733
+ if (typeof input === "string") {
734
+ return createConfigFromUrl(input);
735
+ }
736
+ return createConfigFromComponents(input);
737
+ }
738
+ function createServerClient(input) {
739
+ const config = createClient(input);
740
+ if (config.credentialType !== "sk") {
744
741
  throw new SylphxError(
745
- "[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.",
742
+ "[Sylphx] createServerClient() requires a secret key (sk_*). Use a SYLPHX_SECRET_URL with an sk_ credential, or pass { secretKey } in the components object.",
746
743
  { code: "BAD_REQUEST" }
747
744
  );
748
745
  }
749
- const prefix = sanitized.startsWith("pk_") ? "pk" : sanitized.startsWith("sk_") ? "sk" : null;
750
- if (!prefix) {
746
+ return config;
747
+ }
748
+ function createConfigFromUrl(url) {
749
+ if (!url || typeof url !== "string") {
751
750
  throw new SylphxError(
752
- `[Sylphx] Invalid key format. Keys must start with 'pk_' (publishable) or 'sk_' (secret). Got: "${sanitized.slice(0, 15)}..."`,
751
+ "[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",
753
752
  { code: "BAD_REQUEST" }
754
753
  );
755
754
  }
756
- const parts = sanitized.split("_");
757
- if (parts.length !== 4) {
755
+ const trimmed = url.trim();
756
+ rejectLegacyKeyFormat(trimmed);
757
+ if (!trimmed.startsWith("sylphx://")) {
758
+ if (CREDENTIAL_REGEX.test(trimmed)) {
759
+ throw new SylphxError(
760
+ "[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.",
761
+ { code: "BAD_REQUEST" }
762
+ );
763
+ }
758
764
  throw new SylphxError(
759
- `[Sylphx] Invalid key structure. Expected format: ${prefix}_{env}_{ref}_{token} (4 segments separated by '_'). Got ${parts.length} segment(s).`,
765
+ `[Sylphx] Invalid connection URL \u2014 must start with "sylphx://". Got: "${trimmed.slice(0, 30)}..."`,
760
766
  { code: "BAD_REQUEST" }
761
767
  );
762
768
  }
763
- const [, env, ref, token] = parts;
764
- if (env !== "prod" && env !== "dev" && env !== "stg" && env !== "prev") {
765
- throw new SylphxError(
766
- `[Sylphx] Invalid key environment "${env}". Must be 'prod', 'dev', 'stg', or 'prev'.`,
767
- { code: "BAD_REQUEST" }
768
- );
769
+ let parsed;
770
+ try {
771
+ parsed = parseConnectionUrl(trimmed);
772
+ } catch (err) {
773
+ if (err instanceof InvalidConnectionUrlError) {
774
+ throw new SylphxError(err.message, { code: "BAD_REQUEST", cause: err });
775
+ }
776
+ throw err;
769
777
  }
770
- if (!/^[a-z0-9]{12}$/.test(ref)) {
771
- throw new SylphxError(
772
- `[Sylphx] Invalid project ref in key: "${ref}". Must be a 12-character lowercase alphanumeric string.`,
773
- { code: "BAD_REQUEST" }
774
- );
778
+ return freezeConfig({
779
+ credential: parsed.credential,
780
+ credentialType: parsed.credentialType,
781
+ env: parsed.env,
782
+ slug: parsed.slug,
783
+ baseUrl: parsed.apiBaseUrl
784
+ });
785
+ }
786
+ function createConfigFromComponents(input) {
787
+ const credential = input.secretKey || input.publicKey;
788
+ if (!credential) {
789
+ throw new SylphxError("[Sylphx] Either publicKey or secretKey must be provided.", {
790
+ code: "BAD_REQUEST"
791
+ });
775
792
  }
776
- const expectedTokenLen = prefix === "pk" ? 32 : 64;
777
- if (token.length !== expectedTokenLen || !/^[a-f0-9]+$/.test(token)) {
778
- throw new SylphxError(
779
- `[Sylphx] Invalid key token. ${prefix === "pk" ? "Publishable" : "Secret"} keys must have a ${expectedTokenLen}-char hex token.`,
780
- { code: "BAD_REQUEST" }
781
- );
793
+ const resolvedSlug = input.slug || input.ref;
794
+ if (!resolvedSlug) {
795
+ throw new SylphxError("[Sylphx] slug is required when using explicit components.", {
796
+ code: "BAD_REQUEST"
797
+ });
782
798
  }
783
- return {
784
- type: prefix,
785
- env,
786
- ref,
787
- token,
788
- isPublic: prefix === "pk",
789
- baseUrl: `https://${ref}.${DEFAULT_SDK_API_HOST}${SDK_API_PATH}`
790
- };
799
+ const trimmedCred = credential.trim().toLowerCase();
800
+ if (CREDENTIAL_REGEX.test(trimmedCred)) {
801
+ const match = CREDENTIAL_REGEX.exec(trimmedCred);
802
+ const credentialType = match[1];
803
+ const env = match[2];
804
+ const slug = resolvedSlug.trim().toLowerCase();
805
+ const domain = input.domain?.trim() || "sylphx.com";
806
+ const baseUrl = `https://${slug}.${domain}/v1`;
807
+ return freezeConfig({
808
+ credential: trimmedCred,
809
+ credentialType,
810
+ env,
811
+ slug,
812
+ baseUrl,
813
+ accessToken: input.accessToken
814
+ });
815
+ }
816
+ const parts = trimmedCred.split("_");
817
+ const prefix = parts[0];
818
+ if ((prefix === "pk" || prefix === "sk") && parts.length >= 3) {
819
+ const envSegment = parts[1];
820
+ const validEnvs = ["dev", "stg", "prod", "prev"];
821
+ const env = validEnvs.includes(envSegment) ? envSegment : "prod";
822
+ const slug = resolvedSlug.trim().toLowerCase();
823
+ let baseUrl;
824
+ if (input.platformUrl) {
825
+ const platform = input.platformUrl.trim().replace(/\/$/, "");
826
+ baseUrl = platform.includes("/v1") ? platform : `${platform}/v1`;
827
+ } else {
828
+ const domain = input.domain?.trim() || "api.sylphx.com";
829
+ baseUrl = `https://${slug}.${domain}/v1`;
830
+ }
831
+ return freezeConfig({
832
+ credential: trimmedCred,
833
+ credentialType: prefix,
834
+ env,
835
+ slug,
836
+ baseUrl,
837
+ accessToken: input.accessToken
838
+ });
839
+ }
840
+ throw new SylphxError(
841
+ `[Sylphx] Invalid credential format. Expected (pk|sk)_(dev|stg|prod|prev)_[a-f0-9]{32,64}. Got: "${trimmedCred.slice(0, 30)}..."`,
842
+ { code: "BAD_REQUEST" }
843
+ );
844
+ }
845
+ function withToken(config, accessToken) {
846
+ return Object.freeze({
847
+ ...config,
848
+ accessToken
849
+ });
791
850
  }
851
+ var createConfig = createClient;
792
852
  function httpStatusToErrorCode(status) {
793
853
  switch (status) {
794
854
  case 400:
@@ -821,79 +881,12 @@ function httpStatusToErrorCode(status) {
821
881
  return status >= 500 ? "INTERNAL_SERVER_ERROR" : "BAD_REQUEST";
822
882
  }
823
883
  }
824
- var REF_PATTERN = /^[a-z0-9]{12}$/;
825
- function createConfig(input) {
826
- const keyForParsing = input.secretKey || input.publicKey;
827
- if (!keyForParsing) {
828
- if (input.ref) {
829
- const trimmedRef = input.ref.trim();
830
- if (!REF_PATTERN.test(trimmedRef)) {
831
- throw new SylphxError(
832
- `[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.`,
833
- { code: "BAD_REQUEST" }
834
- );
835
- }
836
- const baseUrl2 = `https://${trimmedRef}.${DEFAULT_SDK_API_HOST}${SDK_API_PATH}`;
837
- console.warn(
838
- "[Sylphx] Providing only ref without a key is deprecated. Provide secretKey or publicKey \u2014 the ref is now embedded in keys (ADR-021)."
839
- );
840
- return Object.freeze({ ref: trimmedRef, baseUrl: baseUrl2, accessToken: input.accessToken });
841
- }
842
- throw new SylphxError(
843
- "[Sylphx] Either publicKey or secretKey must be provided to createConfig().",
844
- { code: "BAD_REQUEST" }
845
- );
846
- }
847
- const parsed = parseKey(keyForParsing);
848
- const ref = parsed.ref;
849
- const baseUrl = parsed.baseUrl;
850
- let secretKey;
851
- if (input.secretKey) {
852
- const result = validateKey(input.secretKey);
853
- if (!result.valid) {
854
- throw new SylphxError(result.error || "Invalid secret key", {
855
- code: "BAD_REQUEST",
856
- data: { issues: result.issues }
857
- });
858
- }
859
- if (result.warning) console.warn(`[Sylphx] ${result.warning}`);
860
- secretKey = result.sanitizedKey;
861
- }
862
- let publicKey;
863
- if (input.publicKey) {
864
- const result = validateKey(input.publicKey);
865
- if (!result.valid) {
866
- throw new SylphxError(result.error || "Invalid public key", {
867
- code: "BAD_REQUEST",
868
- data: { issues: result.issues }
869
- });
870
- }
871
- if (result.warning) console.warn(`[Sylphx] ${result.warning}`);
872
- publicKey = result.sanitizedKey;
873
- }
874
- return Object.freeze({
875
- secretKey,
876
- publicKey,
877
- ref,
878
- baseUrl,
879
- accessToken: input.accessToken
880
- });
881
- }
882
- function withToken(config, accessToken) {
883
- return Object.freeze({
884
- ...config,
885
- accessToken
886
- // Preserve baseUrl and ref from original config
887
- });
888
- }
889
884
  function buildHeaders(config) {
890
885
  const headers = {
891
886
  "Content-Type": "application/json"
892
887
  };
893
- if (config.secretKey) {
894
- headers["x-app-secret"] = config.secretKey;
895
- } else if (config.publicKey) {
896
- headers["x-app-secret"] = config.publicKey;
888
+ if (config.credential) {
889
+ headers["x-app-secret"] = config.credential;
897
890
  }
898
891
  if (config.accessToken) {
899
892
  headers.Authorization = `Bearer ${config.accessToken}`;
@@ -912,7 +905,8 @@ async function callApi(config, path, options = {}) {
912
905
  query,
913
906
  timeout = DEFAULT_TIMEOUT_MS,
914
907
  signal,
915
- idempotencyKey
908
+ idempotencyKey,
909
+ headers: extraHeaders
916
910
  } = options;
917
911
  let url = buildApiUrl(config, path);
918
912
  if (query) {
@@ -934,6 +928,11 @@ async function callApi(config, path, options = {}) {
934
928
  if (idempotencyKey) {
935
929
  headers["Idempotency-Key"] = idempotencyKey;
936
930
  }
931
+ if (extraHeaders) {
932
+ for (const [k, v] of Object.entries(extraHeaders)) {
933
+ headers[k] = v;
934
+ }
935
+ }
937
936
  const fetchOptions = {
938
937
  method,
939
938
  headers,
@@ -1010,7 +1009,6 @@ async function callApi(config, path, options = {}) {
1010
1009
  code: "PARSE_ERROR",
1011
1010
  cause: error instanceof Error ? error : void 0,
1012
1011
  data: { body: text.slice(0, 200) }
1013
- // Include snippet for debugging
1014
1012
  });
1015
1013
  }
1016
1014
  }
@@ -1120,6 +1118,111 @@ function installGlobalDebugHelpers() {
1120
1118
 
1121
1119
  // src/rest-client.ts
1122
1120
  var import_openapi_fetch = __toESM(require("openapi-fetch"), 1);
1121
+
1122
+ // src/key-validation.ts
1123
+ var SECRET_KEY_PATTERN = /^sk_(dev|stg|prod)_[a-z0-9_-]+$/;
1124
+ var ENV_PREFIX_MAP = {
1125
+ dev: "development",
1126
+ stg: "staging",
1127
+ prod: "production"
1128
+ };
1129
+ function detectKeyIssues(key) {
1130
+ const issues = [];
1131
+ if (key !== key.trim()) issues.push("whitespace");
1132
+ if (key.includes("\n")) issues.push("newline");
1133
+ if (key.includes("\r")) issues.push("carriage-return");
1134
+ if (key.includes(" ")) issues.push("space");
1135
+ if (key !== key.toLowerCase()) issues.push("uppercase-chars");
1136
+ return issues;
1137
+ }
1138
+ function createSanitizationWarning(keyType, issues, envVarName) {
1139
+ const keyTypeName = keyType === "appId" ? "App ID" : "Secret Key";
1140
+ return `[Sylphx] ${keyTypeName} contains ${issues.join(", ")}. This is commonly caused by Vercel CLI's 'env pull' command.
1141
+
1142
+ To fix permanently:
1143
+ 1. Go to Vercel Dashboard \u2192 Your Project \u2192 Settings \u2192 Environment Variables
1144
+ 2. Edit ${envVarName}
1145
+ 3. Remove any trailing whitespace or newline characters
1146
+ 4. Redeploy your application
1147
+
1148
+ The SDK will automatically sanitize the key, but fixing the source is recommended.`;
1149
+ }
1150
+ function createInvalidKeyError(keyType, key, envVarName) {
1151
+ const maskedKey = key.length > 20 ? `${key.slice(0, 20)}...` : key;
1152
+ const formatHint = keyType === "appId" ? "pk_(dev|stg|prod)_{ref}_{hex} or app_(dev|stg|prod)_[id]" : "sk_(dev|stg|prod)_{ref}_{hex}";
1153
+ const keyTypeName = keyType === "appId" ? "App ID" : "Secret Key";
1154
+ return `[Sylphx] Invalid ${keyTypeName} format.
1155
+
1156
+ Expected format: ${formatHint}
1157
+ Received: "${maskedKey}"
1158
+
1159
+ Please check your ${envVarName} environment variable.
1160
+ You can find your keys in the Sylphx Console \u2192 API Keys.
1161
+
1162
+ Common issues:
1163
+ \u2022 Key has uppercase characters (must be lowercase)
1164
+ \u2022 Key has wrong prefix (App ID: pk_ or app_, Secret Key: sk_)
1165
+ \u2022 Key has invalid environment (must be dev, stg, or prod)
1166
+ \u2022 Key was copied with extra whitespace`;
1167
+ }
1168
+ function extractEnvironment(key) {
1169
+ const match = key.match(/^(?:app|pk|sk)_(dev|stg|prod|prev)_/);
1170
+ if (!match) return void 0;
1171
+ return ENV_PREFIX_MAP[match[1]];
1172
+ }
1173
+ function validateKeyForType(key, keyType, pattern, envVarName) {
1174
+ const keyTypeName = keyType === "appId" ? "App ID" : "Secret Key";
1175
+ if (!key) {
1176
+ return {
1177
+ valid: false,
1178
+ sanitizedKey: "",
1179
+ error: `[Sylphx] ${keyTypeName} is required. Set ${envVarName} in your environment variables.`,
1180
+ issues: ["missing"]
1181
+ };
1182
+ }
1183
+ const issues = detectKeyIssues(key);
1184
+ if (pattern.test(key)) {
1185
+ return {
1186
+ valid: true,
1187
+ sanitizedKey: key,
1188
+ keyType,
1189
+ environment: extractEnvironment(key),
1190
+ issues: []
1191
+ };
1192
+ }
1193
+ const sanitized = key.trim().toLowerCase();
1194
+ if (pattern.test(sanitized)) {
1195
+ return {
1196
+ valid: true,
1197
+ sanitizedKey: sanitized,
1198
+ keyType,
1199
+ environment: extractEnvironment(sanitized),
1200
+ warning: createSanitizationWarning(keyType, issues, envVarName),
1201
+ issues
1202
+ };
1203
+ }
1204
+ return {
1205
+ valid: false,
1206
+ sanitizedKey: "",
1207
+ error: createInvalidKeyError(keyType, key, envVarName),
1208
+ issues: [...issues, "invalid-format"]
1209
+ };
1210
+ }
1211
+ function validateSecretKey(key) {
1212
+ return validateKeyForType(key, "secret", SECRET_KEY_PATTERN, "SYLPHX_SECRET_KEY");
1213
+ }
1214
+ function validateAndSanitizeSecretKey(key) {
1215
+ const result = validateSecretKey(key);
1216
+ if (!result.valid) {
1217
+ throw new Error(result.error);
1218
+ }
1219
+ if (result.warning) {
1220
+ console.warn(result.warning);
1221
+ }
1222
+ return result.sanitizedKey;
1223
+ }
1224
+
1225
+ // src/rest-client.ts
1123
1226
  function createAuthMiddleware(config) {
1124
1227
  return {
1125
1228
  async onRequest({ request }) {
@@ -2339,6 +2442,73 @@ async function updatePushPreferences(config, preferences) {
2339
2442
  });
2340
2443
  }
2341
2444
 
2445
+ // src/lib/triggers/index.ts
2446
+ var TriggersClient = class {
2447
+ /** Create a new trigger (cron or event source, task/run/http target) */
2448
+ static async create(config, options) {
2449
+ return callApi(config, "/triggers", {
2450
+ method: "POST",
2451
+ body: options
2452
+ });
2453
+ }
2454
+ /** List all triggers for the project */
2455
+ static async list(config) {
2456
+ return callApi(config, "/triggers");
2457
+ }
2458
+ /** Get a trigger by ID */
2459
+ static async get(config, triggerId) {
2460
+ return callApi(config, `/triggers/${triggerId}`);
2461
+ }
2462
+ /** Update a trigger */
2463
+ static async update(config, triggerId, options) {
2464
+ return callApi(config, `/triggers/${triggerId}`, {
2465
+ method: "PATCH",
2466
+ body: options
2467
+ });
2468
+ }
2469
+ /** Delete a trigger */
2470
+ static async delete(config, triggerId) {
2471
+ return callApi(config, `/triggers/${triggerId}`, { method: "DELETE" });
2472
+ }
2473
+ /** Pause a trigger */
2474
+ static async pause(config, triggerId) {
2475
+ return callApi(config, `/triggers/${triggerId}/pause`, { method: "POST" });
2476
+ }
2477
+ /** Resume a paused trigger */
2478
+ static async resume(config, triggerId) {
2479
+ return callApi(config, `/triggers/${triggerId}/resume`, { method: "POST" });
2480
+ }
2481
+ /** Fire a trigger immediately (one-shot, regardless of schedule) */
2482
+ static async fire(config, triggerId) {
2483
+ return callApi(config, `/triggers/${triggerId}/fire`, {
2484
+ method: "POST"
2485
+ });
2486
+ }
2487
+ /**
2488
+ * Publish an event — dispatches all active event triggers matching the event name.
2489
+ *
2490
+ * @example
2491
+ * ```typescript
2492
+ * await TriggersClient.publishEvent(config, 'user.signup', { userId: '123', plan: 'pro' })
2493
+ * ```
2494
+ */
2495
+ /**
2496
+ * Publish an event — dispatches all active event triggers matching the event name.
2497
+ * Endpoint: POST /triggers/events
2498
+ *
2499
+ * @example
2500
+ * ```typescript
2501
+ * await TriggersClient.publishEvent(config, 'user.signup', { userId: '123', plan: 'pro' })
2502
+ * ```
2503
+ */
2504
+ static async publishEvent(config, eventName, payload) {
2505
+ return callApi(config, "/triggers/events", {
2506
+ method: "POST",
2507
+ body: { eventName, payload: payload ?? {} }
2508
+ });
2509
+ }
2510
+ };
2511
+
2342
2512
  // src/lib/tasks/handler.ts
2343
2513
  var import_node_crypto = require("crypto");
2344
2514
  var StepCompleteSignal = class {
@@ -2355,6 +2525,14 @@ var StepSleepSignal = class {
2355
2525
  }
2356
2526
  _isStepSleepSignal = true;
2357
2527
  };
2528
+ var StepWaitEventSignal = class {
2529
+ constructor(stepName, eventName, options = {}) {
2530
+ this.stepName = stepName;
2531
+ this.eventName = eventName;
2532
+ this.options = options;
2533
+ }
2534
+ _isStepWaitEventSignal = true;
2535
+ };
2358
2536
  function createStepContext(completedSteps, resolvedWaits) {
2359
2537
  return {
2360
2538
  /**
@@ -2381,6 +2559,32 @@ function createStepContext(completedSteps, resolvedWaits) {
2381
2559
  return;
2382
2560
  }
2383
2561
  throw new StepSleepSignal(name, duration);
2562
+ },
2563
+ /**
2564
+ * Pause execution until a named event is published via TriggersClient.publishEvent().
2565
+ *
2566
+ * - If event already arrived (platform re-dispatched with result): return event payload.
2567
+ * - If not yet arrived: throw StepWaitEventSignal to pause execution.
2568
+ *
2569
+ * @param name Step identifier (unique within handler).
2570
+ * @param eventName The event name to listen for (e.g. 'user.approved').
2571
+ * @param options Optional timeout ('24h', '7d') and payload filter.
2572
+ *
2573
+ * @example Human-in-the-loop approval
2574
+ * ```typescript
2575
+ * const approval = await step.waitForEvent('wait-approval', 'order.approved', {
2576
+ * timeout: '48h',
2577
+ * filter: { orderId: payload.orderId },
2578
+ * })
2579
+ * if (!approval) throw new Error('Approval timed out')
2580
+ * await sendConfirmation(approval.approvedBy)
2581
+ * ```
2582
+ */
2583
+ async waitForEvent(name, eventName, options = {}) {
2584
+ if (resolvedWaits.has(name)) {
2585
+ return resolvedWaits.get(name) ?? null;
2586
+ }
2587
+ throw new StepWaitEventSignal(name, eventName, options);
2384
2588
  }
2385
2589
  };
2386
2590
  }
@@ -2460,9 +2664,9 @@ function createTasksHandler(taskDefs, options = {}) {
2460
2664
  for (const step of context?.steps ?? []) {
2461
2665
  completedSteps.set(step.name, step.result);
2462
2666
  }
2463
- const resolvedWaits = /* @__PURE__ */ new Set();
2667
+ const resolvedWaits = /* @__PURE__ */ new Map();
2464
2668
  for (const wait of context?.waits ?? []) {
2465
- resolvedWaits.add(wait.name);
2669
+ resolvedWaits.set(wait.name, wait.result ?? void 0);
2466
2670
  }
2467
2671
  const stepCtx = createStepContext(completedSteps, resolvedWaits);
2468
2672
  try {
@@ -2485,6 +2689,16 @@ function createTasksHandler(taskDefs, options = {}) {
2485
2689
  duration: signal.duration
2486
2690
  });
2487
2691
  }
2692
+ if (err instanceof StepWaitEventSignal || err?._isStepWaitEventSignal) {
2693
+ const signal = err;
2694
+ return Response.json({
2695
+ status: "step_wait_event",
2696
+ stepName: signal.stepName,
2697
+ eventName: signal.eventName,
2698
+ timeout: signal.options.timeout ?? null,
2699
+ filter: signal.options.filter ?? null
2700
+ });
2701
+ }
2488
2702
  const message = err instanceof Error ? err.message : String(err);
2489
2703
  console.error(`[sylphx/tasks] Task "${taskName}" threw an error:`, err);
2490
2704
  return Response.json(
@@ -3494,6 +3708,143 @@ var SandboxFiles = class {
3494
3708
  return data.files;
3495
3709
  }
3496
3710
  };
3711
+ var SandboxProcesses = class {
3712
+ constructor(endpoint, token) {
3713
+ this.endpoint = endpoint;
3714
+ this.token = token;
3715
+ }
3716
+ authHeader() {
3717
+ return { Authorization: `Bearer ${this.token}` };
3718
+ }
3719
+ /** Spawn a new tracked process. Returns processId + pid immediately. */
3720
+ async start(opts) {
3721
+ const res = await fetch(`${this.endpoint}/process/start`, {
3722
+ method: "POST",
3723
+ headers: { ...this.authHeader(), "Content-Type": "application/json" },
3724
+ body: JSON.stringify(opts)
3725
+ });
3726
+ if (!res.ok) throw new Error(`process.start failed: ${await res.text()}`);
3727
+ return await res.json();
3728
+ }
3729
+ /** List all tracked processes. */
3730
+ async list() {
3731
+ const res = await fetch(`${this.endpoint}/process/list`, {
3732
+ headers: this.authHeader()
3733
+ });
3734
+ if (!res.ok) throw new Error(`process.list failed: ${await res.text()}`);
3735
+ return await res.json();
3736
+ }
3737
+ /** Get full process info including buffered output. */
3738
+ async get(processId) {
3739
+ const res = await fetch(`${this.endpoint}/process/${processId}`, {
3740
+ headers: this.authHeader()
3741
+ });
3742
+ if (!res.ok) throw new Error(`process.get failed: ${await res.text()}`);
3743
+ return await res.json();
3744
+ }
3745
+ /** Send a signal to a process. */
3746
+ async kill(processId, signal = "SIGTERM") {
3747
+ const res = await fetch(`${this.endpoint}/process/${processId}/kill`, {
3748
+ method: "POST",
3749
+ headers: { ...this.authHeader(), "Content-Type": "application/json" },
3750
+ body: JSON.stringify({ signal })
3751
+ });
3752
+ if (!res.ok) throw new Error(`process.kill failed: ${await res.text()}`);
3753
+ }
3754
+ /** Write to process stdin. */
3755
+ async writeStdin(processId, data) {
3756
+ const res = await fetch(`${this.endpoint}/process/${processId}/input`, {
3757
+ method: "POST",
3758
+ headers: { ...this.authHeader(), "Content-Type": "application/json" },
3759
+ body: JSON.stringify({ data })
3760
+ });
3761
+ if (!res.ok) throw new Error(`process.writeStdin failed: ${await res.text()}`);
3762
+ }
3763
+ /**
3764
+ * Wait for a process to complete and return its final info.
3765
+ * Polls every 500ms until status is no longer 'running'.
3766
+ *
3767
+ * For real-time output, use stream() instead.
3768
+ */
3769
+ async wait(processId, timeoutMs = 3e5) {
3770
+ const deadline = Date.now() + timeoutMs;
3771
+ while (Date.now() < deadline) {
3772
+ const info = await this.get(processId);
3773
+ if (info.status !== "running") return info;
3774
+ await new Promise((r) => setTimeout(r, 500));
3775
+ }
3776
+ throw new Error(`Timed out waiting for process ${processId} to complete (${timeoutMs}ms)`);
3777
+ }
3778
+ /** Stream process output as async iterable SSE events. */
3779
+ async *stream(processId) {
3780
+ const res = await fetch(
3781
+ `${this.endpoint}/process/${processId}/stream`,
3782
+ { headers: this.authHeader() }
3783
+ );
3784
+ if (!res.ok) throw new Error(`process.stream failed: ${await res.text()}`);
3785
+ if (!res.body) throw new Error("process.stream: no response body");
3786
+ const decoder = new TextDecoder();
3787
+ const reader = res.body.getReader();
3788
+ let buffer = "";
3789
+ try {
3790
+ while (true) {
3791
+ const { done, value } = await reader.read();
3792
+ if (done) break;
3793
+ buffer += decoder.decode(value, { stream: true });
3794
+ const lines = buffer.split("\n");
3795
+ buffer = lines.pop() ?? "";
3796
+ for (const line of lines) {
3797
+ if (line.startsWith("data: ")) {
3798
+ try {
3799
+ const event = JSON.parse(line.slice(6));
3800
+ yield event;
3801
+ if (event.type === "exit") return;
3802
+ } catch {
3803
+ }
3804
+ }
3805
+ }
3806
+ }
3807
+ } finally {
3808
+ reader.releaseLock();
3809
+ }
3810
+ }
3811
+ };
3812
+ var SandboxWatch = class {
3813
+ constructor(endpoint, token) {
3814
+ this.endpoint = endpoint;
3815
+ this.token = token;
3816
+ }
3817
+ authHeader() {
3818
+ return { Authorization: `Bearer ${this.token}` };
3819
+ }
3820
+ /** Start watching a path. Events delivered via sandbox.events() SSE stream. */
3821
+ async add(opts) {
3822
+ const res = await fetch(`${this.endpoint}/watch`, {
3823
+ method: "POST",
3824
+ headers: { ...this.authHeader(), "Content-Type": "application/json" },
3825
+ body: JSON.stringify(opts)
3826
+ });
3827
+ if (!res.ok) throw new Error(`watch.add failed: ${await res.text()}`);
3828
+ return await res.json();
3829
+ }
3830
+ /** List active watches. */
3831
+ async list() {
3832
+ const res = await fetch(`${this.endpoint}/watch`, {
3833
+ headers: this.authHeader()
3834
+ });
3835
+ if (!res.ok) throw new Error(`watch.list failed: ${await res.text()}`);
3836
+ const data = await res.json();
3837
+ return data.watches;
3838
+ }
3839
+ /** Stop watching a path. */
3840
+ async remove(path) {
3841
+ const res = await fetch(`${this.endpoint}/watch?path=${encodeURIComponent(path)}`, {
3842
+ method: "DELETE",
3843
+ headers: this.authHeader()
3844
+ });
3845
+ if (!res.ok) throw new Error(`watch.remove failed: ${await res.text()}`);
3846
+ }
3847
+ };
3497
3848
  var SandboxClient = class _SandboxClient {
3498
3849
  id;
3499
3850
  config;
@@ -3503,12 +3854,18 @@ var SandboxClient = class _SandboxClient {
3503
3854
  token;
3504
3855
  /** File operations (direct to exec-server) */
3505
3856
  files;
3857
+ /** Concurrent process management (direct to exec-server) */
3858
+ processes;
3859
+ /** Filesystem watch management (direct to exec-server) */
3860
+ watch;
3506
3861
  constructor(id, config, endpoint, token) {
3507
3862
  this.id = id;
3508
3863
  this.config = config;
3509
3864
  this.endpoint = endpoint;
3510
3865
  this.token = token;
3511
3866
  this.files = endpoint && token ? new SandboxFiles(endpoint, token) : null;
3867
+ this.processes = endpoint && token ? new SandboxProcesses(endpoint, token) : null;
3868
+ this.watch = endpoint && token ? new SandboxWatch(endpoint, token) : null;
3512
3869
  }
3513
3870
  // ---------------------------------------------------------------------------
3514
3871
  // Factory
@@ -3528,7 +3885,8 @@ var SandboxClient = class _SandboxClient {
3528
3885
  idleTimeoutMs: options?.idleTimeoutMs ?? 3e5,
3529
3886
  resources: options?.resources,
3530
3887
  env: options?.env,
3531
- storage: options?.storageGi !== void 0 ? { enabled: true, sizeGi: options.storageGi } : void 0
3888
+ storage: options?.storageGi !== void 0 ? { enabled: true, sizeGi: options.storageGi } : void 0,
3889
+ volumeMounts: options?.volumeMounts
3532
3890
  }
3533
3891
  });
3534
3892
  return new _SandboxClient(record.id, config, record.endpoint, record.token);
@@ -3556,10 +3914,21 @@ var SandboxClient = class _SandboxClient {
3556
3914
  // Exec — SSE streaming (primary)
3557
3915
  // ---------------------------------------------------------------------------
3558
3916
  /**
3559
- * Execute a command and stream output as async iterable events.
3917
+ * Execute a command and stream output as async iterable SSE events.
3918
+ *
3919
+ * **Stateless mode**: each exec() call runs in an isolated bash invocation.
3920
+ * Shell state (CWD changes, exported env vars, functions) is NOT preserved
3921
+ * between calls.
3560
3922
  *
3561
- * Uses Server-Sent Events (SSE) for real-time stdout/stderr streaming.
3562
- * Communicates DIRECTLY with exec-server (Platform not in data path).
3923
+ * For state-preserving execution (CWD, env), use `run()` which runs in the
3924
+ * persistent active shell and returns the result once complete.
3925
+ *
3926
+ * For streaming + state-preserving (advanced), combine `sandbox.events()` with `run()`:
3927
+ * ```typescript
3928
+ * const eventStream = sandbox.events({ type: 'stdout' })
3929
+ * sandbox.run(['npm', 'install']) // don't await yet
3930
+ * for await (const ev of eventStream) { ... }
3931
+ * ```
3563
3932
  *
3564
3933
  * @example
3565
3934
  * ```typescript
@@ -3571,13 +3940,13 @@ var SandboxClient = class _SandboxClient {
3571
3940
  */
3572
3941
  async *exec(command, options) {
3573
3942
  this.assertDirect();
3574
- const res = await fetch(`${this.endpoint}/exec/stream`, {
3943
+ const res = await fetch(`${this.endpoint}/exec`, {
3575
3944
  method: "POST",
3576
3945
  headers: {
3577
3946
  Authorization: `Bearer ${this.token}`,
3578
3947
  "Content-Type": "application/json"
3579
3948
  },
3580
- body: JSON.stringify({ command, ...options })
3949
+ body: JSON.stringify({ command, ...options, stateless: true, stream: true })
3581
3950
  });
3582
3951
  if (!res.ok) {
3583
3952
  throw new Error(`exec failed (${res.status}): ${await res.text()}`);
@@ -3630,6 +3999,58 @@ var SandboxClient = class _SandboxClient {
3630
3999
  return { stdout, stderr, exitCode, durationMs };
3631
4000
  }
3632
4001
  // ---------------------------------------------------------------------------
4002
+ // Events — Unified SSE stream
4003
+ // ---------------------------------------------------------------------------
4004
+ /**
4005
+ * Subscribe to the unified event stream (SSE).
4006
+ *
4007
+ * Receives all sandbox events: stdout, stderr, exit, port, file, shell, resource.
4008
+ * Filter by type/pid/shellId using query params.
4009
+ *
4010
+ * @example
4011
+ * ```typescript
4012
+ * for await (const event of sandbox.events({ type: 'file' })) {
4013
+ * console.log('File changed:', event.path, event.event)
4014
+ * }
4015
+ * ```
4016
+ */
4017
+ async *events(filter) {
4018
+ this.assertDirect();
4019
+ const params = new URLSearchParams();
4020
+ if (filter?.type) params.set("type", filter.type);
4021
+ if (filter?.pid !== void 0) params.set("pid", String(filter.pid));
4022
+ if (filter?.shellId) params.set("shellId", filter.shellId);
4023
+ const qs = params.toString();
4024
+ const url = `${this.endpoint}/events${qs ? `?${qs}` : ""}`;
4025
+ const res = await fetch(url, {
4026
+ headers: { Authorization: `Bearer ${this.token}` }
4027
+ });
4028
+ if (!res.ok) throw new Error(`events failed (${res.status}): ${await res.text()}`);
4029
+ if (!res.body) throw new Error("events: no response body");
4030
+ const decoder = new TextDecoder();
4031
+ const reader = res.body.getReader();
4032
+ let buffer = "";
4033
+ try {
4034
+ while (true) {
4035
+ const { done, value } = await reader.read();
4036
+ if (done) break;
4037
+ buffer += decoder.decode(value, { stream: true });
4038
+ const lines = buffer.split("\n");
4039
+ buffer = lines.pop() ?? "";
4040
+ for (const line of lines) {
4041
+ if (line.startsWith("data: ")) {
4042
+ try {
4043
+ yield JSON.parse(line.slice(6));
4044
+ } catch {
4045
+ }
4046
+ }
4047
+ }
4048
+ }
4049
+ } finally {
4050
+ reader.releaseLock();
4051
+ }
4052
+ }
4053
+ // ---------------------------------------------------------------------------
3633
4054
  // PTY — Interactive terminal (WebSocket)
3634
4055
  // ---------------------------------------------------------------------------
3635
4056
  /**
@@ -3675,7 +4096,7 @@ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
3675
4096
  ]);
3676
4097
  var DEFAULT_POLL_INTERVAL_MS = 3e3;
3677
4098
  var DEFAULT_WAIT_TIMEOUT_MS = 72e5;
3678
- var WorkerHandle = class {
4099
+ var RunHandle = class {
3679
4100
  id;
3680
4101
  config;
3681
4102
  constructor(id, config) {
@@ -3699,7 +4120,7 @@ var WorkerHandle = class {
3699
4120
  *
3700
4121
  * @param options.pollIntervalMs - How often to poll in ms (default: 3000)
3701
4122
  * @param options.timeoutMs - Max time to wait before throwing (default: 7_200_000 = 2h)
3702
- * @returns WorkerResult with exit code, status, stdout/stderr
4123
+ * @returns RunResult with exit code, status, stdout/stderr
3703
4124
  * @throws Error if waitTimeout is exceeded
3704
4125
  *
3705
4126
  * @example
@@ -3767,7 +4188,7 @@ var WorkerHandle = class {
3767
4188
  await callApi(this.config, `/workers/${this.id}`, { method: "DELETE" });
3768
4189
  }
3769
4190
  };
3770
- var WorkersClient = {
4191
+ var RunsClient = {
3771
4192
  // --------------------------------------------------------------------------
3772
4193
  // Run
3773
4194
  // --------------------------------------------------------------------------
@@ -3779,7 +4200,7 @@ var WorkersClient = {
3779
4200
  *
3780
4201
  * @example
3781
4202
  * ```typescript
3782
- * const worker = await WorkersClient.run(config, {
4203
+ * const run = await RunsClient.create(config, {
3783
4204
  * image: 'registry.sylphx.com/sylphx/trainer:abc123',
3784
4205
  * command: ['python', 'train.py', '--fold', '3'],
3785
4206
  * resources: { requests: { cpu: '4', memory: '16Gi' } },
@@ -3789,7 +4210,7 @@ var WorkersClient = {
3789
4210
  * ```
3790
4211
  */
3791
4212
  async run(config, options) {
3792
- const run = await callApi(config, "/workers", {
4213
+ const run = await callApi(config, "/runs", {
3793
4214
  method: "POST",
3794
4215
  body: {
3795
4216
  image: options.image,
@@ -3800,25 +4221,25 @@ var WorkersClient = {
3800
4221
  volumeMounts: options.volumeMounts
3801
4222
  }
3802
4223
  });
3803
- return new WorkerHandle(run.id, config);
4224
+ return new RunHandle(run.id, config);
3804
4225
  },
3805
4226
  // --------------------------------------------------------------------------
3806
4227
  // Get
3807
4228
  // --------------------------------------------------------------------------
3808
4229
  /**
3809
- * Get a WorkerHandle for an existing run by ID.
4230
+ * Get a RunHandle for an existing run by ID.
3810
4231
  *
3811
4232
  * Useful for resuming monitoring across requests.
3812
4233
  *
3813
4234
  * @example
3814
4235
  * ```typescript
3815
4236
  * // Store the worker ID, retrieve later
3816
- * const handle = WorkersClient.fromId(config, storedWorkerId)
4237
+ * const handle = RunsClient.fromId(config, storedWorkerId)
3817
4238
  * const result = await handle.wait()
3818
4239
  * ```
3819
4240
  */
3820
4241
  fromId(config, workerId) {
3821
- return new WorkerHandle(workerId, config);
4242
+ return new RunHandle(workerId, config);
3822
4243
  },
3823
4244
  // --------------------------------------------------------------------------
3824
4245
  // List
@@ -3828,12 +4249,12 @@ var WorkersClient = {
3828
4249
  *
3829
4250
  * @example
3830
4251
  * ```typescript
3831
- * const { workers } = await WorkersClient.list(config, { status: 'running' })
4252
+ * const { workers } = await RunsClient.list(config, { status: 'running' })
3832
4253
  * console.log(`${workers.length} workers currently running`)
3833
4254
  * ```
3834
4255
  */
3835
4256
  async list(config, options) {
3836
- return callApi(config, "/workers", {
4257
+ return callApi(config, "/runs", {
3837
4258
  method: "GET",
3838
4259
  query: options?.status ? { status: options.status } : void 0
3839
4260
  });
@@ -3844,11 +4265,11 @@ var WorkersClient = {
3844
4265
  /**
3845
4266
  * Spawn a worker and wait for it to complete in one call.
3846
4267
  *
3847
- * Equivalent to `(await WorkersClient.run(config, options)).wait(waitOptions)`.
4268
+ * Equivalent to `(await RunsClient.create(config, options)).wait(waitOptions)`.
3848
4269
  *
3849
4270
  * @example
3850
4271
  * ```typescript
3851
- * const result = await WorkersClient.runAndWait(config, {
4272
+ * const result = await RunsClient.runAndWait(config, {
3852
4273
  * image: 'registry.sylphx.com/sylphx/process:abc',
3853
4274
  * command: ['node', 'dist/process.js'],
3854
4275
  * })
@@ -3856,13 +4277,14 @@ var WorkersClient = {
3856
4277
  * ```
3857
4278
  */
3858
4279
  async runAndWait(config, options, waitOptions) {
3859
- const handle = await WorkersClient.run(config, options);
4280
+ const handle = await RunsClient.run(config, options);
3860
4281
  return handle.wait(waitOptions);
3861
4282
  }
3862
4283
  };
3863
4284
  function sleep2(ms) {
3864
4285
  return new Promise((resolve) => setTimeout(resolve, ms));
3865
4286
  }
4287
+ var WorkersClient = RunsClient;
3866
4288
  // Annotate the CommonJS export names for ESM import in node:
3867
4289
  0 && (module.exports = {
3868
4290
  ACHIEVEMENT_TIER_CONFIG,
@@ -3870,15 +4292,22 @@ function sleep2(ms) {
3870
4292
  AuthorizationError,
3871
4293
  CircuitBreakerOpenError,
3872
4294
  ERROR_CODE_STATUS,
4295
+ InvalidConnectionUrlError,
3873
4296
  NetworkError,
3874
4297
  NotFoundError,
3875
4298
  RETRYABLE_CODES,
3876
4299
  RateLimitError,
4300
+ RunHandle,
4301
+ RunsClient,
3877
4302
  SandboxClient,
4303
+ SandboxFiles,
4304
+ SandboxProcesses,
4305
+ SandboxWatch,
3878
4306
  StepCompleteSignal,
3879
4307
  StepSleepSignal,
3880
4308
  SylphxError,
3881
4309
  TimeoutError,
4310
+ TriggersClient,
3882
4311
  ValidationError,
3883
4312
  WorkerHandle,
3884
4313
  WorkersClient,
@@ -3899,6 +4328,7 @@ function sleep2(ms) {
3899
4328
  checkFlag,
3900
4329
  complete,
3901
4330
  createCheckout,
4331
+ createClient,
3902
4332
  createConfig,
3903
4333
  createCron,
3904
4334
  createDynamicRestClient,
@@ -3907,6 +4337,7 @@ function sleep2(ms) {
3907
4337
  createPortalSession,
3908
4338
  createRestClient,
3909
4339
  createRole,
4340
+ createServerClient,
3910
4341
  createServiceWorkerScript,
3911
4342
  createStepContext,
3912
4343
  createTasksHandler,
@@ -4036,7 +4467,6 @@ function sleep2(ms) {
4036
4467
  listTasks,
4037
4468
  listUsers,
4038
4469
  page,
4039
- parseKey,
4040
4470
  pauseCron,
4041
4471
  realtimeEmit,
4042
4472
  recordStreakActivity,