@phpsandbox/sdk 0.1.9 → 0.1.10

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.
Files changed (59) hide show
  1. package/README.md +42 -50
  2. package/dist/beacon/index.d.ts +1 -7
  3. package/dist/beacon/index.js +14 -13
  4. package/dist/beacon/navigator.d.ts +8 -36
  5. package/dist/beacon/navigator.js +1 -62
  6. package/dist/browser/phpsandbox-sdk.esm.js +547 -691
  7. package/dist/browser/phpsandbox-sdk.esm.min.js +23 -23
  8. package/dist/browser/phpsandbox-sdk.iife.js +547 -691
  9. package/dist/browser/phpsandbox-sdk.iife.min.js +35 -35
  10. package/dist/composer.d.ts +10 -8
  11. package/dist/composer.js +18 -18
  12. package/dist/errors/index.d.ts +43 -8
  13. package/dist/errors/index.js +120 -14
  14. package/dist/feedback.js +1 -2
  15. package/dist/filesystem.d.ts +0 -17
  16. package/dist/filesystem.js +2 -21
  17. package/dist/git.d.ts +1 -5
  18. package/dist/git.js +6 -8
  19. package/dist/index.d.ts +54 -144
  20. package/dist/index.js +172 -289
  21. package/dist/integrations.d.ts +0 -2
  22. package/dist/integrations.js +5 -11
  23. package/dist/log.d.ts +0 -2
  24. package/dist/lsp.js +1 -1
  25. package/dist/metrics.d.ts +3 -3
  26. package/dist/ports.d.ts +1 -1
  27. package/dist/publications.d.ts +4 -54
  28. package/dist/publications.js +25 -103
  29. package/dist/runtime/rest.js +34 -17
  30. package/dist/runtime.d.ts +68 -0
  31. package/dist/runtime.js +46 -0
  32. package/dist/sandbox-integrations.d.ts +1 -2
  33. package/dist/sandbox-integrations.js +11 -14
  34. package/dist/schemas/publications.d.ts +0 -6
  35. package/dist/schemas/publications.js +0 -1
  36. package/dist/servers.d.ts +4 -2
  37. package/dist/servers.js +5 -8
  38. package/dist/shell.d.ts +2 -14
  39. package/dist/shell.js +1 -21
  40. package/dist/socket/index.d.ts +6 -7
  41. package/dist/socket/index.js +39 -25
  42. package/dist/types.d.ts +0 -12
  43. package/dist/types.js +1 -15
  44. package/docs/api-summary.md +28 -28
  45. package/docs/cdn-usage.md +43 -55
  46. package/docs/getting-started.md +43 -26
  47. package/examples/README.md +5 -35
  48. package/examples/basic.ts +9 -18
  49. package/examples/get.ts +2 -2
  50. package/examples/git-merge-smoke.ts +2 -3
  51. package/examples/laravel-load-test.ts +31 -36
  52. package/examples/notebook-readonly-smoke.ts +12 -26
  53. package/examples/publication-provider-label.ts +1 -1
  54. package/examples/publish-cloudflare-container.ts +2 -2
  55. package/examples/publish-laravel-cloud.ts +2 -2
  56. package/examples/publish-ssh-agent.ts +2 -2
  57. package/package.json +4 -3
  58. package/dist/container.d.ts +0 -65
  59. package/dist/container.js +0 -30
package/README.md CHANGED
@@ -12,6 +12,8 @@ Node.js `>=18` is required. This package is ESM-only; use `import` rather than `
12
12
 
13
13
  ## Quick Start
14
14
 
15
+ Save this as `quickstart.mjs` and run `node quickstart.mjs` after setting `PHPSANDBOX_TOKEN`. Expected output: `Hello from PHPSandbox`. The example creates and deletes a temporary plain PHP notebook.
16
+
15
17
  ```ts
16
18
  import { PHPSandbox } from '@phpsandbox/sdk';
17
19
 
@@ -19,36 +21,24 @@ const token = process.env.PHPSANDBOX_TOKEN;
19
21
  if (!token) throw new Error('Missing PHPSANDBOX_TOKEN');
20
22
 
21
23
  const client = PHPSandbox.realtime(token);
24
+ const notebook = await client.notebook.create('standard');
22
25
 
23
- // create() initializes the notebook by default
24
- const notebook = await client.notebook.create('php');
25
-
26
- await notebook.files.write('index.php', '<?php echo "Hello from PHPSandbox";');
27
-
28
- const process = await notebook.terminals.create({ command: 'php', args: ['index.php'] });
29
-
30
- const reader = process.output.getReader();
31
- let output = '';
32
- try {
33
- while (true) {
34
- const { done, value } = await reader.read();
35
- if (done) break;
36
- output += value;
37
- }
38
- } finally {
39
- reader.releaseLock();
40
- }
41
-
42
- await process.wait();
43
- console.log(output.trim());
26
+ await notebook.files.write('hello.php', '<?php echo "Hello from PHPSandbox";');
27
+ const result = await notebook.exec('php', ['hello.php']);
28
+ if (result.exitCode !== 0) throw new Error(result.stderr || result.output);
29
+ console.log(result.stdout.trim());
44
30
 
45
- // close websocket resources when done
31
+ await notebook.destroy();
46
32
  notebook.dispose();
47
33
  ```
48
34
 
49
35
  ## Authentication
50
36
 
51
- Use an API token:
37
+ Sign in to PHPSandbox and open **API keys**. Create a named key and copy it when shown; it cannot be revealed again. API-key access currently requires SDK access to be enabled for your account. Creating or forking notebooks requires an active subscription and available notebook capacity.
38
+
39
+ Keys from this screen grant account-level notebook read and write access. Keep them in a server environment variable or secret store. Never place them in browser code, notebooks, URLs, logs, or source control. To rotate, create a replacement, update your application, verify it, then revoke the old key.
40
+
41
+ Use an API key:
52
42
 
53
43
  ```ts
54
44
  import { PHPSandbox } from '@phpsandbox/sdk';
@@ -61,7 +51,6 @@ Explicit realtime and REST clients:
61
51
  ```ts
62
52
  PHPSandbox.realtime('token', 'https://api.phpsandbox.io/v1', {
63
53
  debug: false,
64
- startClosed: true,
65
54
  });
66
55
 
67
56
  // Use explicit request/response runtime actions without a websocket.
@@ -77,7 +66,6 @@ const client = PHPSandbox.realtime(process.env.PHPSANDBOX_TOKEN!);
77
66
 
78
67
  const created = await client.notebook.create('laravel');
79
68
  const fetched = await client.notebook.get('notebook-id');
80
- await fetched.ready();
81
69
 
82
70
  const forked = await created.fork();
83
71
  await forked.destroy();
@@ -97,8 +85,8 @@ console.log(persistentFork.data.policy);
97
85
 
98
86
  Notes:
99
87
 
100
- - `create()` and `fork()` initialize automatically.
101
- - `get()` returns a notebook instance; call `await notebook.ready()` before using tools.
88
+ - Runtime actions wait for the sandbox to become ready before Okra handles them.
89
+ - Call `ready()` explicitly only when you need provisioning results such as the preview URL or opened ports.
102
90
  - API notebook creates and forks are ephemeral by default. `persistent: true` requires an entitled account.
103
91
  - Use `NotebookData.runtimeUrl`, `runtimeUrlProvider`, and `PublicationData.eventStreamUrl` for runtime access.
104
92
 
@@ -108,16 +96,13 @@ Each `NotebookInstance` exposes service clients:
108
96
 
109
97
  - `notebook.files` (`Filesystem`)
110
98
  - `notebook.terminals` (`Terminals`)
111
- - `notebook.container` (`Container`)
99
+ - `notebook.runtime` (lifecycle, configuration, logs, metrics, and ports)
112
100
  - `notebook.composer` (`Composer`)
113
101
  - `notebook.git` (`Git`)
114
102
  - `notebook.integrations` (`SandboxIntegrationApi`)
115
103
  - `notebook.lsp` (`Lsp`)
116
104
  - `notebook.repl` (`Repl`)
117
105
  - `notebook.auth` (`Auth`)
118
- - `notebook.logs` (`Logs`)
119
- - `notebook.ports` (`Ports`)
120
- - `notebook.metrics` (`Metrics`)
121
106
  - `notebook.services` (`Services`)
122
107
  - `notebook.mail` (`NotebookMail`)
123
108
  - `notebook.feedback` (`Feedback`)
@@ -174,13 +159,14 @@ console.log(exitCode);
174
159
 
175
160
  ```ts
176
161
  const result = await notebook.exec('php -v');
177
- result.throw();
162
+ if (result.exitCode !== 0) throw new Error(result.stderr || result.output);
178
163
 
179
164
  const process = notebook.run('php -v', { tty: false });
180
165
  for await (const chunk of process.output) {
181
166
  console.log(chunk.source, new TextDecoder().decode(chunk.data));
182
167
  }
183
- (await process.wait()).throw();
168
+ const processResult = await process.wait();
169
+ if (processResult.exitCode !== 0) throw new Error(processResult.stderr || processResult.output);
184
170
  ```
185
171
 
186
172
  ### Services
@@ -208,18 +194,18 @@ await reader.cancel();
208
194
  ### Runtime observability
209
195
 
210
196
  ```ts
211
- const currentPorts = await notebook.ports.list();
212
- const currentMetrics = await notebook.metrics.current();
197
+ const currentPorts = await notebook.runtime.ports.list();
198
+ const currentMetrics = await notebook.runtime.metrics.current();
213
199
 
214
- for await (const sample of notebook.metrics.watch({ signal })) {
200
+ for await (const sample of notebook.runtime.metrics.watch({ signal })) {
215
201
  console.log(sample.cpu, sample.memory, sample.disk);
216
202
  }
217
203
 
218
- for await (const ports of notebook.ports.watch({ signal })) {
204
+ for await (const ports of notebook.runtime.ports.watch({ signal })) {
219
205
  console.log(ports);
220
206
  }
221
207
 
222
- for await (const entry of notebook.logs.follow({ signal })) {
208
+ for await (const entry of notebook.runtime.logs.follow({ signal })) {
223
209
  console.log(entry.timestamp, entry.source, entry.message);
224
210
  }
225
211
  ```
@@ -227,13 +213,15 @@ for await (const entry of notebook.logs.follow({ signal })) {
227
213
  ### Composer
228
214
 
229
215
  ```ts
230
- const install = notebook.composer.install();
216
+ const install = notebook.composer.run('install');
231
217
  for await (const chunk of install.output) {
232
218
  console.log(chunk.source, new TextDecoder().decode(chunk.data));
233
219
  }
234
- (await install.wait()).throw();
220
+ const installResult = await install.wait();
221
+ if (installResult.exitCode !== 0) throw new Error(installResult.stderr || installResult.output);
235
222
 
236
- (await notebook.composer.require({ packages: ['monolog/monolog'] }).wait()).throw();
223
+ const requireResult = await notebook.composer.run('require', { packages: ['monolog/monolog'] }).wait();
224
+ if (requireResult.exitCode !== 0) throw new Error(requireResult.stderr || requireResult.output);
237
225
 
238
226
  const installed = await notebook.composer.packages();
239
227
  console.log(installed.map((pkg) => pkg.name));
@@ -338,7 +326,7 @@ const disposeConnect = notebook.onDidConnect(() => {
338
326
  console.log('connected');
339
327
  });
340
328
 
341
- const disposeFs = notebook.files.watch(
329
+ const disposeFs = await notebook.files.watch(
342
330
  '/app',
343
331
  { recursive: true, excludes: ['vendor/**', 'node_modules/**'] },
344
332
  (change) => {
@@ -354,15 +342,19 @@ disposeFs.dispose();
354
342
  ## Error Handling
355
343
 
356
344
  ```ts
357
- import { ApiError, FilesystemError, FilesystemErrorType } from '@phpsandbox/sdk';
345
+ import { RemoteError, TransportError } from '@phpsandbox/sdk';
358
346
 
359
347
  try {
360
348
  await notebook.files.read('/does-not-exist.php');
361
349
  } catch (error) {
362
- if (error instanceof FilesystemError && error.name === FilesystemErrorType.FileNotFound) {
350
+ if (RemoteError.is(error, 'FileNotFound')) {
363
351
  console.error('Missing file');
364
- } else if (error instanceof ApiError) {
365
- console.error(error.status, error.body);
352
+ } else if (RemoteError.isValidation(error)) {
353
+ console.error(error.details.errors);
354
+ } else if (error instanceof RemoteError) {
355
+ console.error(error.source, error.status, error.code, error.message);
356
+ } else if (error instanceof TransportError) {
357
+ console.error(error.code, error.cause);
366
358
  } else {
367
359
  throw error;
368
360
  }
@@ -371,13 +363,13 @@ try {
371
363
 
372
364
  ## Browser/CDN Usage
373
365
 
374
- See `docs/cdn-usage.md` for ESM and script-tag examples.
366
+ See [Browser usage](docs/cdn-usage.md) for ESM and script-tag examples.
375
367
 
376
368
  ## More Docs
377
369
 
378
- - Getting started walkthrough: `docs/getting-started.md`
379
- - API overview: `docs/api-summary.md`
380
- - Examples: `examples/README.md`
370
+ - Getting started walkthrough: [Getting started](docs/getting-started.md)
371
+ - API overview: [API overview](docs/api-summary.md)
372
+ - Examples: [Examples](examples/README.md)
381
373
 
382
374
  ## Support
383
375
 
@@ -1,9 +1,7 @@
1
1
  import { Disposable } from '../types.js';
2
2
  import { BeaconActions, BeaconEvents, BeaconOptions, DebugInfo, ConsoleEvent, BeaconErrorEvent, DebugRequest, DebugResult, ScreenshotRequest, ScreenshotResult, FetchRequest, FetchResult } from './types.js';
3
3
  import { Navigator } from './navigator.js';
4
- import type { NotebookInstance } from '../index.js';
5
4
  export * from './types.js';
6
- export { Navigator } from './navigator.js';
7
5
  /**
8
6
  * BeaconError for beacon-specific errors
9
7
  */
@@ -163,11 +161,7 @@ export declare class Beacon implements BeaconActions {
163
161
  */
164
162
  dispose(): void;
165
163
  }
166
- /**
167
- * Factory function to create a beacon instance for an iframe
168
- */
169
- export declare function createBeacon(iframe: HTMLIFrameElement, options?: BeaconOptions): Beacon;
170
- export declare function connectBeacon(notebook: NotebookInstance, iframe: HTMLIFrameElement, options?: BeaconOptions): Promise<Beacon>;
164
+ export declare function connectBeacon(iframe: HTMLIFrameElement, options?: BeaconOptions): Promise<Beacon>;
171
165
  /**
172
166
  * Check if the current environment supports the Beacon SDK
173
167
  */
@@ -3,7 +3,6 @@ import { timeout } from '../utils/promise.js';
3
3
  import { retry } from '../utils/retry.js';
4
4
  import { Navigator } from './navigator.js';
5
5
  export * from './types.js';
6
- export { Navigator } from './navigator.js';
7
6
  /**
8
7
  * Browser environment check
9
8
  */
@@ -89,6 +88,9 @@ export class Beacon {
89
88
  if (event.source !== this.iframe.contentWindow) {
90
89
  return;
91
90
  }
91
+ if (this.options.targetOrigin !== '*' && event.origin !== this.options.targetOrigin) {
92
+ return;
93
+ }
92
94
  const message = event.data;
93
95
  if (!message || typeof message !== 'object' || !message.type) {
94
96
  return;
@@ -151,8 +153,7 @@ export class Beacon {
151
153
  timestamp: Date.now(),
152
154
  id: this.generateId(),
153
155
  };
154
- this.iframe.contentWindow.postMessage(establishMessage, '*', // Safe for channel establishment
155
- [this.messageChannel.port2] // Transfer the port
156
+ this.iframe.contentWindow.postMessage(establishMessage, this.options.targetOrigin, [this.messageChannel.port2] // Transfer the port
156
157
  );
157
158
  if (this.options.debug) {
158
159
  console.log('[Beacon SDK] Channel establishment message sent');
@@ -524,16 +525,16 @@ export class Beacon {
524
525
  this.readyPromise = null;
525
526
  }
526
527
  }
527
- /**
528
- * Factory function to create a beacon instance for an iframe
529
- */
530
- export function createBeacon(iframe, options) {
531
- return new Beacon(iframe, options);
532
- }
533
- export async function connectBeacon(notebook, iframe, options) {
534
- const result = await notebook.ready();
535
- iframe.src = result.data.previewUrl;
536
- return createBeacon(iframe, options);
528
+ export async function connectBeacon(iframe, options) {
529
+ const beacon = new Beacon(iframe, options);
530
+ try {
531
+ await beacon.start();
532
+ }
533
+ catch (error) {
534
+ beacon.dispose();
535
+ throw error;
536
+ }
537
+ return beacon;
537
538
  }
538
539
  /**
539
540
  * Check if the current environment supports the Beacon SDK
@@ -11,7 +11,7 @@ export interface NavigatorActions {
11
11
  goForward(): Promise<NavigationResult>;
12
12
  canGoBack: boolean;
13
13
  canGoForward: boolean;
14
- refresh(): Promise<void>;
14
+ reload(): Promise<void>;
15
15
  getCurrentUrl(): URL;
16
16
  on<K extends keyof NavigatorEvents>(event: K, handler: (payload: NavigatorEvents[K]) => void): Disposable;
17
17
  }
@@ -46,10 +46,6 @@ export interface HistoryInfoResponse {
46
46
  canGoBack: boolean;
47
47
  canGoForward: boolean;
48
48
  }
49
- export interface Navigator extends NavigatorActions {
50
- on<K extends keyof NavigatorEvents>(event: K, handler: (payload: NavigatorEvents[K]) => void): Disposable;
51
- once<K extends keyof NavigatorEvents>(event: K, handler: (payload: NavigatorEvents[K]) => void): void;
52
- }
53
49
  /**
54
50
  * SDK Navigator implementation with internal history management
55
51
  */
@@ -61,7 +57,7 @@ export declare class Navigator implements NavigatorActions {
61
57
  private currentIndex;
62
58
  private _canGoBack;
63
59
  private _canGoForward;
64
- constructor(onEvent: <K extends keyof NavigatorEvents>(event: K, handler: (payload: NavigatorEvents[K]) => void) => Disposable, onceEvent: <K extends keyof NavigatorEvents>(event: K, handler: (payload: NavigatorEvents[K]) => void) => void, beacon: Beacon);
60
+ constructor(onEvent: <K extends keyof NavigatorEvents>(event: K, handler: (payload: NavigatorEvents[K]) => void) => Disposable, onceEvent: <K extends keyof NavigatorEvents>(event: K, handler: (payload: NavigatorEvents[K]) => void) => Disposable, beacon: Beacon);
65
61
  /**
66
62
  * Navigate to a URL using internal history management
67
63
  */
@@ -97,44 +93,20 @@ export declare class Navigator implements NavigatorActions {
97
93
  * Get current URL from internal history
98
94
  */
99
95
  getCurrentUrl(): URL;
100
- /**
101
- * Get the complete history array
102
- */
103
- getHistory(): readonly string[];
104
- /**
105
- * Get current history index
106
- */
107
- getCurrentIndex(): number;
108
- /**
109
- * Check if can go back synchronously
110
- */
111
- canGoBackSync(): boolean;
112
- /**
113
- * Check if can go forward synchronously
114
- */
115
- canGoForwardSync(): boolean;
116
96
  /**
117
97
  * Emit history change event
118
98
  */
119
99
  private emitHistoryChange;
120
100
  /**
121
- * Push a new state to internal history (legacy method for compatibility)
122
- */
123
- pushState(url: string): Promise<void>;
124
- /**
125
- * Replace current state in internal history (legacy method for compatibility)
126
- */
127
- replaceState(url: string): Promise<void>;
128
- /**
129
- * Get the current history length
101
+ * Get comprehensive history information
130
102
  */
131
- getHistoryLength(): Promise<number>;
103
+ getHistoryInfo(): Promise<HistoryInfoResponse>;
132
104
  /**
133
- * Get the current history state (always null since we don't store state)
105
+ * Listen for navigator events
134
106
  */
135
- getHistoryState(): Promise<unknown>;
107
+ on<K extends keyof NavigatorEvents>(event: K, handler: (payload: NavigatorEvents[K]) => void): Disposable;
136
108
  /**
137
- * Get comprehensive history information
109
+ * Listen for navigator events (once)
138
110
  */
139
- getHistoryInfo(): Promise<HistoryInfoResponse>;
111
+ once<K extends keyof NavigatorEvents>(event: K, handler: (payload: NavigatorEvents[K]) => void): Disposable;
140
112
  }
@@ -142,30 +142,6 @@ export class Navigator {
142
142
  }
143
143
  return new URL('about:blank');
144
144
  }
145
- /**
146
- * Get the complete history array
147
- */
148
- getHistory() {
149
- return [...this.urlHistory];
150
- }
151
- /**
152
- * Get current history index
153
- */
154
- getCurrentIndex() {
155
- return this.currentIndex;
156
- }
157
- /**
158
- * Check if can go back synchronously
159
- */
160
- canGoBackSync() {
161
- return this.currentIndex > 0;
162
- }
163
- /**
164
- * Check if can go forward synchronously
165
- */
166
- canGoForwardSync() {
167
- return this.currentIndex < this.urlHistory.length - 1;
168
- }
169
145
  /**
170
146
  * Emit history change event
171
147
  */
@@ -181,43 +157,6 @@ export class Navigator {
181
157
  });
182
158
  }
183
159
  }
184
- /**
185
- * Push a new state to internal history (legacy method for compatibility)
186
- */
187
- async pushState(url) {
188
- // For compatibility, treat this as a navigation
189
- this.visit(url);
190
- }
191
- /**
192
- * Replace current state in internal history (legacy method for compatibility)
193
- */
194
- async replaceState(url) {
195
- if (this.currentIndex >= 0 && this.currentIndex < this.urlHistory.length) {
196
- // Replace the current URL in history
197
- this.urlHistory[this.currentIndex] = url;
198
- if (!this.beacon) {
199
- throw new Error('Beacon reference not set');
200
- }
201
- this.beacon.navigate(url);
202
- this.emitHistoryChange(url, 'replace');
203
- }
204
- else {
205
- // If no current history, treat as push
206
- this.visit(url);
207
- }
208
- }
209
- /**
210
- * Get the current history length
211
- */
212
- async getHistoryLength() {
213
- return this.urlHistory.length;
214
- }
215
- /**
216
- * Get the current history state (always null since we don't store state)
217
- */
218
- async getHistoryState() {
219
- return null;
220
- }
221
160
  /**
222
161
  * Get comprehensive history information
223
162
  */
@@ -240,6 +179,6 @@ export class Navigator {
240
179
  * Listen for navigator events (once)
241
180
  */
242
181
  once(event, handler) {
243
- this.onceEvent(event, handler);
182
+ return this.onceEvent(event, handler);
244
183
  }
245
184
  }