@sylphx/sdk 0.3.2 → 0.3.3

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,5 @@
1
1
  // src/constants.ts
2
- var DEFAULT_PLATFORM_URL = "https://sylphx.com";
3
- var SDK_API_PATH = `/api/v1`;
4
- var SDK_API_PATH_NEW = `/v1`;
2
+ var SDK_API_PATH = `/v1`;
5
3
  var DEFAULT_SDK_API_HOST = "api.sylphx.com";
6
4
  var SDK_VERSION = "0.1.0";
7
5
  var SDK_PLATFORM = typeof window !== "undefined" ? "browser" : typeof process !== "undefined" && process.versions?.node ? "node" : "unknown";
@@ -509,32 +507,18 @@ function createConfig(input) {
509
507
  }
510
508
  secretKey = result.sanitizedKey;
511
509
  }
512
- if (input.ref !== void 0) {
513
- const trimmedRef = input.ref.trim();
514
- if (!REF_PATTERN.test(trimmedRef)) {
515
- throw new SylphxError(
516
- `[Sylphx] Invalid project ref format: "${input.ref}". Expected a 16-character lowercase alphanumeric string (e.g. "abc123def456ghij"). Get your ref from Platform Console \u2192 Projects \u2192 Your Project \u2192 Overview.`,
517
- { code: "BAD_REQUEST" }
518
- );
519
- }
520
- }
521
- let platformUrl;
522
- let apiBasePath;
523
- if (input.platformUrl) {
524
- platformUrl = input.platformUrl.trim();
525
- apiBasePath = SDK_API_PATH;
526
- } else if (input.ref) {
527
- platformUrl = `https://${input.ref.trim()}.${DEFAULT_SDK_API_HOST}`;
528
- apiBasePath = SDK_API_PATH_NEW;
529
- } else {
530
- platformUrl = DEFAULT_PLATFORM_URL;
531
- apiBasePath = SDK_API_PATH;
510
+ const trimmedRef = input.ref.trim();
511
+ if (!REF_PATTERN.test(trimmedRef)) {
512
+ throw new SylphxError(
513
+ `[Sylphx] Invalid project ref format: "${input.ref}". Expected a 16-character lowercase alphanumeric string (e.g. "abc123def456ghij"). Get your ref from Platform Console \u2192 Projects \u2192 Your Project \u2192 Overview.`,
514
+ { code: "BAD_REQUEST" }
515
+ );
532
516
  }
517
+ const baseUrl = `https://${trimmedRef}.${DEFAULT_SDK_API_HOST}${SDK_API_PATH}`;
533
518
  return Object.freeze({
534
519
  secretKey,
535
- platformUrl,
536
- ref: input.ref,
537
- apiBasePath,
520
+ ref: trimmedRef,
521
+ baseUrl,
538
522
  accessToken: input.accessToken
539
523
  });
540
524
  }
@@ -542,7 +526,7 @@ function withToken(config, accessToken) {
542
526
  return Object.freeze({
543
527
  ...config,
544
528
  accessToken
545
- // Preserve apiBasePath and ref from original config
529
+ // Preserve baseUrl and ref from original config
546
530
  });
547
531
  }
548
532
  function buildHeaders(config) {
@@ -558,9 +542,9 @@ function buildHeaders(config) {
558
542
  return headers;
559
543
  }
560
544
  function buildApiUrl(config, path) {
561
- const base = config.platformUrl.replace(/\/$/, "");
545
+ const base = config.baseUrl.replace(/\/$/, "");
562
546
  const cleanPath = path.startsWith("/") ? path : `/${path}`;
563
- return `${base}${config.apiBasePath ?? SDK_API_PATH}${cleanPath}`;
547
+ return `${base}${cleanPath}`;
564
548
  }
565
549
  async function callApi(config, path, options = {}) {
566
550
  const {
@@ -1114,7 +1098,7 @@ function createRetryMiddleware(retryConfig) {
1114
1098
  function validateClientConfig(config) {
1115
1099
  return {
1116
1100
  secretKey: validateAndSanitizeSecretKey(config.secretKey),
1117
- baseUrl: (config.platformUrl || DEFAULT_PLATFORM_URL).trim()
1101
+ baseUrl: (config.platformUrl || `https://${DEFAULT_SDK_API_HOST}`).trim()
1118
1102
  };
1119
1103
  }
1120
1104
  function createRestClient(config) {
@@ -1240,7 +1224,7 @@ async function verifyTwoFactor(config, userId, code) {
1240
1224
  });
1241
1225
  }
1242
1226
  async function introspectToken(config, token, tokenTypeHint) {
1243
- const response = await fetch(`${config.platformUrl}/api/v1/auth/introspect`, {
1227
+ const response = await fetch(buildApiUrl(config, "/auth/introspect"), {
1244
1228
  method: "POST",
1245
1229
  headers: { "Content-Type": "application/json" },
1246
1230
  body: JSON.stringify({
@@ -1255,7 +1239,7 @@ async function introspectToken(config, token, tokenTypeHint) {
1255
1239
  return response.json();
1256
1240
  }
1257
1241
  async function revokeToken(config, token, options) {
1258
- await fetch(`${config.platformUrl}/api/v1/auth/revoke`, {
1242
+ await fetch(buildApiUrl(config, "/auth/revoke"), {
1259
1243
  method: "POST",
1260
1244
  headers: { "Content-Type": "application/json" },
1261
1245
  body: JSON.stringify({
@@ -1406,7 +1390,7 @@ function createTracker(config, defaultAnonymousId) {
1406
1390
 
1407
1391
  // src/ai.ts
1408
1392
  async function chat(config, input) {
1409
- const response = await fetch(`${config.platformUrl}/api/v1/chat/completions`, {
1393
+ const response = await fetch(buildApiUrl(config, "/chat/completions"), {
1410
1394
  method: "POST",
1411
1395
  headers: {
1412
1396
  ...buildHeaders(config),
@@ -1454,7 +1438,7 @@ async function chat(config, input) {
1454
1438
  function chatStream(config, input) {
1455
1439
  return {
1456
1440
  [Symbol.asyncIterator]: async function* () {
1457
- const response = await fetch(`${config.platformUrl}/api/v1/chat/completions`, {
1441
+ const response = await fetch(buildApiUrl(config, "/chat/completions"), {
1458
1442
  method: "POST",
1459
1443
  headers: {
1460
1444
  ...buildHeaders(config),
@@ -1526,7 +1510,7 @@ function chatStream(config, input) {
1526
1510
  };
1527
1511
  }
1528
1512
  async function embed(config, input) {
1529
- const response = await fetch(`${config.platformUrl}/api/v1/embeddings`, {
1513
+ const response = await fetch(buildApiUrl(config, "/embeddings"), {
1530
1514
  method: "POST",
1531
1515
  headers: {
1532
1516
  ...buildHeaders(config),
@@ -1656,7 +1640,7 @@ async function uploadFile(config, file, options) {
1656
1640
  let lastError = null;
1657
1641
  for (let attempt = 0; attempt <= UPLOAD_RETRY_CONFIG.maxRetries; attempt++) {
1658
1642
  try {
1659
- tokenResponse = await fetch(`${config.platformUrl}/api/v1/storage/upload`, {
1643
+ tokenResponse = await fetch(buildApiUrl(config, "/storage/upload"), {
1660
1644
  method: "POST",
1661
1645
  headers: buildHeaders(config),
1662
1646
  body: JSON.stringify({
@@ -2384,7 +2368,7 @@ async function getConsentHistory(config, input) {
2384
2368
  userId: input.userId,
2385
2369
  anonymousId: input.anonymousId,
2386
2370
  limit: input.limit?.toString(),
2387
- offset: input.offset?.toString()
2371
+ cursor: input.cursor
2388
2372
  }
2389
2373
  });
2390
2374
  }
@@ -2957,27 +2941,6 @@ async function deleteEnvVar(config, envId, key) {
2957
2941
  { method: "DELETE" }
2958
2942
  );
2959
2943
  }
2960
- async function listCustomDomains(config, envId) {
2961
- const result = await callApi(
2962
- config,
2963
- `/sdk/deploy/domains/${encodeURIComponent(envId)}`,
2964
- { method: "GET" }
2965
- );
2966
- return result.domains;
2967
- }
2968
- async function addCustomDomain(config, envId, request) {
2969
- return callApi(config, `/sdk/deploy/domains/${encodeURIComponent(envId)}`, {
2970
- method: "POST",
2971
- body: request
2972
- });
2973
- }
2974
- async function removeCustomDomain(config, envId, domain) {
2975
- return callApi(
2976
- config,
2977
- `/sdk/deploy/domains/${encodeURIComponent(envId)}/${encodeURIComponent(domain)}`,
2978
- { method: "DELETE" }
2979
- );
2980
- }
2981
2944
 
2982
2945
  // src/monitoring.ts
2983
2946
  function errorToExceptionValue(error) {
@@ -3037,13 +3000,67 @@ async function captureMessage(config, message, options = {}) {
3037
3000
  }
3038
3001
 
3039
3002
  // src/sandbox.ts
3040
- var DEFAULT_IMAGE = "registry.sylphx.com/library/exec-server:latest";
3003
+ var SandboxFiles = class {
3004
+ constructor(endpoint, token) {
3005
+ this.endpoint = endpoint;
3006
+ this.token = token;
3007
+ }
3008
+ authHeader() {
3009
+ return { Authorization: `Bearer ${this.token}` };
3010
+ }
3011
+ /** Write a file to the sandbox filesystem. */
3012
+ async write(path, content, encoding = "utf8") {
3013
+ const contentStr = Buffer.isBuffer(content) ? content.toString("base64") : content;
3014
+ const effectiveEncoding = Buffer.isBuffer(content) ? "base64" : encoding;
3015
+ const res = await fetch(`${this.endpoint}/files`, {
3016
+ method: "POST",
3017
+ headers: { ...this.authHeader(), "Content-Type": "application/json" },
3018
+ body: JSON.stringify({ path, content: contentStr, encoding: effectiveEncoding })
3019
+ });
3020
+ if (!res.ok) throw new Error(`files.write failed: ${await res.text()}`);
3021
+ }
3022
+ /** Read a file from the sandbox filesystem. Returns content as string. */
3023
+ async read(path) {
3024
+ const res = await fetch(`${this.endpoint}/files?path=${encodeURIComponent(path)}`, {
3025
+ headers: this.authHeader()
3026
+ });
3027
+ if (!res.ok) throw new Error(`files.read failed: ${await res.text()}`);
3028
+ const data = await res.json();
3029
+ return data.content;
3030
+ }
3031
+ /** Delete a file from the sandbox filesystem. */
3032
+ async delete(path) {
3033
+ const res = await fetch(`${this.endpoint}/files?path=${encodeURIComponent(path)}`, {
3034
+ method: "DELETE",
3035
+ headers: this.authHeader()
3036
+ });
3037
+ if (!res.ok) throw new Error(`files.delete failed: ${await res.text()}`);
3038
+ }
3039
+ /** List files in a directory. */
3040
+ async list(path = "/") {
3041
+ const res = await fetch(`${this.endpoint}/list?path=${encodeURIComponent(path)}`, {
3042
+ headers: this.authHeader()
3043
+ });
3044
+ if (!res.ok) throw new Error(`files.list failed: ${await res.text()}`);
3045
+ const data = await res.json();
3046
+ return data.files;
3047
+ }
3048
+ };
3041
3049
  var SandboxClient = class _SandboxClient {
3042
3050
  id;
3043
3051
  config;
3044
- constructor(id, config) {
3052
+ /** Public endpoint from Platform (may be null for sandboxes from pool pre-v2) */
3053
+ endpoint;
3054
+ /** Per-sandbox JWT for direct exec-server auth */
3055
+ token;
3056
+ /** File operations (direct to exec-server) */
3057
+ files;
3058
+ constructor(id, config, endpoint, token) {
3045
3059
  this.id = id;
3046
3060
  this.config = config;
3061
+ this.endpoint = endpoint;
3062
+ this.token = token;
3063
+ this.files = endpoint && token ? new SandboxFiles(endpoint, token) : null;
3047
3064
  }
3048
3065
  // ---------------------------------------------------------------------------
3049
3066
  // Factory
@@ -3051,130 +3068,153 @@ var SandboxClient = class _SandboxClient {
3051
3068
  /**
3052
3069
  * Create a new sandbox.
3053
3070
  *
3054
- * The sandbox pod starts asynchronously it will be in `starting` status
3055
- * after creation. File/run operations will block until the pod is ready.
3056
- *
3057
- * @param config - Sylphx config with a secret key (sk_*) and project ref
3058
- * @param options - Sandbox creation options
3071
+ * Platform provisions the K8s pod, waits for readiness, and returns
3072
+ * { endpoint, token } once the sandbox is fully ready to accept traffic.
3073
+ * No client-side polling required.
3059
3074
  */
3060
3075
  static async create(config, options) {
3061
- const record = await callApi(config, `/sandboxes`, {
3076
+ const record = await callApi(config, "/sandboxes", {
3062
3077
  method: "POST",
3063
3078
  body: {
3064
- image: options?.image ?? DEFAULT_IMAGE,
3079
+ image: options?.image,
3065
3080
  idleTimeoutMs: options?.idleTimeoutMs ?? 3e5,
3066
3081
  resources: options?.resources,
3067
3082
  env: options?.env,
3068
3083
  storage: options?.storageGi !== void 0 ? { enabled: true, sizeGi: options.storageGi } : void 0
3069
3084
  }
3070
3085
  });
3071
- return new _SandboxClient(record.id, config);
3086
+ return new _SandboxClient(record.id, config, record.endpoint, record.token);
3072
3087
  }
3073
3088
  /**
3074
3089
  * Reconnect to an existing sandbox by ID.
3075
- *
3076
- * Use this to resume operations on a sandbox created in a previous request.
3090
+ * Fetches the current status to get the endpoint + token.
3077
3091
  */
3078
- static fromId(config, sandboxId) {
3079
- return new _SandboxClient(sandboxId, config);
3092
+ static async fromId(config, sandboxId) {
3093
+ const record = await callApi(config, `/sandboxes/${sandboxId}`, {
3094
+ method: "GET"
3095
+ });
3096
+ return new _SandboxClient(record.id, config, record.endpoint, record.token);
3080
3097
  }
3081
3098
  // ---------------------------------------------------------------------------
3082
3099
  // Lifecycle
3083
3100
  // ---------------------------------------------------------------------------
3084
- /**
3085
- * Get the current status of this sandbox.
3086
- */
3087
3101
  async getStatus() {
3088
3102
  return callApi(this.config, `/sandboxes/${this.id}`, { method: "GET" });
3089
3103
  }
3090
- /**
3091
- * Terminate the sandbox immediately.
3092
- *
3093
- * Deletes the K8s Pod and Service. PVC (storage) is preserved for reuse.
3094
- * This operation is idempotent — safe to call multiple times.
3095
- */
3096
3104
  async terminate() {
3097
3105
  await callApi(this.config, `/sandboxes/${this.id}`, { method: "DELETE" });
3098
3106
  }
3099
3107
  // ---------------------------------------------------------------------------
3100
- // File Operations
3108
+ // Exec — SSE streaming (primary)
3101
3109
  // ---------------------------------------------------------------------------
3102
3110
  /**
3103
- * Write a file to the sandbox filesystem.
3111
+ * Execute a command and stream output as async iterable events.
3104
3112
  *
3105
- * @param path - Absolute path inside the sandbox (e.g. '/workspace/file.py')
3106
- * @param content - File content as string or Buffer
3107
- * @param encoding - 'utf8' (default) or 'base64' for binary files
3108
- */
3109
- async writeFile(path, content, encoding = "utf8") {
3110
- const contentStr = Buffer.isBuffer(content) ? content.toString("base64") : content;
3111
- const effectiveEncoding = Buffer.isBuffer(content) ? "base64" : encoding;
3112
- await callApi(this.config, `/sandboxes/${this.id}/files`, {
3113
- method: "POST",
3114
- body: { path, content: contentStr, encoding: effectiveEncoding }
3115
- });
3116
- }
3117
- /**
3118
- * Read a file from the sandbox filesystem.
3113
+ * Uses Server-Sent Events (SSE) for real-time stdout/stderr streaming.
3114
+ * Communicates DIRECTLY with exec-server (Platform not in data path).
3119
3115
  *
3120
- * @param path - Absolute path inside the sandbox
3121
- * @returns File content as a string
3122
- */
3123
- async readFile(path) {
3124
- const result = await callApi(this.config, `/sandboxes/${this.id}/files`, {
3125
- method: "GET",
3126
- query: { path }
3127
- });
3128
- return result.content;
3129
- }
3130
- /**
3131
- * Delete a file from the sandbox filesystem.
3132
- *
3133
- * @param path - Absolute path inside the sandbox
3116
+ * @example
3117
+ * ```typescript
3118
+ * for await (const event of sandbox.exec(['npm', 'install'])) {
3119
+ * if (event.type === 'stdout') process.stdout.write(event.data)
3120
+ * if (event.type === 'exit') console.log('Done:', event.exitCode)
3121
+ * }
3122
+ * ```
3134
3123
  */
3135
- async deleteFile(path) {
3136
- await callApi(this.config, `/sandboxes/${this.id}/files`, {
3137
- method: "DELETE",
3138
- query: { path }
3124
+ async *exec(command, options) {
3125
+ this.assertDirect();
3126
+ const res = await fetch(`${this.endpoint}/exec/stream`, {
3127
+ method: "POST",
3128
+ headers: {
3129
+ Authorization: `Bearer ${this.token}`,
3130
+ "Content-Type": "application/json"
3131
+ },
3132
+ body: JSON.stringify({ command, ...options })
3139
3133
  });
3134
+ if (!res.ok) {
3135
+ throw new Error(`exec failed (${res.status}): ${await res.text()}`);
3136
+ }
3137
+ if (!res.body) throw new Error("exec: no response body");
3138
+ const decoder = new TextDecoder();
3139
+ const reader = res.body.getReader();
3140
+ let buffer = "";
3141
+ try {
3142
+ while (true) {
3143
+ const { done, value } = await reader.read();
3144
+ if (done) break;
3145
+ buffer += decoder.decode(value, { stream: true });
3146
+ const lines = buffer.split("\n");
3147
+ buffer = lines.pop() ?? "";
3148
+ for (const line of lines) {
3149
+ if (line.startsWith("data: ")) {
3150
+ try {
3151
+ const event = JSON.parse(line.slice(6));
3152
+ yield event;
3153
+ if (event.type === "exit" || event.type === "error") return;
3154
+ } catch {
3155
+ }
3156
+ }
3157
+ }
3158
+ }
3159
+ } finally {
3160
+ reader.releaseLock();
3161
+ }
3140
3162
  }
3141
3163
  /**
3142
- * List files in a directory.
3164
+ * Execute a command and collect all output (non-streaming).
3165
+ * Convenience wrapper over exec() for simple use cases.
3143
3166
  *
3144
- * @param path - Directory path inside the sandbox (default: '/')
3145
- * @returns Array of file/directory paths
3167
+ * For long-running commands, prefer exec() to stream output incrementally.
3146
3168
  */
3147
- async listFiles(path = "/") {
3148
- const result = await callApi(this.config, `/sandboxes/${this.id}/list`, {
3149
- method: "GET",
3150
- query: { path }
3151
- });
3152
- return result.files;
3169
+ async run(command, options) {
3170
+ let stdout = "";
3171
+ let stderr = "";
3172
+ let exitCode = 1;
3173
+ let durationMs = 0;
3174
+ for await (const event of this.exec(command, options)) {
3175
+ if (event.type === "stdout") stdout += event.data;
3176
+ else if (event.type === "stderr") stderr += event.data;
3177
+ else if (event.type === "exit") {
3178
+ exitCode = event.exitCode;
3179
+ durationMs = event.durationMs;
3180
+ }
3181
+ }
3182
+ return { stdout, stderr, exitCode, durationMs };
3153
3183
  }
3154
3184
  // ---------------------------------------------------------------------------
3155
- // Command Execution
3185
+ // PTY — Interactive terminal (WebSocket)
3156
3186
  // ---------------------------------------------------------------------------
3157
3187
  /**
3158
- * Run a command inside the sandbox.
3188
+ * Open an interactive PTY session (WebSocket).
3159
3189
  *
3160
- * The command runs synchronously inside the exec pod and returns when complete.
3161
- *
3162
- * @param command - Full command + args as array (e.g. ['python3', 'script.py'])
3163
- * @param options - Optional cwd, env, timeout, stdin
3164
- * @returns { stdout, stderr, exitCode, durationMs }
3190
+ * Returns a WebSocket connected to a bash shell in the sandbox.
3191
+ * Token is passed as a query param (WebSocket doesn't support custom headers in browsers).
3165
3192
  *
3166
3193
  * @example
3167
3194
  * ```typescript
3168
- * const { stdout, exitCode } = await sandbox.run(['python3', '-c', 'print(1+1)'])
3169
- * console.log(stdout) // "2\n"
3170
- * console.log(exitCode) // 0
3195
+ * const ws = await sandbox.pty()
3196
+ * ws.on('message', (data) => process.stdout.write(JSON.parse(data).data))
3197
+ * ws.send(JSON.stringify({ type: 'input', data: 'ls -la\n' }))
3198
+ * ws.send(JSON.stringify({ type: 'resize', cols: 120, rows: 40 }))
3171
3199
  * ```
3172
3200
  */
3173
- async run(command, options) {
3174
- return callApi(this.config, `/sandboxes/${this.id}/run`, {
3175
- method: "POST",
3176
- body: { command, ...options }
3177
- });
3201
+ pty() {
3202
+ this.assertDirect();
3203
+ const wsEndpoint = this.endpoint.replace(/^https:\/\//, "wss://").replace(
3204
+ /^http:\/\//,
3205
+ "ws://"
3206
+ );
3207
+ return new WebSocket(`${wsEndpoint}/pty?token=${encodeURIComponent(this.token)}`);
3208
+ }
3209
+ // ---------------------------------------------------------------------------
3210
+ // Private
3211
+ // ---------------------------------------------------------------------------
3212
+ assertDirect() {
3213
+ if (!this.endpoint || !this.token) {
3214
+ throw new Error(
3215
+ "Sandbox endpoint/token not available. This sandbox was created with an older SDK version or does not have a public endpoint."
3216
+ );
3217
+ }
3178
3218
  }
3179
3219
  };
3180
3220
 
@@ -3395,7 +3435,6 @@ export {
3395
3435
  WorkersClient,
3396
3436
  acceptAllConsents,
3397
3437
  acceptOrganizationInvitation,
3398
- addCustomDomain,
3399
3438
  batchIndex,
3400
3439
  canDeleteOrganization,
3401
3440
  canManageMembers,
@@ -3530,7 +3569,6 @@ export {
3530
3569
  kvZrange,
3531
3570
  leaveOrganization,
3532
3571
  linkAnonymousConsents,
3533
- listCustomDomains,
3534
3572
  listEnvVars,
3535
3573
  listScheduledEmails,
3536
3574
  listSecretKeys,
@@ -3546,7 +3584,6 @@ export {
3546
3584
  regenerateReferralCode,
3547
3585
  registerPush,
3548
3586
  registerPushServiceWorker,
3549
- removeCustomDomain,
3550
3587
  removeOrganizationMember,
3551
3588
  replayWebhookDelivery,
3552
3589
  rescheduleEmail,