@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.js CHANGED
@@ -35,20 +35,28 @@ __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,
59
+ assignMemberRole: () => assignMemberRole,
52
60
  batchIndex: () => batchIndex,
53
61
  canDeleteOrganization: () => canDeleteOrganization,
54
62
  canManageMembers: () => canManageMembers,
@@ -63,12 +71,16 @@ __export(index_exports, {
63
71
  checkFlag: () => checkFlag,
64
72
  complete: () => complete,
65
73
  createCheckout: () => createCheckout,
74
+ createClient: () => createClient,
66
75
  createConfig: () => createConfig,
67
76
  createCron: () => createCron,
68
77
  createDynamicRestClient: () => createDynamicRestClient,
69
78
  createOrganization: () => createOrganization,
79
+ createPermission: () => createPermission,
70
80
  createPortalSession: () => createPortalSession,
71
81
  createRestClient: () => createRestClient,
82
+ createRole: () => createRole,
83
+ createServerClient: () => createServerClient,
72
84
  createServiceWorkerScript: () => createServiceWorkerScript,
73
85
  createStepContext: () => createStepContext,
74
86
  createTasksHandler: () => createTasksHandler,
@@ -83,6 +95,8 @@ __export(index_exports, {
83
95
  deleteEnvVar: () => deleteEnvVar,
84
96
  deleteFile: () => deleteFile,
85
97
  deleteOrganization: () => deleteOrganization,
98
+ deletePermission: () => deletePermission,
99
+ deleteRole: () => deleteRole,
86
100
  deleteUser: () => deleteUser,
87
101
  disableDebug: () => disableDebug,
88
102
  embed: () => embed,
@@ -116,6 +130,7 @@ __export(index_exports, {
116
130
  getFlagPayload: () => getFlagPayload,
117
131
  getFlags: () => getFlags,
118
132
  getLeaderboard: () => getLeaderboard,
133
+ getMemberPermissions: () => getMemberPermissions,
119
134
  getMyReferralCode: () => getMyReferralCode,
120
135
  getOrganization: () => getOrganization,
121
136
  getOrganizationInvitations: () => getOrganizationInvitations,
@@ -127,6 +142,7 @@ __export(index_exports, {
127
142
  getReferralLeaderboard: () => getReferralLeaderboard,
128
143
  getReferralStats: () => getReferralStats,
129
144
  getRestErrorMessage: () => getRestErrorMessage,
145
+ getRole: () => getRole,
130
146
  getScheduledEmail: () => getScheduledEmail,
131
147
  getScheduledEmailStats: () => getScheduledEmailStats,
132
148
  getSearchStats: () => getSearchStats,
@@ -146,8 +162,11 @@ __export(index_exports, {
146
162
  getWebhookDeliveries: () => getWebhookDeliveries,
147
163
  getWebhookDelivery: () => getWebhookDelivery,
148
164
  getWebhookStats: () => getWebhookStats,
165
+ hasAllPermissions: () => hasAllPermissions,
166
+ hasAnyPermission: () => hasAnyPermission,
149
167
  hasConsent: () => hasConsent,
150
168
  hasError: () => hasError,
169
+ hasPermission: () => hasPermission,
151
170
  hasRole: () => hasRole,
152
171
  hasSecret: () => hasSecret,
153
172
  identify: () => identify,
@@ -184,12 +203,13 @@ __export(index_exports, {
184
203
  leaveOrganization: () => leaveOrganization,
185
204
  linkAnonymousConsents: () => linkAnonymousConsents,
186
205
  listEnvVars: () => listEnvVars,
206
+ listPermissions: () => listPermissions,
207
+ listRoles: () => listRoles,
187
208
  listScheduledEmails: () => listScheduledEmails,
188
209
  listSecretKeys: () => listSecretKeys,
189
210
  listTasks: () => listTasks,
190
211
  listUsers: () => listUsers,
191
212
  page: () => page,
192
- parseKey: () => parseKey,
193
213
  pauseCron: () => pauseCron,
194
214
  realtimeEmit: () => realtimeEmit,
195
215
  recordStreakActivity: () => recordStreakActivity,
@@ -236,6 +256,7 @@ __export(index_exports, {
236
256
  updateOrganization: () => updateOrganization,
237
257
  updateOrganizationMemberRole: () => updateOrganizationMemberRole,
238
258
  updatePushPreferences: () => updatePushPreferences,
259
+ updateRole: () => updateRole,
239
260
  updateUser: () => updateUser,
240
261
  updateUserMetadata: () => updateUserMetadata,
241
262
  updateWebhookConfig: () => updateWebhookConfig,
@@ -249,10 +270,104 @@ __export(index_exports, {
249
270
  });
250
271
  module.exports = __toCommonJS(index_exports);
251
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
+
252
367
  // src/constants.ts
253
368
  var SDK_API_PATH = `/v1`;
254
369
  var DEFAULT_SDK_API_HOST = "api.sylphx.com";
255
- var SDK_VERSION = "0.1.0";
370
+ var SDK_VERSION = "0.5.0";
256
371
  var SDK_PLATFORM = typeof window !== "undefined" ? "browser" : typeof process !== "undefined" && process.versions?.node ? "node" : "unknown";
257
372
  var DEFAULT_TIMEOUT_MS = 3e4;
258
373
  var SESSION_TOKEN_LIFETIME_SECONDS = 5 * 60;
@@ -354,6 +469,13 @@ var SylphxError = class _SylphxError extends Error {
354
469
  static isRateLimited(err) {
355
470
  return err instanceof _SylphxError && err.code === "TOO_MANY_REQUESTS";
356
471
  }
472
+ /**
473
+ * Check if error is an account lockout error (too many failed login attempts).
474
+ * When true, `error.data?.lockoutUntil` contains the ISO 8601 timestamp when the lockout expires.
475
+ */
476
+ static isAccountLocked(err) {
477
+ return err instanceof _SylphxError && err.code === "TOO_MANY_REQUESTS" && err.data?.code === "ACCOUNT_LOCKED";
478
+ }
357
479
  /**
358
480
  * Check if error is a quota exceeded error (plan limit reached)
359
481
  */
@@ -580,195 +702,153 @@ function exponentialBackoff(attempt, baseDelay = BASE_RETRY_DELAY_MS, maxDelay =
580
702
  return Math.round(cappedDelay + jitter);
581
703
  }
582
704
 
583
- // src/key-validation.ts
584
- var PUBLIC_KEY_PATTERN = /^pk_(dev|stg|prod)_[a-z0-9]{12}_[a-f0-9]{32}$/;
585
- var APP_ID_PATTERN = /^app_(dev|stg|prod)_[a-z0-9_-]+$/;
586
- var SECRET_KEY_PATTERN = /^sk_(dev|stg|prod)_[a-z0-9_-]+$/;
587
- var ENV_PREFIX_MAP = {
588
- dev: "development",
589
- stg: "staging",
590
- prod: "production"
591
- };
592
- function detectKeyIssues(key) {
593
- const issues = [];
594
- if (key !== key.trim()) issues.push("whitespace");
595
- if (key.includes("\n")) issues.push("newline");
596
- if (key.includes("\r")) issues.push("carriage-return");
597
- if (key.includes(" ")) issues.push("space");
598
- if (key !== key.toLowerCase()) issues.push("uppercase-chars");
599
- return issues;
600
- }
601
- function createSanitizationWarning(keyType, issues, envVarName) {
602
- const keyTypeName = keyType === "appId" ? "App ID" : "Secret Key";
603
- return `[Sylphx] ${keyTypeName} contains ${issues.join(", ")}. This is commonly caused by Vercel CLI's 'env pull' command.
604
-
605
- To fix permanently:
606
- 1. Go to Vercel Dashboard \u2192 Your Project \u2192 Settings \u2192 Environment Variables
607
- 2. Edit ${envVarName}
608
- 3. Remove any trailing whitespace or newline characters
609
- 4. Redeploy your application
610
-
611
- The SDK will automatically sanitize the key, but fixing the source is recommended.`;
612
- }
613
- function createInvalidKeyError(keyType, key, envVarName) {
614
- const prefix = keyType === "appId" ? "app" : "sk";
615
- const maskedKey = key.length > 20 ? `${key.slice(0, 20)}...` : key;
616
- const formatHint = `${prefix}_(dev|stg|prod)_[identifier]`;
617
- const keyTypeName = keyType === "appId" ? "App ID" : "Secret Key";
618
- return `[Sylphx] Invalid ${keyTypeName} format.
619
-
620
- Expected format: ${formatHint}
621
- Received: "${maskedKey}"
622
-
623
- Please check your ${envVarName} environment variable.
624
- You can find your keys in the Sylphx Console \u2192 API Keys.
625
-
626
- Common issues:
627
- \u2022 Key has uppercase characters (must be lowercase)
628
- \u2022 Key has wrong prefix (App ID: app_, Secret Key: sk_)
629
- \u2022 Key has invalid environment (must be dev, stg, or prod)
630
- \u2022 Key was copied with extra whitespace`;
631
- }
632
- function extractEnvironment(key) {
633
- const match = key.match(/^(?:app|sk)_(dev|stg|prod)_/);
634
- if (!match) return void 0;
635
- return ENV_PREFIX_MAP[match[1]];
636
- }
637
- function validateKeyForType(key, keyType, pattern, envVarName) {
638
- const keyTypeName = keyType === "appId" ? "App ID" : "Secret Key";
639
- if (!key) {
640
- return {
641
- valid: false,
642
- sanitizedKey: "",
643
- error: `[Sylphx] ${keyTypeName} is required. Set ${envVarName} in your environment variables.`,
644
- issues: ["missing"]
645
- };
646
- }
647
- const issues = detectKeyIssues(key);
648
- if (pattern.test(key)) {
649
- return {
650
- valid: true,
651
- sanitizedKey: key,
652
- keyType,
653
- environment: extractEnvironment(key),
654
- issues: []
655
- };
656
- }
657
- const sanitized = key.trim().toLowerCase();
658
- if (pattern.test(sanitized)) {
659
- return {
660
- valid: true,
661
- sanitizedKey: sanitized,
662
- keyType,
663
- environment: extractEnvironment(sanitized),
664
- warning: createSanitizationWarning(keyType, issues, envVarName),
665
- issues
666
- };
667
- }
668
- return {
669
- valid: false,
670
- sanitizedKey: "",
671
- error: createInvalidKeyError(keyType, key, envVarName),
672
- issues: [...issues, "invalid-format"]
673
- };
674
- }
675
- function validatePublicKey(key) {
676
- return validateKeyForType(key, "publicKey", PUBLIC_KEY_PATTERN, "NEXT_PUBLIC_SYLPHX_KEY");
677
- }
678
- function validateAppId(key) {
679
- return validateKeyForType(key, "appId", APP_ID_PATTERN, "NEXT_PUBLIC_SYLPHX_APP_ID");
680
- }
681
- function validateSecretKey(key) {
682
- return validateKeyForType(key, "secret", SECRET_KEY_PATTERN, "SYLPHX_SECRET_KEY");
683
- }
684
- function validateAndSanitizeSecretKey(key) {
685
- const result = validateSecretKey(key);
686
- if (!result.valid) {
687
- throw new Error(result.error);
688
- }
689
- if (result.warning) {
690
- console.warn(result.warning);
691
- }
692
- return result.sanitizedKey;
693
- }
694
- function detectKeyType(key) {
695
- const sanitized = key.trim().toLowerCase();
696
- if (sanitized.startsWith("pk_")) return "publicKey";
697
- if (sanitized.startsWith("app_")) return "appId";
698
- if (sanitized.startsWith("sk_")) return "secret";
699
- return null;
700
- }
701
- function validateKey(key) {
702
- const keyType = key ? detectKeyType(key) : null;
703
- if (keyType === "publicKey") {
704
- return validatePublicKey(key);
705
- }
706
- if (keyType === "appId") {
707
- 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" });
708
713
  }
709
- if (keyType === "secret") {
710
- return validateSecretKey(key);
714
+ if (LEGACY_EMBEDDED_REF_PATTERN.test(trimmed)) {
715
+ throw new SylphxError(`[Sylphx] ${MIGRATION_MESSAGE}`, { code: "BAD_REQUEST" });
711
716
  }
712
- return {
713
- valid: false,
714
- sanitizedKey: "",
715
- 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.",
716
- issues: key ? ["invalid_format"] : ["missing"]
717
- };
718
717
  }
719
-
720
- // src/config.ts
721
- function parseKey(key) {
722
- const sanitized = key.trim().toLowerCase();
723
- 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") {
724
741
  throw new SylphxError(
725
- "[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.",
726
743
  { code: "BAD_REQUEST" }
727
744
  );
728
745
  }
729
- const prefix = sanitized.startsWith("pk_") ? "pk" : sanitized.startsWith("sk_") ? "sk" : null;
730
- if (!prefix) {
746
+ return config;
747
+ }
748
+ function createConfigFromUrl(url) {
749
+ if (!url || typeof url !== "string") {
731
750
  throw new SylphxError(
732
- `[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",
733
752
  { code: "BAD_REQUEST" }
734
753
  );
735
754
  }
736
- const parts = sanitized.split("_");
737
- 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
+ }
738
764
  throw new SylphxError(
739
- `[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)}..."`,
740
766
  { code: "BAD_REQUEST" }
741
767
  );
742
768
  }
743
- const [, env, ref, token] = parts;
744
- if (env !== "prod" && env !== "dev" && env !== "stg" && env !== "prev") {
745
- throw new SylphxError(
746
- `[Sylphx] Invalid key environment "${env}". Must be 'prod', 'dev', 'stg', or 'prev'.`,
747
- { code: "BAD_REQUEST" }
748
- );
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;
749
777
  }
750
- if (!/^[a-z0-9]{12}$/.test(ref)) {
751
- throw new SylphxError(
752
- `[Sylphx] Invalid project ref in key: "${ref}". Must be a 12-character lowercase alphanumeric string.`,
753
- { code: "BAD_REQUEST" }
754
- );
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
+ });
755
792
  }
756
- const expectedTokenLen = prefix === "pk" ? 32 : 64;
757
- if (token.length !== expectedTokenLen || !/^[a-f0-9]+$/.test(token)) {
758
- throw new SylphxError(
759
- `[Sylphx] Invalid key token. ${prefix === "pk" ? "Publishable" : "Secret"} keys must have a ${expectedTokenLen}-char hex token.`,
760
- { code: "BAD_REQUEST" }
761
- );
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
+ });
762
798
  }
763
- return {
764
- type: prefix,
765
- env,
766
- ref,
767
- token,
768
- isPublic: prefix === "pk",
769
- baseUrl: `https://${ref}.${DEFAULT_SDK_API_HOST}${SDK_API_PATH}`
770
- };
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
+ });
771
850
  }
851
+ var createConfig = createClient;
772
852
  function httpStatusToErrorCode(status) {
773
853
  switch (status) {
774
854
  case 400:
@@ -801,79 +881,12 @@ function httpStatusToErrorCode(status) {
801
881
  return status >= 500 ? "INTERNAL_SERVER_ERROR" : "BAD_REQUEST";
802
882
  }
803
883
  }
804
- var REF_PATTERN = /^[a-z0-9]{12}$/;
805
- function createConfig(input) {
806
- const keyForParsing = input.secretKey || input.publicKey;
807
- if (!keyForParsing) {
808
- if (input.ref) {
809
- const trimmedRef = input.ref.trim();
810
- if (!REF_PATTERN.test(trimmedRef)) {
811
- throw new SylphxError(
812
- `[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.`,
813
- { code: "BAD_REQUEST" }
814
- );
815
- }
816
- const baseUrl2 = `https://${trimmedRef}.${DEFAULT_SDK_API_HOST}${SDK_API_PATH}`;
817
- console.warn(
818
- "[Sylphx] Providing only ref without a key is deprecated. Provide secretKey or publicKey \u2014 the ref is now embedded in keys (ADR-021)."
819
- );
820
- return Object.freeze({ ref: trimmedRef, baseUrl: baseUrl2, accessToken: input.accessToken });
821
- }
822
- throw new SylphxError(
823
- "[Sylphx] Either publicKey or secretKey must be provided to createConfig().",
824
- { code: "BAD_REQUEST" }
825
- );
826
- }
827
- const parsed = parseKey(keyForParsing);
828
- const ref = parsed.ref;
829
- const baseUrl = parsed.baseUrl;
830
- let secretKey;
831
- if (input.secretKey) {
832
- const result = validateKey(input.secretKey);
833
- if (!result.valid) {
834
- throw new SylphxError(result.error || "Invalid secret key", {
835
- code: "BAD_REQUEST",
836
- data: { issues: result.issues }
837
- });
838
- }
839
- if (result.warning) console.warn(`[Sylphx] ${result.warning}`);
840
- secretKey = result.sanitizedKey;
841
- }
842
- let publicKey;
843
- if (input.publicKey) {
844
- const result = validateKey(input.publicKey);
845
- if (!result.valid) {
846
- throw new SylphxError(result.error || "Invalid public key", {
847
- code: "BAD_REQUEST",
848
- data: { issues: result.issues }
849
- });
850
- }
851
- if (result.warning) console.warn(`[Sylphx] ${result.warning}`);
852
- publicKey = result.sanitizedKey;
853
- }
854
- return Object.freeze({
855
- secretKey,
856
- publicKey,
857
- ref,
858
- baseUrl,
859
- accessToken: input.accessToken
860
- });
861
- }
862
- function withToken(config, accessToken) {
863
- return Object.freeze({
864
- ...config,
865
- accessToken
866
- // Preserve baseUrl and ref from original config
867
- });
868
- }
869
884
  function buildHeaders(config) {
870
885
  const headers = {
871
886
  "Content-Type": "application/json"
872
887
  };
873
- if (config.secretKey) {
874
- headers["x-app-secret"] = config.secretKey;
875
- } else if (config.publicKey) {
876
- headers["x-app-secret"] = config.publicKey;
888
+ if (config.credential) {
889
+ headers["x-app-secret"] = config.credential;
877
890
  }
878
891
  if (config.accessToken) {
879
892
  headers.Authorization = `Bearer ${config.accessToken}`;
@@ -892,7 +905,8 @@ async function callApi(config, path, options = {}) {
892
905
  query,
893
906
  timeout = DEFAULT_TIMEOUT_MS,
894
907
  signal,
895
- idempotencyKey
908
+ idempotencyKey,
909
+ headers: extraHeaders
896
910
  } = options;
897
911
  let url = buildApiUrl(config, path);
898
912
  if (query) {
@@ -914,6 +928,11 @@ async function callApi(config, path, options = {}) {
914
928
  if (idempotencyKey) {
915
929
  headers["Idempotency-Key"] = idempotencyKey;
916
930
  }
931
+ if (extraHeaders) {
932
+ for (const [k, v] of Object.entries(extraHeaders)) {
933
+ headers[k] = v;
934
+ }
935
+ }
917
936
  const fetchOptions = {
918
937
  method,
919
938
  headers,
@@ -990,7 +1009,6 @@ async function callApi(config, path, options = {}) {
990
1009
  code: "PARSE_ERROR",
991
1010
  cause: error instanceof Error ? error : void 0,
992
1011
  data: { body: text.slice(0, 200) }
993
- // Include snippet for debugging
994
1012
  });
995
1013
  }
996
1014
  }
@@ -1100,6 +1118,111 @@ function installGlobalDebugHelpers() {
1100
1118
 
1101
1119
  // src/rest-client.ts
1102
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
1103
1226
  function createAuthMiddleware(config) {
1104
1227
  return {
1105
1228
  async onRequest({ request }) {
@@ -2319,6 +2442,73 @@ async function updatePushPreferences(config, preferences) {
2319
2442
  });
2320
2443
  }
2321
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
+
2322
2512
  // src/lib/tasks/handler.ts
2323
2513
  var import_node_crypto = require("crypto");
2324
2514
  var StepCompleteSignal = class {
@@ -2335,6 +2525,14 @@ var StepSleepSignal = class {
2335
2525
  }
2336
2526
  _isStepSleepSignal = true;
2337
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
+ };
2338
2536
  function createStepContext(completedSteps, resolvedWaits) {
2339
2537
  return {
2340
2538
  /**
@@ -2361,6 +2559,32 @@ function createStepContext(completedSteps, resolvedWaits) {
2361
2559
  return;
2362
2560
  }
2363
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);
2364
2588
  }
2365
2589
  };
2366
2590
  }
@@ -2440,9 +2664,9 @@ function createTasksHandler(taskDefs, options = {}) {
2440
2664
  for (const step of context?.steps ?? []) {
2441
2665
  completedSteps.set(step.name, step.result);
2442
2666
  }
2443
- const resolvedWaits = /* @__PURE__ */ new Set();
2667
+ const resolvedWaits = /* @__PURE__ */ new Map();
2444
2668
  for (const wait of context?.waits ?? []) {
2445
- resolvedWaits.add(wait.name);
2669
+ resolvedWaits.set(wait.name, wait.result ?? void 0);
2446
2670
  }
2447
2671
  const stepCtx = createStepContext(completedSteps, resolvedWaits);
2448
2672
  try {
@@ -2465,6 +2689,16 @@ function createTasksHandler(taskDefs, options = {}) {
2465
2689
  duration: signal.duration
2466
2690
  });
2467
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
+ }
2468
2702
  const message = err instanceof Error ? err.message : String(err);
2469
2703
  console.error(`[sylphx/tasks] Task "${taskName}" threw an error:`, err);
2470
2704
  return Response.json(
@@ -2936,6 +3170,86 @@ function canDeleteOrganization(membership) {
2936
3170
  return hasRole(membership, "super_admin");
2937
3171
  }
2938
3172
 
3173
+ // src/permissions.ts
3174
+ async function listPermissions(config) {
3175
+ return callApi(config, "/permissions");
3176
+ }
3177
+ async function createPermission(config, input) {
3178
+ return callApi(config, "/permissions", {
3179
+ method: "POST",
3180
+ body: input
3181
+ });
3182
+ }
3183
+ async function deletePermission(config, permissionKey) {
3184
+ return callApi(config, `/permissions/${permissionKey}`, {
3185
+ method: "DELETE"
3186
+ });
3187
+ }
3188
+ async function getMemberPermissions(config, orgIdOrSlug, memberId) {
3189
+ return callApi(
3190
+ config,
3191
+ `/orgs/${orgIdOrSlug}/members/${memberId}/permissions`
3192
+ );
3193
+ }
3194
+ function hasPermission(permissions, required) {
3195
+ return permissions.includes(required);
3196
+ }
3197
+ function hasAnyPermission(permissions, required) {
3198
+ return required.some((perm) => permissions.includes(perm));
3199
+ }
3200
+ function hasAllPermissions(permissions, required) {
3201
+ return required.every((perm) => permissions.includes(perm));
3202
+ }
3203
+
3204
+ // src/roles.ts
3205
+ async function listRoles(config) {
3206
+ return callApi(config, "/roles");
3207
+ }
3208
+ async function getRole(config, roleKey) {
3209
+ return callApi(config, `/roles/${roleKey}`);
3210
+ }
3211
+ async function createRole(config, input) {
3212
+ const body = {
3213
+ key: input.key,
3214
+ name: input.name
3215
+ };
3216
+ if (input.description !== void 0) body.description = input.description;
3217
+ if (input.permissions !== void 0) body.permissionKeys = input.permissions;
3218
+ if (input.isDefault !== void 0) body.isDefault = input.isDefault;
3219
+ if (input.sortOrder !== void 0) body.sortOrder = input.sortOrder;
3220
+ return callApi(config, "/roles", {
3221
+ method: "POST",
3222
+ body
3223
+ });
3224
+ }
3225
+ async function updateRole(config, roleKey, input) {
3226
+ const body = {};
3227
+ if (input.name !== void 0) body.name = input.name;
3228
+ if (input.description !== void 0) body.description = input.description;
3229
+ if (input.permissions !== void 0) body.permissionKeys = input.permissions;
3230
+ if (input.isDefault !== void 0) body.isDefault = input.isDefault;
3231
+ if (input.sortOrder !== void 0) body.sortOrder = input.sortOrder;
3232
+ return callApi(config, `/roles/${roleKey}`, {
3233
+ method: "PUT",
3234
+ body
3235
+ });
3236
+ }
3237
+ async function deleteRole(config, roleKey) {
3238
+ return callApi(config, `/roles/${roleKey}`, {
3239
+ method: "DELETE"
3240
+ });
3241
+ }
3242
+ async function assignMemberRole(config, orgIdOrSlug, memberId, roleKey) {
3243
+ return callApi(
3244
+ config,
3245
+ `/orgs/${orgIdOrSlug}/members/${memberId}/assign-role`,
3246
+ {
3247
+ method: "PUT",
3248
+ body: { roleKey }
3249
+ }
3250
+ );
3251
+ }
3252
+
2939
3253
  // src/secrets.ts
2940
3254
  async function getSecret(config, input) {
2941
3255
  return callApi(config, "/secrets/get", {
@@ -3394,6 +3708,143 @@ var SandboxFiles = class {
3394
3708
  return data.files;
3395
3709
  }
3396
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
+ };
3397
3848
  var SandboxClient = class _SandboxClient {
3398
3849
  id;
3399
3850
  config;
@@ -3403,12 +3854,18 @@ var SandboxClient = class _SandboxClient {
3403
3854
  token;
3404
3855
  /** File operations (direct to exec-server) */
3405
3856
  files;
3857
+ /** Concurrent process management (direct to exec-server) */
3858
+ processes;
3859
+ /** Filesystem watch management (direct to exec-server) */
3860
+ watch;
3406
3861
  constructor(id, config, endpoint, token) {
3407
3862
  this.id = id;
3408
3863
  this.config = config;
3409
3864
  this.endpoint = endpoint;
3410
3865
  this.token = token;
3411
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;
3412
3869
  }
3413
3870
  // ---------------------------------------------------------------------------
3414
3871
  // Factory
@@ -3428,7 +3885,8 @@ var SandboxClient = class _SandboxClient {
3428
3885
  idleTimeoutMs: options?.idleTimeoutMs ?? 3e5,
3429
3886
  resources: options?.resources,
3430
3887
  env: options?.env,
3431
- 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
3432
3890
  }
3433
3891
  });
3434
3892
  return new _SandboxClient(record.id, config, record.endpoint, record.token);
@@ -3456,10 +3914,21 @@ var SandboxClient = class _SandboxClient {
3456
3914
  // Exec — SSE streaming (primary)
3457
3915
  // ---------------------------------------------------------------------------
3458
3916
  /**
3459
- * 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.
3460
3922
  *
3461
- * Uses Server-Sent Events (SSE) for real-time stdout/stderr streaming.
3462
- * 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
+ * ```
3463
3932
  *
3464
3933
  * @example
3465
3934
  * ```typescript
@@ -3471,13 +3940,13 @@ var SandboxClient = class _SandboxClient {
3471
3940
  */
3472
3941
  async *exec(command, options) {
3473
3942
  this.assertDirect();
3474
- const res = await fetch(`${this.endpoint}/exec/stream`, {
3943
+ const res = await fetch(`${this.endpoint}/exec`, {
3475
3944
  method: "POST",
3476
3945
  headers: {
3477
3946
  Authorization: `Bearer ${this.token}`,
3478
3947
  "Content-Type": "application/json"
3479
3948
  },
3480
- body: JSON.stringify({ command, ...options })
3949
+ body: JSON.stringify({ command, ...options, stateless: true, stream: true })
3481
3950
  });
3482
3951
  if (!res.ok) {
3483
3952
  throw new Error(`exec failed (${res.status}): ${await res.text()}`);
@@ -3530,6 +3999,58 @@ var SandboxClient = class _SandboxClient {
3530
3999
  return { stdout, stderr, exitCode, durationMs };
3531
4000
  }
3532
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
+ // ---------------------------------------------------------------------------
3533
4054
  // PTY — Interactive terminal (WebSocket)
3534
4055
  // ---------------------------------------------------------------------------
3535
4056
  /**
@@ -3575,7 +4096,7 @@ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
3575
4096
  ]);
3576
4097
  var DEFAULT_POLL_INTERVAL_MS = 3e3;
3577
4098
  var DEFAULT_WAIT_TIMEOUT_MS = 72e5;
3578
- var WorkerHandle = class {
4099
+ var RunHandle = class {
3579
4100
  id;
3580
4101
  config;
3581
4102
  constructor(id, config) {
@@ -3599,7 +4120,7 @@ var WorkerHandle = class {
3599
4120
  *
3600
4121
  * @param options.pollIntervalMs - How often to poll in ms (default: 3000)
3601
4122
  * @param options.timeoutMs - Max time to wait before throwing (default: 7_200_000 = 2h)
3602
- * @returns WorkerResult with exit code, status, stdout/stderr
4123
+ * @returns RunResult with exit code, status, stdout/stderr
3603
4124
  * @throws Error if waitTimeout is exceeded
3604
4125
  *
3605
4126
  * @example
@@ -3667,7 +4188,7 @@ var WorkerHandle = class {
3667
4188
  await callApi(this.config, `/workers/${this.id}`, { method: "DELETE" });
3668
4189
  }
3669
4190
  };
3670
- var WorkersClient = {
4191
+ var RunsClient = {
3671
4192
  // --------------------------------------------------------------------------
3672
4193
  // Run
3673
4194
  // --------------------------------------------------------------------------
@@ -3679,7 +4200,7 @@ var WorkersClient = {
3679
4200
  *
3680
4201
  * @example
3681
4202
  * ```typescript
3682
- * const worker = await WorkersClient.run(config, {
4203
+ * const run = await RunsClient.create(config, {
3683
4204
  * image: 'registry.sylphx.com/sylphx/trainer:abc123',
3684
4205
  * command: ['python', 'train.py', '--fold', '3'],
3685
4206
  * resources: { requests: { cpu: '4', memory: '16Gi' } },
@@ -3689,7 +4210,7 @@ var WorkersClient = {
3689
4210
  * ```
3690
4211
  */
3691
4212
  async run(config, options) {
3692
- const run = await callApi(config, "/workers", {
4213
+ const run = await callApi(config, "/runs", {
3693
4214
  method: "POST",
3694
4215
  body: {
3695
4216
  image: options.image,
@@ -3700,25 +4221,25 @@ var WorkersClient = {
3700
4221
  volumeMounts: options.volumeMounts
3701
4222
  }
3702
4223
  });
3703
- return new WorkerHandle(run.id, config);
4224
+ return new RunHandle(run.id, config);
3704
4225
  },
3705
4226
  // --------------------------------------------------------------------------
3706
4227
  // Get
3707
4228
  // --------------------------------------------------------------------------
3708
4229
  /**
3709
- * Get a WorkerHandle for an existing run by ID.
4230
+ * Get a RunHandle for an existing run by ID.
3710
4231
  *
3711
4232
  * Useful for resuming monitoring across requests.
3712
4233
  *
3713
4234
  * @example
3714
4235
  * ```typescript
3715
4236
  * // Store the worker ID, retrieve later
3716
- * const handle = WorkersClient.fromId(config, storedWorkerId)
4237
+ * const handle = RunsClient.fromId(config, storedWorkerId)
3717
4238
  * const result = await handle.wait()
3718
4239
  * ```
3719
4240
  */
3720
4241
  fromId(config, workerId) {
3721
- return new WorkerHandle(workerId, config);
4242
+ return new RunHandle(workerId, config);
3722
4243
  },
3723
4244
  // --------------------------------------------------------------------------
3724
4245
  // List
@@ -3728,12 +4249,12 @@ var WorkersClient = {
3728
4249
  *
3729
4250
  * @example
3730
4251
  * ```typescript
3731
- * const { workers } = await WorkersClient.list(config, { status: 'running' })
4252
+ * const { workers } = await RunsClient.list(config, { status: 'running' })
3732
4253
  * console.log(`${workers.length} workers currently running`)
3733
4254
  * ```
3734
4255
  */
3735
4256
  async list(config, options) {
3736
- return callApi(config, "/workers", {
4257
+ return callApi(config, "/runs", {
3737
4258
  method: "GET",
3738
4259
  query: options?.status ? { status: options.status } : void 0
3739
4260
  });
@@ -3744,11 +4265,11 @@ var WorkersClient = {
3744
4265
  /**
3745
4266
  * Spawn a worker and wait for it to complete in one call.
3746
4267
  *
3747
- * Equivalent to `(await WorkersClient.run(config, options)).wait(waitOptions)`.
4268
+ * Equivalent to `(await RunsClient.create(config, options)).wait(waitOptions)`.
3748
4269
  *
3749
4270
  * @example
3750
4271
  * ```typescript
3751
- * const result = await WorkersClient.runAndWait(config, {
4272
+ * const result = await RunsClient.runAndWait(config, {
3752
4273
  * image: 'registry.sylphx.com/sylphx/process:abc',
3753
4274
  * command: ['node', 'dist/process.js'],
3754
4275
  * })
@@ -3756,13 +4277,14 @@ var WorkersClient = {
3756
4277
  * ```
3757
4278
  */
3758
4279
  async runAndWait(config, options, waitOptions) {
3759
- const handle = await WorkersClient.run(config, options);
4280
+ const handle = await RunsClient.run(config, options);
3760
4281
  return handle.wait(waitOptions);
3761
4282
  }
3762
4283
  };
3763
4284
  function sleep2(ms) {
3764
4285
  return new Promise((resolve) => setTimeout(resolve, ms));
3765
4286
  }
4287
+ var WorkersClient = RunsClient;
3766
4288
  // Annotate the CommonJS export names for ESM import in node:
3767
4289
  0 && (module.exports = {
3768
4290
  ACHIEVEMENT_TIER_CONFIG,
@@ -3770,20 +4292,28 @@ function sleep2(ms) {
3770
4292
  AuthorizationError,
3771
4293
  CircuitBreakerOpenError,
3772
4294
  ERROR_CODE_STATUS,
4295
+ InvalidConnectionUrlError,
3773
4296
  NetworkError,
3774
4297
  NotFoundError,
3775
4298
  RETRYABLE_CODES,
3776
4299
  RateLimitError,
4300
+ RunHandle,
4301
+ RunsClient,
3777
4302
  SandboxClient,
4303
+ SandboxFiles,
4304
+ SandboxProcesses,
4305
+ SandboxWatch,
3778
4306
  StepCompleteSignal,
3779
4307
  StepSleepSignal,
3780
4308
  SylphxError,
3781
4309
  TimeoutError,
4310
+ TriggersClient,
3782
4311
  ValidationError,
3783
4312
  WorkerHandle,
3784
4313
  WorkersClient,
3785
4314
  acceptAllConsents,
3786
4315
  acceptOrganizationInvitation,
4316
+ assignMemberRole,
3787
4317
  batchIndex,
3788
4318
  canDeleteOrganization,
3789
4319
  canManageMembers,
@@ -3798,12 +4328,16 @@ function sleep2(ms) {
3798
4328
  checkFlag,
3799
4329
  complete,
3800
4330
  createCheckout,
4331
+ createClient,
3801
4332
  createConfig,
3802
4333
  createCron,
3803
4334
  createDynamicRestClient,
3804
4335
  createOrganization,
4336
+ createPermission,
3805
4337
  createPortalSession,
3806
4338
  createRestClient,
4339
+ createRole,
4340
+ createServerClient,
3807
4341
  createServiceWorkerScript,
3808
4342
  createStepContext,
3809
4343
  createTasksHandler,
@@ -3818,6 +4352,8 @@ function sleep2(ms) {
3818
4352
  deleteEnvVar,
3819
4353
  deleteFile,
3820
4354
  deleteOrganization,
4355
+ deletePermission,
4356
+ deleteRole,
3821
4357
  deleteUser,
3822
4358
  disableDebug,
3823
4359
  embed,
@@ -3851,6 +4387,7 @@ function sleep2(ms) {
3851
4387
  getFlagPayload,
3852
4388
  getFlags,
3853
4389
  getLeaderboard,
4390
+ getMemberPermissions,
3854
4391
  getMyReferralCode,
3855
4392
  getOrganization,
3856
4393
  getOrganizationInvitations,
@@ -3862,6 +4399,7 @@ function sleep2(ms) {
3862
4399
  getReferralLeaderboard,
3863
4400
  getReferralStats,
3864
4401
  getRestErrorMessage,
4402
+ getRole,
3865
4403
  getScheduledEmail,
3866
4404
  getScheduledEmailStats,
3867
4405
  getSearchStats,
@@ -3881,8 +4419,11 @@ function sleep2(ms) {
3881
4419
  getWebhookDeliveries,
3882
4420
  getWebhookDelivery,
3883
4421
  getWebhookStats,
4422
+ hasAllPermissions,
4423
+ hasAnyPermission,
3884
4424
  hasConsent,
3885
4425
  hasError,
4426
+ hasPermission,
3886
4427
  hasRole,
3887
4428
  hasSecret,
3888
4429
  identify,
@@ -3919,12 +4460,13 @@ function sleep2(ms) {
3919
4460
  leaveOrganization,
3920
4461
  linkAnonymousConsents,
3921
4462
  listEnvVars,
4463
+ listPermissions,
4464
+ listRoles,
3922
4465
  listScheduledEmails,
3923
4466
  listSecretKeys,
3924
4467
  listTasks,
3925
4468
  listUsers,
3926
4469
  page,
3927
- parseKey,
3928
4470
  pauseCron,
3929
4471
  realtimeEmit,
3930
4472
  recordStreakActivity,
@@ -3971,6 +4513,7 @@ function sleep2(ms) {
3971
4513
  updateOrganization,
3972
4514
  updateOrganizationMemberRole,
3973
4515
  updatePushPreferences,
4516
+ updateRole,
3974
4517
  updateUser,
3975
4518
  updateUserMetadata,
3976
4519
  updateWebhookConfig,