@sylphx/sdk 0.3.7 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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;
@@ -103,6 +197,13 @@ var SylphxError = class _SylphxError extends Error {
103
197
  static isRateLimited(err) {
104
198
  return err instanceof _SylphxError && err.code === "TOO_MANY_REQUESTS";
105
199
  }
200
+ /**
201
+ * Check if error is an account lockout error (too many failed login attempts).
202
+ * When true, `error.data?.lockoutUntil` contains the ISO 8601 timestamp when the lockout expires.
203
+ */
204
+ static isAccountLocked(err) {
205
+ return err instanceof _SylphxError && err.code === "TOO_MANY_REQUESTS" && err.data?.code === "ACCOUNT_LOCKED";
206
+ }
106
207
  /**
107
208
  * Check if error is a quota exceeded error (plan limit reached)
108
209
  */
@@ -329,195 +430,153 @@ function exponentialBackoff(attempt, baseDelay = BASE_RETRY_DELAY_MS, maxDelay =
329
430
  return Math.round(cappedDelay + jitter);
330
431
  }
331
432
 
332
- // src/key-validation.ts
333
- var PUBLIC_KEY_PATTERN = /^pk_(dev|stg|prod)_[a-z0-9]{12}_[a-f0-9]{32}$/;
334
- var APP_ID_PATTERN = /^app_(dev|stg|prod)_[a-z0-9_-]+$/;
335
- var SECRET_KEY_PATTERN = /^sk_(dev|stg|prod)_[a-z0-9_-]+$/;
336
- var ENV_PREFIX_MAP = {
337
- dev: "development",
338
- stg: "staging",
339
- prod: "production"
340
- };
341
- function detectKeyIssues(key) {
342
- const issues = [];
343
- if (key !== key.trim()) issues.push("whitespace");
344
- if (key.includes("\n")) issues.push("newline");
345
- if (key.includes("\r")) issues.push("carriage-return");
346
- if (key.includes(" ")) issues.push("space");
347
- if (key !== key.toLowerCase()) issues.push("uppercase-chars");
348
- return issues;
349
- }
350
- function createSanitizationWarning(keyType, issues, envVarName) {
351
- const keyTypeName = keyType === "appId" ? "App ID" : "Secret Key";
352
- return `[Sylphx] ${keyTypeName} contains ${issues.join(", ")}. This is commonly caused by Vercel CLI's 'env pull' command.
353
-
354
- To fix permanently:
355
- 1. Go to Vercel Dashboard \u2192 Your Project \u2192 Settings \u2192 Environment Variables
356
- 2. Edit ${envVarName}
357
- 3. Remove any trailing whitespace or newline characters
358
- 4. Redeploy your application
359
-
360
- The SDK will automatically sanitize the key, but fixing the source is recommended.`;
361
- }
362
- function createInvalidKeyError(keyType, key, envVarName) {
363
- const prefix = keyType === "appId" ? "app" : "sk";
364
- const maskedKey = key.length > 20 ? `${key.slice(0, 20)}...` : key;
365
- const formatHint = `${prefix}_(dev|stg|prod)_[identifier]`;
366
- const keyTypeName = keyType === "appId" ? "App ID" : "Secret Key";
367
- return `[Sylphx] Invalid ${keyTypeName} format.
368
-
369
- Expected format: ${formatHint}
370
- Received: "${maskedKey}"
371
-
372
- Please check your ${envVarName} environment variable.
373
- You can find your keys in the Sylphx Console \u2192 API Keys.
374
-
375
- Common issues:
376
- \u2022 Key has uppercase characters (must be lowercase)
377
- \u2022 Key has wrong prefix (App ID: app_, Secret Key: sk_)
378
- \u2022 Key has invalid environment (must be dev, stg, or prod)
379
- \u2022 Key was copied with extra whitespace`;
380
- }
381
- function extractEnvironment(key) {
382
- const match = key.match(/^(?:app|sk)_(dev|stg|prod)_/);
383
- if (!match) return void 0;
384
- return ENV_PREFIX_MAP[match[1]];
385
- }
386
- function validateKeyForType(key, keyType, pattern, envVarName) {
387
- const keyTypeName = keyType === "appId" ? "App ID" : "Secret Key";
388
- if (!key) {
389
- return {
390
- valid: false,
391
- sanitizedKey: "",
392
- error: `[Sylphx] ${keyTypeName} is required. Set ${envVarName} in your environment variables.`,
393
- issues: ["missing"]
394
- };
395
- }
396
- const issues = detectKeyIssues(key);
397
- if (pattern.test(key)) {
398
- return {
399
- valid: true,
400
- sanitizedKey: key,
401
- keyType,
402
- environment: extractEnvironment(key),
403
- issues: []
404
- };
405
- }
406
- const sanitized = key.trim().toLowerCase();
407
- if (pattern.test(sanitized)) {
408
- return {
409
- valid: true,
410
- sanitizedKey: sanitized,
411
- keyType,
412
- environment: extractEnvironment(sanitized),
413
- warning: createSanitizationWarning(keyType, issues, envVarName),
414
- issues
415
- };
416
- }
417
- return {
418
- valid: false,
419
- sanitizedKey: "",
420
- error: createInvalidKeyError(keyType, key, envVarName),
421
- issues: [...issues, "invalid-format"]
422
- };
423
- }
424
- function validatePublicKey(key) {
425
- return validateKeyForType(key, "publicKey", PUBLIC_KEY_PATTERN, "NEXT_PUBLIC_SYLPHX_KEY");
426
- }
427
- function validateAppId(key) {
428
- return validateKeyForType(key, "appId", APP_ID_PATTERN, "NEXT_PUBLIC_SYLPHX_APP_ID");
429
- }
430
- function validateSecretKey(key) {
431
- return validateKeyForType(key, "secret", SECRET_KEY_PATTERN, "SYLPHX_SECRET_KEY");
432
- }
433
- function validateAndSanitizeSecretKey(key) {
434
- const result = validateSecretKey(key);
435
- if (!result.valid) {
436
- throw new Error(result.error);
437
- }
438
- if (result.warning) {
439
- console.warn(result.warning);
440
- }
441
- return result.sanitizedKey;
442
- }
443
- function detectKeyType(key) {
444
- const sanitized = key.trim().toLowerCase();
445
- if (sanitized.startsWith("pk_")) return "publicKey";
446
- if (sanitized.startsWith("app_")) return "appId";
447
- if (sanitized.startsWith("sk_")) return "secret";
448
- return null;
449
- }
450
- function validateKey(key) {
451
- const keyType = key ? detectKeyType(key) : null;
452
- if (keyType === "publicKey") {
453
- return validatePublicKey(key);
454
- }
455
- if (keyType === "appId") {
456
- return validateAppId(key);
433
+ // src/config.ts
434
+ var LEGACY_EMBEDDED_REF_PATTERN = /^(pk|sk)_(dev|stg|prod|prev)_[a-z0-9]{12}_[a-f0-9]+$/;
435
+ var LEGACY_APP_KEY_PATTERN = /^app_(dev|stg|prod|prev)_/;
436
+ var MIGRATION_MESSAGE = "API key format has changed. Use a sylphx:// connection URL instead.\n\nNew format: sylphx://pk_prod_{hex}@your-slug.sylphx.com\n\nGenerate new credentials from the Sylphx Console \u2192 Your App \u2192 Environments.\nSee https://docs.sylphx.com/migration for details.";
437
+ function rejectLegacyKeyFormat(input) {
438
+ const trimmed = input.trim().toLowerCase();
439
+ if (LEGACY_APP_KEY_PATTERN.test(trimmed)) {
440
+ throw new SylphxError(`[Sylphx] ${MIGRATION_MESSAGE}`, { code: "BAD_REQUEST" });
457
441
  }
458
- if (keyType === "secret") {
459
- return validateSecretKey(key);
442
+ if (LEGACY_EMBEDDED_REF_PATTERN.test(trimmed)) {
443
+ throw new SylphxError(`[Sylphx] ${MIGRATION_MESSAGE}`, { code: "BAD_REQUEST" });
460
444
  }
461
- return {
462
- valid: false,
463
- sanitizedKey: "",
464
- error: key ? `Invalid key format. Keys must start with 'pk_' (publishable), 'app_' (legacy), or 'sk_' (secret), followed by environment (dev/stg/prod). Got: ${key.slice(0, 20)}...` : "API key is required but was not provided.",
465
- issues: key ? ["invalid_format"] : ["missing"]
466
- };
467
445
  }
468
-
469
- // src/config.ts
470
- function parseKey(key) {
471
- const sanitized = key.trim().toLowerCase();
472
- 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") {
473
469
  throw new SylphxError(
474
- "[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.",
475
471
  { code: "BAD_REQUEST" }
476
472
  );
477
473
  }
478
- const prefix = sanitized.startsWith("pk_") ? "pk" : sanitized.startsWith("sk_") ? "sk" : null;
479
- if (!prefix) {
474
+ return config;
475
+ }
476
+ function createConfigFromUrl(url) {
477
+ if (!url || typeof url !== "string") {
480
478
  throw new SylphxError(
481
- `[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",
482
480
  { code: "BAD_REQUEST" }
483
481
  );
484
482
  }
485
- const parts = sanitized.split("_");
486
- 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
+ }
487
492
  throw new SylphxError(
488
- `[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)}..."`,
489
494
  { code: "BAD_REQUEST" }
490
495
  );
491
496
  }
492
- const [, env, ref, token] = parts;
493
- if (env !== "prod" && env !== "dev" && env !== "stg" && env !== "prev") {
494
- throw new SylphxError(
495
- `[Sylphx] Invalid key environment "${env}". Must be 'prod', 'dev', 'stg', or 'prev'.`,
496
- { code: "BAD_REQUEST" }
497
- );
497
+ let parsed;
498
+ try {
499
+ parsed = parseConnectionUrl(trimmed);
500
+ } catch (err) {
501
+ if (err instanceof InvalidConnectionUrlError) {
502
+ throw new SylphxError(err.message, { code: "BAD_REQUEST", cause: err });
503
+ }
504
+ throw err;
498
505
  }
499
- if (!/^[a-z0-9]{12}$/.test(ref)) {
500
- throw new SylphxError(
501
- `[Sylphx] Invalid project ref in key: "${ref}". Must be a 12-character lowercase alphanumeric string.`,
502
- { code: "BAD_REQUEST" }
503
- );
506
+ return freezeConfig({
507
+ credential: parsed.credential,
508
+ credentialType: parsed.credentialType,
509
+ env: parsed.env,
510
+ slug: parsed.slug,
511
+ baseUrl: parsed.apiBaseUrl
512
+ });
513
+ }
514
+ function createConfigFromComponents(input) {
515
+ const credential = input.secretKey || input.publicKey;
516
+ if (!credential) {
517
+ throw new SylphxError("[Sylphx] Either publicKey or secretKey must be provided.", {
518
+ code: "BAD_REQUEST"
519
+ });
504
520
  }
505
- const expectedTokenLen = prefix === "pk" ? 32 : 64;
506
- if (token.length !== expectedTokenLen || !/^[a-f0-9]+$/.test(token)) {
507
- throw new SylphxError(
508
- `[Sylphx] Invalid key token. ${prefix === "pk" ? "Publishable" : "Secret"} keys must have a ${expectedTokenLen}-char hex token.`,
509
- { code: "BAD_REQUEST" }
510
- );
521
+ const resolvedSlug = input.slug || input.ref;
522
+ if (!resolvedSlug) {
523
+ throw new SylphxError("[Sylphx] slug is required when using explicit components.", {
524
+ code: "BAD_REQUEST"
525
+ });
511
526
  }
512
- return {
513
- type: prefix,
514
- env,
515
- ref,
516
- token,
517
- isPublic: prefix === "pk",
518
- baseUrl: `https://${ref}.${DEFAULT_SDK_API_HOST}${SDK_API_PATH}`
519
- };
527
+ const trimmedCred = credential.trim().toLowerCase();
528
+ if (CREDENTIAL_REGEX.test(trimmedCred)) {
529
+ const match = CREDENTIAL_REGEX.exec(trimmedCred);
530
+ const credentialType = match[1];
531
+ const env = match[2];
532
+ const slug = resolvedSlug.trim().toLowerCase();
533
+ const domain = input.domain?.trim() || "sylphx.com";
534
+ const baseUrl = `https://${slug}.${domain}/v1`;
535
+ return freezeConfig({
536
+ credential: trimmedCred,
537
+ credentialType,
538
+ env,
539
+ slug,
540
+ baseUrl,
541
+ accessToken: input.accessToken
542
+ });
543
+ }
544
+ const parts = trimmedCred.split("_");
545
+ const prefix = parts[0];
546
+ if ((prefix === "pk" || prefix === "sk") && parts.length >= 3) {
547
+ const envSegment = parts[1];
548
+ const validEnvs = ["dev", "stg", "prod", "prev"];
549
+ const env = validEnvs.includes(envSegment) ? envSegment : "prod";
550
+ const slug = resolvedSlug.trim().toLowerCase();
551
+ let baseUrl;
552
+ if (input.platformUrl) {
553
+ const platform = input.platformUrl.trim().replace(/\/$/, "");
554
+ baseUrl = platform.includes("/v1") ? platform : `${platform}/v1`;
555
+ } else {
556
+ const domain = input.domain?.trim() || "api.sylphx.com";
557
+ baseUrl = `https://${slug}.${domain}/v1`;
558
+ }
559
+ return freezeConfig({
560
+ credential: trimmedCred,
561
+ credentialType: prefix,
562
+ env,
563
+ slug,
564
+ baseUrl,
565
+ accessToken: input.accessToken
566
+ });
567
+ }
568
+ throw new SylphxError(
569
+ `[Sylphx] Invalid credential format. Expected (pk|sk)_(dev|stg|prod|prev)_[a-f0-9]{32,64}. Got: "${trimmedCred.slice(0, 30)}..."`,
570
+ { code: "BAD_REQUEST" }
571
+ );
572
+ }
573
+ function withToken(config, accessToken) {
574
+ return Object.freeze({
575
+ ...config,
576
+ accessToken
577
+ });
520
578
  }
579
+ var createConfig = createClient;
521
580
  function httpStatusToErrorCode(status) {
522
581
  switch (status) {
523
582
  case 400:
@@ -550,79 +609,12 @@ function httpStatusToErrorCode(status) {
550
609
  return status >= 500 ? "INTERNAL_SERVER_ERROR" : "BAD_REQUEST";
551
610
  }
552
611
  }
553
- var REF_PATTERN = /^[a-z0-9]{12}$/;
554
- function createConfig(input) {
555
- const keyForParsing = input.secretKey || input.publicKey;
556
- if (!keyForParsing) {
557
- if (input.ref) {
558
- const trimmedRef = input.ref.trim();
559
- if (!REF_PATTERN.test(trimmedRef)) {
560
- throw new SylphxError(
561
- `[Sylphx] Invalid project ref format: "${input.ref}". Expected a 12-character lowercase alphanumeric string (e.g. "abc123def456"). Get your ref from Platform Console \u2192 Projects \u2192 Your Project \u2192 Overview.`,
562
- { code: "BAD_REQUEST" }
563
- );
564
- }
565
- const baseUrl2 = `https://${trimmedRef}.${DEFAULT_SDK_API_HOST}${SDK_API_PATH}`;
566
- console.warn(
567
- "[Sylphx] Providing only ref without a key is deprecated. Provide secretKey or publicKey \u2014 the ref is now embedded in keys (ADR-021)."
568
- );
569
- return Object.freeze({ ref: trimmedRef, baseUrl: baseUrl2, accessToken: input.accessToken });
570
- }
571
- throw new SylphxError(
572
- "[Sylphx] Either publicKey or secretKey must be provided to createConfig().",
573
- { code: "BAD_REQUEST" }
574
- );
575
- }
576
- const parsed = parseKey(keyForParsing);
577
- const ref = parsed.ref;
578
- const baseUrl = parsed.baseUrl;
579
- let secretKey;
580
- if (input.secretKey) {
581
- const result = validateKey(input.secretKey);
582
- if (!result.valid) {
583
- throw new SylphxError(result.error || "Invalid secret key", {
584
- code: "BAD_REQUEST",
585
- data: { issues: result.issues }
586
- });
587
- }
588
- if (result.warning) console.warn(`[Sylphx] ${result.warning}`);
589
- secretKey = result.sanitizedKey;
590
- }
591
- let publicKey;
592
- if (input.publicKey) {
593
- const result = validateKey(input.publicKey);
594
- if (!result.valid) {
595
- throw new SylphxError(result.error || "Invalid public key", {
596
- code: "BAD_REQUEST",
597
- data: { issues: result.issues }
598
- });
599
- }
600
- if (result.warning) console.warn(`[Sylphx] ${result.warning}`);
601
- publicKey = result.sanitizedKey;
602
- }
603
- return Object.freeze({
604
- secretKey,
605
- publicKey,
606
- ref,
607
- baseUrl,
608
- accessToken: input.accessToken
609
- });
610
- }
611
- function withToken(config, accessToken) {
612
- return Object.freeze({
613
- ...config,
614
- accessToken
615
- // Preserve baseUrl and ref from original config
616
- });
617
- }
618
612
  function buildHeaders(config) {
619
613
  const headers = {
620
614
  "Content-Type": "application/json"
621
615
  };
622
- if (config.secretKey) {
623
- headers["x-app-secret"] = config.secretKey;
624
- } else if (config.publicKey) {
625
- headers["x-app-secret"] = config.publicKey;
616
+ if (config.credential) {
617
+ headers["x-app-secret"] = config.credential;
626
618
  }
627
619
  if (config.accessToken) {
628
620
  headers.Authorization = `Bearer ${config.accessToken}`;
@@ -641,7 +633,8 @@ async function callApi(config, path, options = {}) {
641
633
  query,
642
634
  timeout = DEFAULT_TIMEOUT_MS,
643
635
  signal,
644
- idempotencyKey
636
+ idempotencyKey,
637
+ headers: extraHeaders
645
638
  } = options;
646
639
  let url = buildApiUrl(config, path);
647
640
  if (query) {
@@ -663,6 +656,11 @@ async function callApi(config, path, options = {}) {
663
656
  if (idempotencyKey) {
664
657
  headers["Idempotency-Key"] = idempotencyKey;
665
658
  }
659
+ if (extraHeaders) {
660
+ for (const [k, v] of Object.entries(extraHeaders)) {
661
+ headers[k] = v;
662
+ }
663
+ }
666
664
  const fetchOptions = {
667
665
  method,
668
666
  headers,
@@ -739,7 +737,6 @@ async function callApi(config, path, options = {}) {
739
737
  code: "PARSE_ERROR",
740
738
  cause: error instanceof Error ? error : void 0,
741
739
  data: { body: text.slice(0, 200) }
742
- // Include snippet for debugging
743
740
  });
744
741
  }
745
742
  }
@@ -848,7 +845,112 @@ function installGlobalDebugHelpers() {
848
845
  }
849
846
 
850
847
  // src/rest-client.ts
851
- import 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
852
954
  function createAuthMiddleware(config) {
853
955
  return {
854
956
  async onRequest({ request }) {
@@ -1191,7 +1293,7 @@ function validateClientConfig(config) {
1191
1293
  }
1192
1294
  function createRestClient(config) {
1193
1295
  const { secretKey, baseUrl } = validateClientConfig(config);
1194
- const client = createClient({
1296
+ const client = createClient2({
1195
1297
  baseUrl: `${baseUrl}${SDK_API_PATH}`,
1196
1298
  headers: {
1197
1299
  "Content-Type": "application/json",
@@ -1217,7 +1319,7 @@ function createDynamicRestClient(config) {
1217
1319
  secretKey,
1218
1320
  platformUrl: baseUrl
1219
1321
  };
1220
- const client = createClient({
1322
+ const client = createClient2({
1221
1323
  baseUrl: `${baseUrl}${SDK_API_PATH}`,
1222
1324
  headers: {
1223
1325
  "Content-Type": "application/json"
@@ -2068,6 +2170,73 @@ async function updatePushPreferences(config, preferences) {
2068
2170
  });
2069
2171
  }
2070
2172
 
2173
+ // src/lib/triggers/index.ts
2174
+ var TriggersClient = class {
2175
+ /** Create a new trigger (cron or event source, task/run/http target) */
2176
+ static async create(config, options) {
2177
+ return callApi(config, "/triggers", {
2178
+ method: "POST",
2179
+ body: options
2180
+ });
2181
+ }
2182
+ /** List all triggers for the project */
2183
+ static async list(config) {
2184
+ return callApi(config, "/triggers");
2185
+ }
2186
+ /** Get a trigger by ID */
2187
+ static async get(config, triggerId) {
2188
+ return callApi(config, `/triggers/${triggerId}`);
2189
+ }
2190
+ /** Update a trigger */
2191
+ static async update(config, triggerId, options) {
2192
+ return callApi(config, `/triggers/${triggerId}`, {
2193
+ method: "PATCH",
2194
+ body: options
2195
+ });
2196
+ }
2197
+ /** Delete a trigger */
2198
+ static async delete(config, triggerId) {
2199
+ return callApi(config, `/triggers/${triggerId}`, { method: "DELETE" });
2200
+ }
2201
+ /** Pause a trigger */
2202
+ static async pause(config, triggerId) {
2203
+ return callApi(config, `/triggers/${triggerId}/pause`, { method: "POST" });
2204
+ }
2205
+ /** Resume a paused trigger */
2206
+ static async resume(config, triggerId) {
2207
+ return callApi(config, `/triggers/${triggerId}/resume`, { method: "POST" });
2208
+ }
2209
+ /** Fire a trigger immediately (one-shot, regardless of schedule) */
2210
+ static async fire(config, triggerId) {
2211
+ return callApi(config, `/triggers/${triggerId}/fire`, {
2212
+ method: "POST"
2213
+ });
2214
+ }
2215
+ /**
2216
+ * Publish an event — dispatches all active event triggers matching the event name.
2217
+ *
2218
+ * @example
2219
+ * ```typescript
2220
+ * await TriggersClient.publishEvent(config, 'user.signup', { userId: '123', plan: 'pro' })
2221
+ * ```
2222
+ */
2223
+ /**
2224
+ * Publish an event — dispatches all active event triggers matching the event name.
2225
+ * Endpoint: POST /triggers/events
2226
+ *
2227
+ * @example
2228
+ * ```typescript
2229
+ * await TriggersClient.publishEvent(config, 'user.signup', { userId: '123', plan: 'pro' })
2230
+ * ```
2231
+ */
2232
+ static async publishEvent(config, eventName, payload) {
2233
+ return callApi(config, "/triggers/events", {
2234
+ method: "POST",
2235
+ body: { eventName, payload: payload ?? {} }
2236
+ });
2237
+ }
2238
+ };
2239
+
2071
2240
  // src/lib/tasks/handler.ts
2072
2241
  import { createHmac, timingSafeEqual } from "crypto";
2073
2242
  var StepCompleteSignal = class {
@@ -2084,6 +2253,14 @@ var StepSleepSignal = class {
2084
2253
  }
2085
2254
  _isStepSleepSignal = true;
2086
2255
  };
2256
+ var StepWaitEventSignal = class {
2257
+ constructor(stepName, eventName, options = {}) {
2258
+ this.stepName = stepName;
2259
+ this.eventName = eventName;
2260
+ this.options = options;
2261
+ }
2262
+ _isStepWaitEventSignal = true;
2263
+ };
2087
2264
  function createStepContext(completedSteps, resolvedWaits) {
2088
2265
  return {
2089
2266
  /**
@@ -2110,6 +2287,32 @@ function createStepContext(completedSteps, resolvedWaits) {
2110
2287
  return;
2111
2288
  }
2112
2289
  throw new StepSleepSignal(name, duration);
2290
+ },
2291
+ /**
2292
+ * Pause execution until a named event is published via TriggersClient.publishEvent().
2293
+ *
2294
+ * - If event already arrived (platform re-dispatched with result): return event payload.
2295
+ * - If not yet arrived: throw StepWaitEventSignal to pause execution.
2296
+ *
2297
+ * @param name Step identifier (unique within handler).
2298
+ * @param eventName The event name to listen for (e.g. 'user.approved').
2299
+ * @param options Optional timeout ('24h', '7d') and payload filter.
2300
+ *
2301
+ * @example Human-in-the-loop approval
2302
+ * ```typescript
2303
+ * const approval = await step.waitForEvent('wait-approval', 'order.approved', {
2304
+ * timeout: '48h',
2305
+ * filter: { orderId: payload.orderId },
2306
+ * })
2307
+ * if (!approval) throw new Error('Approval timed out')
2308
+ * await sendConfirmation(approval.approvedBy)
2309
+ * ```
2310
+ */
2311
+ async waitForEvent(name, eventName, options = {}) {
2312
+ if (resolvedWaits.has(name)) {
2313
+ return resolvedWaits.get(name) ?? null;
2314
+ }
2315
+ throw new StepWaitEventSignal(name, eventName, options);
2113
2316
  }
2114
2317
  };
2115
2318
  }
@@ -2189,9 +2392,9 @@ function createTasksHandler(taskDefs, options = {}) {
2189
2392
  for (const step of context?.steps ?? []) {
2190
2393
  completedSteps.set(step.name, step.result);
2191
2394
  }
2192
- const resolvedWaits = /* @__PURE__ */ new Set();
2395
+ const resolvedWaits = /* @__PURE__ */ new Map();
2193
2396
  for (const wait of context?.waits ?? []) {
2194
- resolvedWaits.add(wait.name);
2397
+ resolvedWaits.set(wait.name, wait.result ?? void 0);
2195
2398
  }
2196
2399
  const stepCtx = createStepContext(completedSteps, resolvedWaits);
2197
2400
  try {
@@ -2214,6 +2417,16 @@ function createTasksHandler(taskDefs, options = {}) {
2214
2417
  duration: signal.duration
2215
2418
  });
2216
2419
  }
2420
+ if (err instanceof StepWaitEventSignal || err?._isStepWaitEventSignal) {
2421
+ const signal = err;
2422
+ return Response.json({
2423
+ status: "step_wait_event",
2424
+ stepName: signal.stepName,
2425
+ eventName: signal.eventName,
2426
+ timeout: signal.options.timeout ?? null,
2427
+ filter: signal.options.filter ?? null
2428
+ });
2429
+ }
2217
2430
  const message = err instanceof Error ? err.message : String(err);
2218
2431
  console.error(`[sylphx/tasks] Task "${taskName}" threw an error:`, err);
2219
2432
  return Response.json(
@@ -2685,6 +2898,86 @@ function canDeleteOrganization(membership) {
2685
2898
  return hasRole(membership, "super_admin");
2686
2899
  }
2687
2900
 
2901
+ // src/permissions.ts
2902
+ async function listPermissions(config) {
2903
+ return callApi(config, "/permissions");
2904
+ }
2905
+ async function createPermission(config, input) {
2906
+ return callApi(config, "/permissions", {
2907
+ method: "POST",
2908
+ body: input
2909
+ });
2910
+ }
2911
+ async function deletePermission(config, permissionKey) {
2912
+ return callApi(config, `/permissions/${permissionKey}`, {
2913
+ method: "DELETE"
2914
+ });
2915
+ }
2916
+ async function getMemberPermissions(config, orgIdOrSlug, memberId) {
2917
+ return callApi(
2918
+ config,
2919
+ `/orgs/${orgIdOrSlug}/members/${memberId}/permissions`
2920
+ );
2921
+ }
2922
+ function hasPermission(permissions, required) {
2923
+ return permissions.includes(required);
2924
+ }
2925
+ function hasAnyPermission(permissions, required) {
2926
+ return required.some((perm) => permissions.includes(perm));
2927
+ }
2928
+ function hasAllPermissions(permissions, required) {
2929
+ return required.every((perm) => permissions.includes(perm));
2930
+ }
2931
+
2932
+ // src/roles.ts
2933
+ async function listRoles(config) {
2934
+ return callApi(config, "/roles");
2935
+ }
2936
+ async function getRole(config, roleKey) {
2937
+ return callApi(config, `/roles/${roleKey}`);
2938
+ }
2939
+ async function createRole(config, input) {
2940
+ const body = {
2941
+ key: input.key,
2942
+ name: input.name
2943
+ };
2944
+ if (input.description !== void 0) body.description = input.description;
2945
+ if (input.permissions !== void 0) body.permissionKeys = input.permissions;
2946
+ if (input.isDefault !== void 0) body.isDefault = input.isDefault;
2947
+ if (input.sortOrder !== void 0) body.sortOrder = input.sortOrder;
2948
+ return callApi(config, "/roles", {
2949
+ method: "POST",
2950
+ body
2951
+ });
2952
+ }
2953
+ async function updateRole(config, roleKey, input) {
2954
+ const body = {};
2955
+ if (input.name !== void 0) body.name = input.name;
2956
+ if (input.description !== void 0) body.description = input.description;
2957
+ if (input.permissions !== void 0) body.permissionKeys = input.permissions;
2958
+ if (input.isDefault !== void 0) body.isDefault = input.isDefault;
2959
+ if (input.sortOrder !== void 0) body.sortOrder = input.sortOrder;
2960
+ return callApi(config, `/roles/${roleKey}`, {
2961
+ method: "PUT",
2962
+ body
2963
+ });
2964
+ }
2965
+ async function deleteRole(config, roleKey) {
2966
+ return callApi(config, `/roles/${roleKey}`, {
2967
+ method: "DELETE"
2968
+ });
2969
+ }
2970
+ async function assignMemberRole(config, orgIdOrSlug, memberId, roleKey) {
2971
+ return callApi(
2972
+ config,
2973
+ `/orgs/${orgIdOrSlug}/members/${memberId}/assign-role`,
2974
+ {
2975
+ method: "PUT",
2976
+ body: { roleKey }
2977
+ }
2978
+ );
2979
+ }
2980
+
2688
2981
  // src/secrets.ts
2689
2982
  async function getSecret(config, input) {
2690
2983
  return callApi(config, "/secrets/get", {
@@ -3143,6 +3436,143 @@ var SandboxFiles = class {
3143
3436
  return data.files;
3144
3437
  }
3145
3438
  };
3439
+ var SandboxProcesses = class {
3440
+ constructor(endpoint, token) {
3441
+ this.endpoint = endpoint;
3442
+ this.token = token;
3443
+ }
3444
+ authHeader() {
3445
+ return { Authorization: `Bearer ${this.token}` };
3446
+ }
3447
+ /** Spawn a new tracked process. Returns processId + pid immediately. */
3448
+ async start(opts) {
3449
+ const res = await fetch(`${this.endpoint}/process/start`, {
3450
+ method: "POST",
3451
+ headers: { ...this.authHeader(), "Content-Type": "application/json" },
3452
+ body: JSON.stringify(opts)
3453
+ });
3454
+ if (!res.ok) throw new Error(`process.start failed: ${await res.text()}`);
3455
+ return await res.json();
3456
+ }
3457
+ /** List all tracked processes. */
3458
+ async list() {
3459
+ const res = await fetch(`${this.endpoint}/process/list`, {
3460
+ headers: this.authHeader()
3461
+ });
3462
+ if (!res.ok) throw new Error(`process.list failed: ${await res.text()}`);
3463
+ return await res.json();
3464
+ }
3465
+ /** Get full process info including buffered output. */
3466
+ async get(processId) {
3467
+ const res = await fetch(`${this.endpoint}/process/${processId}`, {
3468
+ headers: this.authHeader()
3469
+ });
3470
+ if (!res.ok) throw new Error(`process.get failed: ${await res.text()}`);
3471
+ return await res.json();
3472
+ }
3473
+ /** Send a signal to a process. */
3474
+ async kill(processId, signal = "SIGTERM") {
3475
+ const res = await fetch(`${this.endpoint}/process/${processId}/kill`, {
3476
+ method: "POST",
3477
+ headers: { ...this.authHeader(), "Content-Type": "application/json" },
3478
+ body: JSON.stringify({ signal })
3479
+ });
3480
+ if (!res.ok) throw new Error(`process.kill failed: ${await res.text()}`);
3481
+ }
3482
+ /** Write to process stdin. */
3483
+ async writeStdin(processId, data) {
3484
+ const res = await fetch(`${this.endpoint}/process/${processId}/input`, {
3485
+ method: "POST",
3486
+ headers: { ...this.authHeader(), "Content-Type": "application/json" },
3487
+ body: JSON.stringify({ data })
3488
+ });
3489
+ if (!res.ok) throw new Error(`process.writeStdin failed: ${await res.text()}`);
3490
+ }
3491
+ /**
3492
+ * Wait for a process to complete and return its final info.
3493
+ * Polls every 500ms until status is no longer 'running'.
3494
+ *
3495
+ * For real-time output, use stream() instead.
3496
+ */
3497
+ async wait(processId, timeoutMs = 3e5) {
3498
+ const deadline = Date.now() + timeoutMs;
3499
+ while (Date.now() < deadline) {
3500
+ const info = await this.get(processId);
3501
+ if (info.status !== "running") return info;
3502
+ await new Promise((r) => setTimeout(r, 500));
3503
+ }
3504
+ throw new Error(`Timed out waiting for process ${processId} to complete (${timeoutMs}ms)`);
3505
+ }
3506
+ /** Stream process output as async iterable SSE events. */
3507
+ async *stream(processId) {
3508
+ const res = await fetch(
3509
+ `${this.endpoint}/process/${processId}/stream`,
3510
+ { headers: this.authHeader() }
3511
+ );
3512
+ if (!res.ok) throw new Error(`process.stream failed: ${await res.text()}`);
3513
+ if (!res.body) throw new Error("process.stream: no response body");
3514
+ const decoder = new TextDecoder();
3515
+ const reader = res.body.getReader();
3516
+ let buffer = "";
3517
+ try {
3518
+ while (true) {
3519
+ const { done, value } = await reader.read();
3520
+ if (done) break;
3521
+ buffer += decoder.decode(value, { stream: true });
3522
+ const lines = buffer.split("\n");
3523
+ buffer = lines.pop() ?? "";
3524
+ for (const line of lines) {
3525
+ if (line.startsWith("data: ")) {
3526
+ try {
3527
+ const event = JSON.parse(line.slice(6));
3528
+ yield event;
3529
+ if (event.type === "exit") return;
3530
+ } catch {
3531
+ }
3532
+ }
3533
+ }
3534
+ }
3535
+ } finally {
3536
+ reader.releaseLock();
3537
+ }
3538
+ }
3539
+ };
3540
+ var SandboxWatch = class {
3541
+ constructor(endpoint, token) {
3542
+ this.endpoint = endpoint;
3543
+ this.token = token;
3544
+ }
3545
+ authHeader() {
3546
+ return { Authorization: `Bearer ${this.token}` };
3547
+ }
3548
+ /** Start watching a path. Events delivered via sandbox.events() SSE stream. */
3549
+ async add(opts) {
3550
+ const res = await fetch(`${this.endpoint}/watch`, {
3551
+ method: "POST",
3552
+ headers: { ...this.authHeader(), "Content-Type": "application/json" },
3553
+ body: JSON.stringify(opts)
3554
+ });
3555
+ if (!res.ok) throw new Error(`watch.add failed: ${await res.text()}`);
3556
+ return await res.json();
3557
+ }
3558
+ /** List active watches. */
3559
+ async list() {
3560
+ const res = await fetch(`${this.endpoint}/watch`, {
3561
+ headers: this.authHeader()
3562
+ });
3563
+ if (!res.ok) throw new Error(`watch.list failed: ${await res.text()}`);
3564
+ const data = await res.json();
3565
+ return data.watches;
3566
+ }
3567
+ /** Stop watching a path. */
3568
+ async remove(path) {
3569
+ const res = await fetch(`${this.endpoint}/watch?path=${encodeURIComponent(path)}`, {
3570
+ method: "DELETE",
3571
+ headers: this.authHeader()
3572
+ });
3573
+ if (!res.ok) throw new Error(`watch.remove failed: ${await res.text()}`);
3574
+ }
3575
+ };
3146
3576
  var SandboxClient = class _SandboxClient {
3147
3577
  id;
3148
3578
  config;
@@ -3152,12 +3582,18 @@ var SandboxClient = class _SandboxClient {
3152
3582
  token;
3153
3583
  /** File operations (direct to exec-server) */
3154
3584
  files;
3585
+ /** Concurrent process management (direct to exec-server) */
3586
+ processes;
3587
+ /** Filesystem watch management (direct to exec-server) */
3588
+ watch;
3155
3589
  constructor(id, config, endpoint, token) {
3156
3590
  this.id = id;
3157
3591
  this.config = config;
3158
3592
  this.endpoint = endpoint;
3159
3593
  this.token = token;
3160
3594
  this.files = endpoint && token ? new SandboxFiles(endpoint, token) : null;
3595
+ this.processes = endpoint && token ? new SandboxProcesses(endpoint, token) : null;
3596
+ this.watch = endpoint && token ? new SandboxWatch(endpoint, token) : null;
3161
3597
  }
3162
3598
  // ---------------------------------------------------------------------------
3163
3599
  // Factory
@@ -3177,7 +3613,8 @@ var SandboxClient = class _SandboxClient {
3177
3613
  idleTimeoutMs: options?.idleTimeoutMs ?? 3e5,
3178
3614
  resources: options?.resources,
3179
3615
  env: options?.env,
3180
- storage: options?.storageGi !== void 0 ? { enabled: true, sizeGi: options.storageGi } : void 0
3616
+ storage: options?.storageGi !== void 0 ? { enabled: true, sizeGi: options.storageGi } : void 0,
3617
+ volumeMounts: options?.volumeMounts
3181
3618
  }
3182
3619
  });
3183
3620
  return new _SandboxClient(record.id, config, record.endpoint, record.token);
@@ -3205,10 +3642,21 @@ var SandboxClient = class _SandboxClient {
3205
3642
  // Exec — SSE streaming (primary)
3206
3643
  // ---------------------------------------------------------------------------
3207
3644
  /**
3208
- * Execute a command and stream output as async iterable events.
3645
+ * Execute a command and stream output as async iterable SSE events.
3646
+ *
3647
+ * **Stateless mode**: each exec() call runs in an isolated bash invocation.
3648
+ * Shell state (CWD changes, exported env vars, functions) is NOT preserved
3649
+ * between calls.
3209
3650
  *
3210
- * Uses Server-Sent Events (SSE) for real-time stdout/stderr streaming.
3211
- * 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
+ * ```
3212
3660
  *
3213
3661
  * @example
3214
3662
  * ```typescript
@@ -3220,13 +3668,13 @@ var SandboxClient = class _SandboxClient {
3220
3668
  */
3221
3669
  async *exec(command, options) {
3222
3670
  this.assertDirect();
3223
- const res = await fetch(`${this.endpoint}/exec/stream`, {
3671
+ const res = await fetch(`${this.endpoint}/exec`, {
3224
3672
  method: "POST",
3225
3673
  headers: {
3226
3674
  Authorization: `Bearer ${this.token}`,
3227
3675
  "Content-Type": "application/json"
3228
3676
  },
3229
- body: JSON.stringify({ command, ...options })
3677
+ body: JSON.stringify({ command, ...options, stateless: true, stream: true })
3230
3678
  });
3231
3679
  if (!res.ok) {
3232
3680
  throw new Error(`exec failed (${res.status}): ${await res.text()}`);
@@ -3279,6 +3727,58 @@ var SandboxClient = class _SandboxClient {
3279
3727
  return { stdout, stderr, exitCode, durationMs };
3280
3728
  }
3281
3729
  // ---------------------------------------------------------------------------
3730
+ // Events — Unified SSE stream
3731
+ // ---------------------------------------------------------------------------
3732
+ /**
3733
+ * Subscribe to the unified event stream (SSE).
3734
+ *
3735
+ * Receives all sandbox events: stdout, stderr, exit, port, file, shell, resource.
3736
+ * Filter by type/pid/shellId using query params.
3737
+ *
3738
+ * @example
3739
+ * ```typescript
3740
+ * for await (const event of sandbox.events({ type: 'file' })) {
3741
+ * console.log('File changed:', event.path, event.event)
3742
+ * }
3743
+ * ```
3744
+ */
3745
+ async *events(filter) {
3746
+ this.assertDirect();
3747
+ const params = new URLSearchParams();
3748
+ if (filter?.type) params.set("type", filter.type);
3749
+ if (filter?.pid !== void 0) params.set("pid", String(filter.pid));
3750
+ if (filter?.shellId) params.set("shellId", filter.shellId);
3751
+ const qs = params.toString();
3752
+ const url = `${this.endpoint}/events${qs ? `?${qs}` : ""}`;
3753
+ const res = await fetch(url, {
3754
+ headers: { Authorization: `Bearer ${this.token}` }
3755
+ });
3756
+ if (!res.ok) throw new Error(`events failed (${res.status}): ${await res.text()}`);
3757
+ if (!res.body) throw new Error("events: no response body");
3758
+ const decoder = new TextDecoder();
3759
+ const reader = res.body.getReader();
3760
+ let buffer = "";
3761
+ try {
3762
+ while (true) {
3763
+ const { done, value } = await reader.read();
3764
+ if (done) break;
3765
+ buffer += decoder.decode(value, { stream: true });
3766
+ const lines = buffer.split("\n");
3767
+ buffer = lines.pop() ?? "";
3768
+ for (const line of lines) {
3769
+ if (line.startsWith("data: ")) {
3770
+ try {
3771
+ yield JSON.parse(line.slice(6));
3772
+ } catch {
3773
+ }
3774
+ }
3775
+ }
3776
+ }
3777
+ } finally {
3778
+ reader.releaseLock();
3779
+ }
3780
+ }
3781
+ // ---------------------------------------------------------------------------
3282
3782
  // PTY — Interactive terminal (WebSocket)
3283
3783
  // ---------------------------------------------------------------------------
3284
3784
  /**
@@ -3324,7 +3824,7 @@ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
3324
3824
  ]);
3325
3825
  var DEFAULT_POLL_INTERVAL_MS = 3e3;
3326
3826
  var DEFAULT_WAIT_TIMEOUT_MS = 72e5;
3327
- var WorkerHandle = class {
3827
+ var RunHandle = class {
3328
3828
  id;
3329
3829
  config;
3330
3830
  constructor(id, config) {
@@ -3348,7 +3848,7 @@ var WorkerHandle = class {
3348
3848
  *
3349
3849
  * @param options.pollIntervalMs - How often to poll in ms (default: 3000)
3350
3850
  * @param options.timeoutMs - Max time to wait before throwing (default: 7_200_000 = 2h)
3351
- * @returns WorkerResult with exit code, status, stdout/stderr
3851
+ * @returns RunResult with exit code, status, stdout/stderr
3352
3852
  * @throws Error if waitTimeout is exceeded
3353
3853
  *
3354
3854
  * @example
@@ -3416,7 +3916,7 @@ var WorkerHandle = class {
3416
3916
  await callApi(this.config, `/workers/${this.id}`, { method: "DELETE" });
3417
3917
  }
3418
3918
  };
3419
- var WorkersClient = {
3919
+ var RunsClient = {
3420
3920
  // --------------------------------------------------------------------------
3421
3921
  // Run
3422
3922
  // --------------------------------------------------------------------------
@@ -3428,7 +3928,7 @@ var WorkersClient = {
3428
3928
  *
3429
3929
  * @example
3430
3930
  * ```typescript
3431
- * const worker = await WorkersClient.run(config, {
3931
+ * const run = await RunsClient.create(config, {
3432
3932
  * image: 'registry.sylphx.com/sylphx/trainer:abc123',
3433
3933
  * command: ['python', 'train.py', '--fold', '3'],
3434
3934
  * resources: { requests: { cpu: '4', memory: '16Gi' } },
@@ -3438,7 +3938,7 @@ var WorkersClient = {
3438
3938
  * ```
3439
3939
  */
3440
3940
  async run(config, options) {
3441
- const run = await callApi(config, "/workers", {
3941
+ const run = await callApi(config, "/runs", {
3442
3942
  method: "POST",
3443
3943
  body: {
3444
3944
  image: options.image,
@@ -3449,25 +3949,25 @@ var WorkersClient = {
3449
3949
  volumeMounts: options.volumeMounts
3450
3950
  }
3451
3951
  });
3452
- return new WorkerHandle(run.id, config);
3952
+ return new RunHandle(run.id, config);
3453
3953
  },
3454
3954
  // --------------------------------------------------------------------------
3455
3955
  // Get
3456
3956
  // --------------------------------------------------------------------------
3457
3957
  /**
3458
- * Get a WorkerHandle for an existing run by ID.
3958
+ * Get a RunHandle for an existing run by ID.
3459
3959
  *
3460
3960
  * Useful for resuming monitoring across requests.
3461
3961
  *
3462
3962
  * @example
3463
3963
  * ```typescript
3464
3964
  * // Store the worker ID, retrieve later
3465
- * const handle = WorkersClient.fromId(config, storedWorkerId)
3965
+ * const handle = RunsClient.fromId(config, storedWorkerId)
3466
3966
  * const result = await handle.wait()
3467
3967
  * ```
3468
3968
  */
3469
3969
  fromId(config, workerId) {
3470
- return new WorkerHandle(workerId, config);
3970
+ return new RunHandle(workerId, config);
3471
3971
  },
3472
3972
  // --------------------------------------------------------------------------
3473
3973
  // List
@@ -3477,12 +3977,12 @@ var WorkersClient = {
3477
3977
  *
3478
3978
  * @example
3479
3979
  * ```typescript
3480
- * const { workers } = await WorkersClient.list(config, { status: 'running' })
3980
+ * const { workers } = await RunsClient.list(config, { status: 'running' })
3481
3981
  * console.log(`${workers.length} workers currently running`)
3482
3982
  * ```
3483
3983
  */
3484
3984
  async list(config, options) {
3485
- return callApi(config, "/workers", {
3985
+ return callApi(config, "/runs", {
3486
3986
  method: "GET",
3487
3987
  query: options?.status ? { status: options.status } : void 0
3488
3988
  });
@@ -3493,11 +3993,11 @@ var WorkersClient = {
3493
3993
  /**
3494
3994
  * Spawn a worker and wait for it to complete in one call.
3495
3995
  *
3496
- * Equivalent to `(await WorkersClient.run(config, options)).wait(waitOptions)`.
3996
+ * Equivalent to `(await RunsClient.create(config, options)).wait(waitOptions)`.
3497
3997
  *
3498
3998
  * @example
3499
3999
  * ```typescript
3500
- * const result = await WorkersClient.runAndWait(config, {
4000
+ * const result = await RunsClient.runAndWait(config, {
3501
4001
  * image: 'registry.sylphx.com/sylphx/process:abc',
3502
4002
  * command: ['node', 'dist/process.js'],
3503
4003
  * })
@@ -3505,33 +4005,42 @@ var WorkersClient = {
3505
4005
  * ```
3506
4006
  */
3507
4007
  async runAndWait(config, options, waitOptions) {
3508
- const handle = await WorkersClient.run(config, options);
4008
+ const handle = await RunsClient.run(config, options);
3509
4009
  return handle.wait(waitOptions);
3510
4010
  }
3511
4011
  };
3512
4012
  function sleep2(ms) {
3513
4013
  return new Promise((resolve) => setTimeout(resolve, ms));
3514
4014
  }
4015
+ var WorkersClient = RunsClient;
3515
4016
  export {
3516
4017
  ACHIEVEMENT_TIER_CONFIG,
3517
4018
  AuthenticationError,
3518
4019
  AuthorizationError,
3519
4020
  CircuitBreakerOpenError,
3520
4021
  ERROR_CODE_STATUS,
4022
+ InvalidConnectionUrlError,
3521
4023
  NetworkError,
3522
4024
  NotFoundError,
3523
4025
  RETRYABLE_CODES,
3524
4026
  RateLimitError,
4027
+ RunHandle,
4028
+ RunsClient,
3525
4029
  SandboxClient,
4030
+ SandboxFiles,
4031
+ SandboxProcesses,
4032
+ SandboxWatch,
3526
4033
  StepCompleteSignal,
3527
4034
  StepSleepSignal,
3528
4035
  SylphxError,
3529
4036
  TimeoutError,
4037
+ TriggersClient,
3530
4038
  ValidationError,
3531
- WorkerHandle,
4039
+ RunHandle as WorkerHandle,
3532
4040
  WorkersClient,
3533
4041
  acceptAllConsents,
3534
4042
  acceptOrganizationInvitation,
4043
+ assignMemberRole,
3535
4044
  batchIndex,
3536
4045
  canDeleteOrganization,
3537
4046
  canManageMembers,
@@ -3546,12 +4055,16 @@ export {
3546
4055
  checkFlag,
3547
4056
  complete,
3548
4057
  createCheckout,
4058
+ createClient,
3549
4059
  createConfig,
3550
4060
  createCron,
3551
4061
  createDynamicRestClient,
3552
4062
  createOrganization,
4063
+ createPermission,
3553
4064
  createPortalSession,
3554
4065
  createRestClient,
4066
+ createRole,
4067
+ createServerClient,
3555
4068
  createServiceWorkerScript,
3556
4069
  createStepContext,
3557
4070
  createTasksHandler,
@@ -3566,6 +4079,8 @@ export {
3566
4079
  deleteEnvVar,
3567
4080
  deleteFile,
3568
4081
  deleteOrganization,
4082
+ deletePermission,
4083
+ deleteRole,
3569
4084
  deleteUser,
3570
4085
  disableDebug,
3571
4086
  embed,
@@ -3599,6 +4114,7 @@ export {
3599
4114
  getFlagPayload,
3600
4115
  getFlags,
3601
4116
  getLeaderboard,
4117
+ getMemberPermissions,
3602
4118
  getMyReferralCode,
3603
4119
  getOrganization,
3604
4120
  getOrganizationInvitations,
@@ -3610,6 +4126,7 @@ export {
3610
4126
  getReferralLeaderboard,
3611
4127
  getReferralStats,
3612
4128
  getRestErrorMessage,
4129
+ getRole,
3613
4130
  getScheduledEmail,
3614
4131
  getScheduledEmailStats,
3615
4132
  getSearchStats,
@@ -3629,8 +4146,11 @@ export {
3629
4146
  getWebhookDeliveries,
3630
4147
  getWebhookDelivery,
3631
4148
  getWebhookStats,
4149
+ hasAllPermissions,
4150
+ hasAnyPermission,
3632
4151
  hasConsent,
3633
4152
  hasError,
4153
+ hasPermission,
3634
4154
  hasRole,
3635
4155
  hasSecret,
3636
4156
  identify,
@@ -3667,12 +4187,13 @@ export {
3667
4187
  leaveOrganization,
3668
4188
  linkAnonymousConsents,
3669
4189
  listEnvVars,
4190
+ listPermissions,
4191
+ listRoles,
3670
4192
  listScheduledEmails,
3671
4193
  listSecretKeys,
3672
4194
  listTasks,
3673
4195
  listUsers,
3674
4196
  page,
3675
- parseKey,
3676
4197
  pauseCron,
3677
4198
  realtimeEmit,
3678
4199
  recordStreakActivity,
@@ -3719,6 +4240,7 @@ export {
3719
4240
  updateOrganization,
3720
4241
  updateOrganizationMemberRole,
3721
4242
  updatePushPreferences,
4243
+ updateRole,
3722
4244
  updateUser,
3723
4245
  updateUserMetadata,
3724
4246
  updateWebhookConfig,