@phpsandbox/sdk 0.0.65 → 0.0.67

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.
@@ -0,0 +1,226 @@
1
+ import { ErrorEvent } from '../types.js';
2
+ import { UnsupportedRuntimeTransportError } from '../errors/index.js';
3
+ const restActionRoutes = {
4
+ 'runtime.ready': { method: 'GET', path: '/runtime' },
5
+ ping: { method: 'GET', path: '/runtime/ping' },
6
+ 'config.get': { method: 'GET', path: '/runtime/config' },
7
+ 'config.update': { method: 'PATCH', path: '/runtime/config' },
8
+ 'config.set-ports': { method: 'PUT', path: '/runtime/config/ports' },
9
+ 'container.state': { method: 'GET', path: '/runtime/container' },
10
+ 'container.start': { method: 'POST', path: '/runtime/container/start' },
11
+ 'container.restart': { method: 'POST', path: '/runtime/container/restart' },
12
+ 'container.stop': { method: 'POST', path: '/runtime/container/stop' },
13
+ 'container.set-php': { method: 'PUT', path: '/runtime/container/php' },
14
+ 'container.opened-ports': { method: 'GET', path: '/runtime/container/ports' },
15
+ 'terminal.list': { method: 'GET', path: '/runtime/terminals' },
16
+ 'shell.exec': { method: 'POST', path: '/runtime/executions' },
17
+ 'fs.info': { method: 'POST', path: '/runtime/files/info' },
18
+ 'fs.readFile': { method: 'GET', path: '/runtime/files/content' },
19
+ 'fs.writeFile': { method: 'PUT', path: '/runtime/files/content' },
20
+ 'fs.write': { method: 'PUT', path: '/runtime/files/text' },
21
+ 'fs.mkdir': { method: 'POST', path: '/runtime/directories' },
22
+ 'fs.createDirectory': { method: 'PUT', path: '/runtime/directories' },
23
+ 'fs.readDirectory': { method: 'POST', path: '/runtime/directories/read' },
24
+ 'fs.stat': { method: 'POST', path: '/runtime/files/stat' },
25
+ 'fs.move': { method: 'POST', path: '/runtime/files/move' },
26
+ 'fs.rename': { method: 'POST', path: '/runtime/files/rename' },
27
+ 'fs.copy': { method: 'POST', path: '/runtime/files/copy' },
28
+ 'fs.remove': { method: 'POST', path: '/runtime/files/remove' },
29
+ 'fs.delete': { method: 'DELETE', path: '/runtime/files' },
30
+ 'fs.tail': { method: 'POST', path: '/runtime/files/tail' },
31
+ 'fs.find': { method: 'POST', path: '/runtime/files/find' },
32
+ 'fs.textSearch': { method: 'POST', path: '/runtime/files/search' },
33
+ 'fs.tree': { method: 'POST', path: '/runtime/files/tree' },
34
+ 'service.list': { method: 'GET', path: '/runtime/services' },
35
+ 'service.run': { method: 'POST', path: '/runtime/services/run' },
36
+ 'service.stop': { method: 'POST', path: '/runtime/services/stop' },
37
+ 'service.logs': { method: 'POST', path: '/runtime/services/logs' },
38
+ 'composer.invoke': { method: 'POST', path: '/runtime/composer/executions' },
39
+ 'composer.packages': { method: 'GET', path: '/runtime/composer/packages' },
40
+ 'composer.dump-autoload': {
41
+ method: 'POST',
42
+ path: '/runtime/composer/autoload',
43
+ },
44
+ 'git.checkpoint': { method: 'POST', path: '/runtime/git/checkpoints' },
45
+ 'git.checkout': { method: 'POST', path: '/runtime/git/checkouts' },
46
+ 'git.log': { method: 'POST', path: '/runtime/git/log' },
47
+ 'git.sync': { method: 'POST', path: '/runtime/git/sync' },
48
+ 'git.diff': { method: 'POST', path: '/runtime/git/diff' },
49
+ 'git.merge': { method: 'POST', path: '/runtime/git/merge' },
50
+ 'git.conflicts': { method: 'GET', path: '/runtime/git/conflicts' },
51
+ 'git.resolve': { method: 'POST', path: '/runtime/git/conflicts/resolve' },
52
+ 'git.restore': { method: 'POST', path: '/runtime/git/restore' },
53
+ 'git.status': { method: 'GET', path: '/runtime/git/status' },
54
+ 'git.review': { method: 'GET', path: '/runtime/git/review' },
55
+ 'git.stage': { method: 'POST', path: '/runtime/git/stage' },
56
+ 'git.unstage': { method: 'POST', path: '/runtime/git/unstage' },
57
+ 'git.revert': { method: 'POST', path: '/runtime/git/revert' },
58
+ };
59
+ export class RestRuntimeInvoker {
60
+ constructor(data, fetch, refresh) {
61
+ this.fetch = fetch;
62
+ this.refresh = refresh;
63
+ this.refreshPromise = null;
64
+ this.notebookId = data.id;
65
+ this.configure(data.runtimeUrl ?? data.okraUrl);
66
+ }
67
+ configure(runtimeUrl) {
68
+ const actionUrl = new URL(runtimeUrl);
69
+ const ticket = actionUrl.searchParams.get('ticket') ?? '';
70
+ if (ticket === '') {
71
+ throw new UnsupportedRuntimeTransportError('The notebook connection does not contain a runtime ticket.');
72
+ }
73
+ this.ticket = ticket;
74
+ actionUrl.protocol = actionUrl.protocol === 'wss:' ? 'https:' : 'http:';
75
+ actionUrl.pathname = `/api/v1/notebooks/${encodeURIComponent(this.notebookId)}`;
76
+ actionUrl.searchParams.delete('ticket');
77
+ actionUrl.hash = '';
78
+ this.actionUrl = actionUrl;
79
+ }
80
+ ready() {
81
+ return this.invokeRaw('runtime.ready', {}, {});
82
+ }
83
+ supports(action) {
84
+ return action in restActionRoutes;
85
+ }
86
+ invoke(action, data = {}, options = {}) {
87
+ return this.invokeRaw(action, data ?? {}, options);
88
+ }
89
+ send() {
90
+ throw new UnsupportedRuntimeTransportError('Fire-and-forget actions require the realtime transport.');
91
+ }
92
+ async invokeRaw(action, data, options) {
93
+ const abort = runtimeAbort(options);
94
+ try {
95
+ for (let attempt = 0; attempt < 2; attempt += 1) {
96
+ const response = await this.fetch(this.request(action, data, abort.signal));
97
+ if (response.status === 401 && attempt === 0 && this.refresh !== undefined) {
98
+ await this.refreshAuthorization();
99
+ continue;
100
+ }
101
+ const payload = await runtimeActionResponse(response);
102
+ if (!response.ok || payload.error !== undefined) {
103
+ const error = payload.error ?? payload;
104
+ const message = typeof error.message === 'string' ? error.message : `Runtime action ${action} failed.`;
105
+ const code = typeof error.code === 'number' ? error.code : response.status;
106
+ throw new ErrorEvent(code, message, error);
107
+ }
108
+ return payload.data;
109
+ }
110
+ throw new ErrorEvent(401, `Runtime action ${action} could not be authorized.`, {});
111
+ }
112
+ finally {
113
+ abort.dispose();
114
+ }
115
+ }
116
+ request(action, data, signal) {
117
+ const route = restActionRoutes[action];
118
+ if (route === undefined) {
119
+ throw new UnsupportedRuntimeTransportError(`Runtime action ${action} requires the realtime transport.`);
120
+ }
121
+ const url = new URL(this.actionUrl);
122
+ url.pathname += route.path;
123
+ const headers = {
124
+ Accept: 'application/json, application/octet-stream',
125
+ Authorization: `Bearer ${this.ticket}`,
126
+ };
127
+ const body = runtimeActionRequestBody(action, data, url, headers);
128
+ return new Request(url, {
129
+ method: route.method,
130
+ headers,
131
+ body: route.method === 'GET' ? undefined : body,
132
+ signal,
133
+ });
134
+ }
135
+ async refreshAuthorization() {
136
+ if (this.refreshPromise === null) {
137
+ this.refreshPromise = this.refresh().then((runtimeUrl) => this.configure(runtimeUrl));
138
+ }
139
+ try {
140
+ await this.refreshPromise;
141
+ }
142
+ finally {
143
+ this.refreshPromise = null;
144
+ }
145
+ }
146
+ }
147
+ async function runtimeActionResponse(response) {
148
+ if (response.ok && response.headers.get('content-type')?.toLowerCase().startsWith('application/octet-stream')) {
149
+ return { data: new Uint8Array(await response.arrayBuffer()) };
150
+ }
151
+ const body = await response.text();
152
+ if (body === '') {
153
+ return {};
154
+ }
155
+ try {
156
+ return JSON.parse(body);
157
+ }
158
+ catch {
159
+ return { error: { code: response.status, message: body } };
160
+ }
161
+ }
162
+ function runtimeActionRequestBody(action, data, url, headers) {
163
+ const payload = runtimeActionRecord(data);
164
+ if (action === 'fs.readFile') {
165
+ setRequiredRuntimeQuery(url, 'path', payload.path);
166
+ const lineRange = runtimeActionRecord(payload.lineRange);
167
+ if (lineRange.lineStart !== undefined || lineRange.lineEnd !== undefined) {
168
+ setRequiredRuntimeQuery(url, 'lineStart', lineRange.lineStart);
169
+ setRequiredRuntimeQuery(url, 'lineEnd', lineRange.lineEnd);
170
+ }
171
+ return undefined;
172
+ }
173
+ if (action === 'fs.writeFile') {
174
+ setRequiredRuntimeQuery(url, 'path', payload.path);
175
+ const options = runtimeActionRecord(payload.options);
176
+ setBooleanRuntimeQuery(url, 'overwrite', options.overwrite);
177
+ setBooleanRuntimeQuery(url, 'create', options.create);
178
+ setBooleanRuntimeQuery(url, 'unlock', options.unlock);
179
+ const atomic = runtimeActionRecord(options.atomic);
180
+ if (typeof atomic.postfix === 'string' && atomic.postfix !== '') {
181
+ url.searchParams.set('atomicPostfix', atomic.postfix);
182
+ }
183
+ if (!ArrayBuffer.isView(payload.contents)) {
184
+ throw new TypeError('Runtime file contents must be binary data.');
185
+ }
186
+ headers['Content-Type'] = 'application/octet-stream';
187
+ return Uint8Array.from(new Uint8Array(payload.contents.buffer, payload.contents.byteOffset, payload.contents.byteLength)).buffer;
188
+ }
189
+ headers['Content-Type'] = 'application/json';
190
+ return JSON.stringify(data);
191
+ }
192
+ function runtimeActionRecord(value) {
193
+ return value !== null && typeof value === 'object' ? value : {};
194
+ }
195
+ function setRequiredRuntimeQuery(url, name, value) {
196
+ if ((typeof value !== 'string' && typeof value !== 'number') || String(value) === '') {
197
+ throw new TypeError(`Runtime file ${name} is required.`);
198
+ }
199
+ url.searchParams.set(name, String(value));
200
+ }
201
+ function setBooleanRuntimeQuery(url, name, value) {
202
+ url.searchParams.set(name, value === true ? 'true' : 'false');
203
+ }
204
+ function runtimeAbort(options) {
205
+ if (options.timeout === undefined && options.abortSignal === undefined) {
206
+ return { signal: new AbortController().signal, dispose: () => undefined };
207
+ }
208
+ const controller = new AbortController();
209
+ const abortFromCaller = () => controller.abort(options.abortSignal?.reason);
210
+ options.abortSignal?.addEventListener('abort', abortFromCaller, {
211
+ once: true,
212
+ });
213
+ if (options.abortSignal?.aborted) {
214
+ abortFromCaller();
215
+ }
216
+ const timeout = options.timeout === undefined ? undefined : setTimeout(() => controller.abort(), options.timeout);
217
+ return {
218
+ signal: controller.signal,
219
+ dispose: () => {
220
+ options.abortSignal?.removeEventListener('abort', abortFromCaller);
221
+ if (timeout !== undefined) {
222
+ clearTimeout(timeout);
223
+ }
224
+ },
225
+ };
226
+ }
@@ -1,6 +1,7 @@
1
1
  import type { CloseEvent as ReconnectingWebSocketCloseEvent, ErrorEvent as ReconnectingWebSocketErrorEvent } from 'reconnecting-websocket';
2
2
  import { EventDispatcher } from '../events/index.js';
3
3
  import { NotebookUnavailableError } from '../errors/index.js';
4
+ type TransportUrlProvider = string | (() => string) | (() => Promise<string>);
4
5
  interface WsOptions {
5
6
  debug?: boolean;
6
7
  startClosed?: boolean;
@@ -73,7 +74,7 @@ export declare class Transport {
73
74
  private readonly CONNECT_WAIT_TIMEOUT;
74
75
  private pingIntervalStarted;
75
76
  private incomingMessageChain;
76
- constructor(url: string, eventEmitter: EventDispatcher, options?: WsOptions);
77
+ constructor(url: TransportUrlProvider, eventEmitter: EventDispatcher, options?: WsOptions);
77
78
  /**
78
79
  * Validate configuration options
79
80
  */
@@ -65,13 +65,16 @@ export class Transport {
65
65
  this.incomingMessageChain = Promise.resolve();
66
66
  // Validate configuration
67
67
  this.validateConfiguration(options);
68
- this.url = new URL(url);
68
+ this.url = typeof url === 'string' ? new URL(url) : null;
69
+ const urlProvider = typeof url === 'string'
70
+ ? this.url.toString()
71
+ : async () => new URL(await url()).toString();
69
72
  // Use configurable ping interval
70
73
  this.PING_INTERVAL = options.pingInterval ?? 30000;
71
74
  this.CONNECT_WAIT_TIMEOUT = options.connectWaitTimeout ?? 30000;
72
75
  // Always start closed by default (lazy initialization)
73
76
  const startClosed = options.startClosed !== false;
74
- this.rws = new ReconnectingWebSocket(this.url.toString(), [], {
77
+ this.rws = new ReconnectingWebSocket(urlProvider, [], {
75
78
  WebSocket: options.webSocket ?? globalThis.WebSocket ?? WebSocket,
76
79
  connectionTimeout: options.connectionTimeout,
77
80
  maxReconnectionDelay: 2000,
@@ -81,7 +84,7 @@ export class Transport {
81
84
  startClosed,
82
85
  });
83
86
  this.log('debug', 'Transport initialized', {
84
- url: this.url.toString(),
87
+ url: this.url === null ? 'dynamic' : transportLogUrl(this.url),
85
88
  options,
86
89
  });
87
90
  this.registerWatchers();
@@ -699,3 +702,9 @@ _Transport_instances = new WeakSet(), _Transport_connect = function _Transport_c
699
702
  };
700
703
  });
701
704
  };
705
+ function transportLogUrl(url) {
706
+ const safeUrl = new URL(url);
707
+ safeUrl.search = '';
708
+ safeUrl.hash = '';
709
+ return safeUrl.toString();
710
+ }
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type ErrorCode = 500 | 400 | 401 | 403 | 404 | 409 | 422 | 429 | 5000;
1
+ export type ErrorCode = 400 | 401 | 403 | 404 | 405 | 409 | 413 | 422 | 429 | 500 | 503 | 5000;
2
2
  export declare class ErrorEvent extends Error {
3
3
  readonly code: ErrorCode;
4
4
  readonly message: string;
@@ -7,12 +7,18 @@ This is a quick reference for the current SDK surface.
7
7
  ```ts
8
8
  import { PHPSandbox } from '@phpsandbox/sdk';
9
9
 
10
- const client = new PHPSandbox(token, 'https://api.phpsandbox.io/v1', {
10
+ const client = PHPSandbox.realtime(token, 'https://api.phpsandbox.io/v1', {
11
11
  debug: false,
12
12
  startClosed: true,
13
13
  });
14
+
15
+ const restClient = PHPSandbox.rest(token, 'https://api.phpsandbox.io/v1', {
16
+ runtimeUrlProvider: async (notebookId) => getFreshRuntimeUrl(notebookId),
17
+ });
14
18
  ```
15
19
 
20
+ Connection lifecycle methods, streaming actions, and event subscriptions require the realtime transport. The REST transport rejects unsupported operations without falling back to a websocket.
21
+
16
22
  ### `NotebookApi`
17
23
 
18
24
  - `create(template, input?, init = true)`
@@ -110,11 +116,11 @@ const client = new PHPSandbox(token, 'https://api.phpsandbox.io/v1', {
110
116
 
111
117
  ### Laravel (`notebook.laravel`)
112
118
 
113
- - Wrapper exists in the SDK, but the current okra websocket action set does not expose Laravel maintenance actions.
119
+ - Wrapper exists in the SDK, but the current runtime action set does not expose Laravel maintenance actions.
114
120
 
115
121
  ### Auth (`notebook.auth`)
116
122
 
117
- - `login(newConnectionData)`
123
+ - `login(runtimeUrl)` using a fresh ticket-bearing runtime URL
118
124
  - `logout()`
119
125
 
120
126
  ### Log (`notebook.log`)
@@ -96,7 +96,7 @@ Useful flags:
96
96
 
97
97
  - `--no-cleanup` keeps every notebook after the run.
98
98
  - `--keep-failed` keeps failed notebooks for inspection. By default, failures are deleted too.
99
- - `--okra-origin=ws://127.0.0.1:9511` preserves the SDK-issued websocket path/auth but routes okra traffic to another host.
99
+ - `--runtime-origin=ws://127.0.0.1:9511` preserves the SDK-issued websocket path/auth but routes runtime traffic to another host.
100
100
  - `--include-container-restart` adds a mutable `container.start()` restart check on the fresh notebook.
101
101
  - `--include-download` adds `file.download()` to the baseline.
102
102
  - `--include-preview` fetches the initialized preview URL in init mode, or the route written by the scenario in full mode.
@@ -104,7 +104,7 @@ Useful flags:
104
104
  - `--require-init-messages="Preparing...|Setting up...|Opening..."` asserts frontend-visible init messages were received in order.
105
105
  - `--expect-provisioned=true` asserts the `notebook.ready()` initialization result reports provisioning work.
106
106
  - `--expect-provisioning-action=template` asserts the initialization result action.
107
- - `--auth-payload=...` exercises `notebook.auth.login()` / `logout()`.
107
+ - `--reauthorization-url=wss://...?ticket=...` exercises `notebook.auth.login()` / `logout()` with a fresh ticket-bearing runtime URL.
108
108
  - `--mode=init` limits the scenario to notebook create, ready, and delete so you can isolate init capacity.
109
109
  - `--init-only` is shorthand for `--mode=init`.
110
110
  - `--stop-on-failure-level` stops after the first level that has at least one failing scenario.
@@ -131,7 +131,7 @@ Default baseline coverage:
131
131
  Intentionally excluded by default:
132
132
 
133
133
  - `beacon`, because it needs a browser iframe runtime
134
- - `auth`, because it needs a real connection payload
134
+ - `auth`, because it needs a fresh ticket-bearing runtime URL
135
135
  - `laravel.maintenance.*`, because it is not part of the current okra websocket action set
136
136
  - `lsp`, because it is useful but more brittle than the rest of the headless baseline
137
137
 
@@ -153,12 +153,12 @@ PHPSANDBOX_TOKEN=your-token npx tsx examples/notebook-readonly-smoke.ts \
153
153
  --notebook-id=gypjc
154
154
  ```
155
155
 
156
- To preserve the SDK-issued notebook auth/path but route the websocket to a different okra host:
156
+ To preserve the SDK-issued notebook auth/path but route the websocket to a different runtime host:
157
157
 
158
158
  ```bash
159
159
  PHPSANDBOX_TOKEN=your-token npx tsx examples/notebook-readonly-smoke.ts \
160
160
  --notebook-id=gypjc \
161
- --okra-origin=ws://127.0.0.1:9501
161
+ --runtime-origin=ws://127.0.0.1:9501
162
162
  ```
163
163
 
164
164
  Default smoke coverage:
@@ -59,8 +59,8 @@ interface LoadTestOptions {
59
59
  token: string;
60
60
  local: boolean;
61
61
  apiUrl: string;
62
- okraOrigin?: string;
63
- okraUrl?: string;
62
+ runtimeOrigin?: string;
63
+ runtimeUrl?: string;
64
64
  template: string;
65
65
  mode: LoadTestMode;
66
66
  stopOnFailureLevel: boolean;
@@ -84,7 +84,7 @@ interface LoadTestOptions {
84
84
  requireInitMessages: string[];
85
85
  expectProvisioned?: boolean;
86
86
  expectProvisioningAction?: NotebookProvisioningAction;
87
- authPayload?: string;
87
+ reauthorizationUrl?: string;
88
88
  json: boolean;
89
89
  jsonFile?: string;
90
90
  insecureTls: boolean;
@@ -478,7 +478,7 @@ async function runScenario(
478
478
 
479
479
  const data = {
480
480
  ...created.data,
481
- okraUrl: resolveOkraUrl(created.data.okraUrl, options),
481
+ runtimeUrl: resolveRuntimeUrl(created.data.runtimeUrl ?? created.data.okraUrl, options),
482
482
  };
483
483
  created.dispose();
484
484
 
@@ -947,14 +947,14 @@ async function runScenario(
947
947
  recordSkippedStep(steps, 'lsp.initialize', 'Enable with --include-lsp.');
948
948
  }
949
949
 
950
- if (options.authPayload) {
950
+ if (options.reauthorizationUrl) {
951
951
  await runCheckedStep('auth.login-logout', async () => {
952
- await notebook!.auth.login(options.authPayload!);
952
+ await notebook!.auth.login(options.reauthorizationUrl!);
953
953
  await notebook!.auth.logout();
954
954
  return 'ok';
955
955
  });
956
956
  } else {
957
- recordSkippedStep(steps, 'auth.login-logout', 'Provide --auth-payload to exercise auth.');
957
+ recordSkippedStep(steps, 'auth.login-logout', 'Provide --reauthorization-url to exercise auth.');
958
958
  }
959
959
  }
960
960
  } catch (error) {
@@ -1071,8 +1071,8 @@ function renderConfiguration(options: LoadTestOptions): string {
1071
1071
  ` mode: ${options.mode}`,
1072
1072
  ` stop on failing level: ${options.stopOnFailureLevel}`,
1073
1073
  ` api url: ${options.apiUrl}`,
1074
- options.okraOrigin ? ` okra origin override: ${options.okraOrigin}` : undefined,
1075
- options.okraUrl ? ` okra url override: ${redactUrl(options.okraUrl)}` : undefined,
1074
+ options.runtimeOrigin ? ` runtime origin override: ${options.runtimeOrigin}` : undefined,
1075
+ options.runtimeUrl ? ` runtime url override: ${redactUrl(options.runtimeUrl)}` : undefined,
1076
1076
  ` levels: ${options.levels.join(', ')}`,
1077
1077
  ` waves per level: ${options.waves}`,
1078
1078
  ` visibility: ${options.visibility}`,
@@ -1085,7 +1085,7 @@ function renderConfiguration(options: LoadTestOptions): string {
1085
1085
  ` required init messages: ${options.requireInitMessages.length > 0 ? options.requireInitMessages.join(' -> ') : 'none'}`,
1086
1086
  ` expect provisioned: ${options.expectProvisioned ?? 'not asserted'}`,
1087
1087
  ` expect provisioning action: ${options.expectProvisioningAction ?? 'not asserted'}`,
1088
- ` include auth: ${Boolean(options.authPayload)}`,
1088
+ ` include auth: ${Boolean(options.reauthorizationUrl)}`,
1089
1089
  ` insecure tls: ${options.insecureTls}`,
1090
1090
  ` ca file: ${options.caFile ?? 'none'}`,
1091
1091
  ];
@@ -2291,8 +2291,8 @@ function sanitizeArtifact<T>(payload: T): T {
2291
2291
  if ('token' in cloned.options) {
2292
2292
  cloned.options.token = '<redacted>';
2293
2293
  }
2294
- if ('authPayload' in cloned.options) {
2295
- cloned.options.authPayload = '<redacted>';
2294
+ if ('reauthorizationUrl' in cloned.options) {
2295
+ cloned.options.reauthorizationUrl = '<redacted>';
2296
2296
  }
2297
2297
  }
2298
2298
 
@@ -2400,8 +2400,8 @@ function parseOptions(argv: string[]): LoadTestOptions {
2400
2400
  token,
2401
2401
  local,
2402
2402
  apiUrl,
2403
- okraOrigin: stringArg(args, 'okra-origin'),
2404
- okraUrl: stringArg(args, 'okra-url'),
2403
+ runtimeOrigin: stringArg(args, 'runtime-origin'),
2404
+ runtimeUrl: stringArg(args, 'runtime-url'),
2405
2405
  template: stringArg(args, 'template') ?? 'laravel',
2406
2406
  mode: resolveMode(args),
2407
2407
  stopOnFailureLevel: booleanArg(args, 'stop-on-failure-level') ?? false,
@@ -2425,7 +2425,7 @@ function parseOptions(argv: string[]): LoadTestOptions {
2425
2425
  requireInitMessages: listArg(args, 'require-init-messages'),
2426
2426
  expectProvisioned: booleanArg(args, 'expect-provisioned'),
2427
2427
  expectProvisioningAction: provisioningActionArg(args, 'expect-provisioning-action'),
2428
- authPayload: stringArg(args, 'auth-payload'),
2428
+ reauthorizationUrl: stringArg(args, 'reauthorization-url'),
2429
2429
  json: booleanArg(args, 'json') ?? false,
2430
2430
  jsonFile: stringArg(args, 'json-file'),
2431
2431
  insecureTls,
@@ -2433,17 +2433,17 @@ function parseOptions(argv: string[]): LoadTestOptions {
2433
2433
  };
2434
2434
  }
2435
2435
 
2436
- function resolveOkraUrl(original: string, options: LoadTestOptions): string {
2437
- if (options.okraUrl) {
2438
- return options.okraUrl;
2436
+ function resolveRuntimeUrl(original: string, options: LoadTestOptions): string {
2437
+ if (options.runtimeUrl) {
2438
+ return options.runtimeUrl;
2439
2439
  }
2440
2440
 
2441
- if (!options.okraOrigin) {
2441
+ if (!options.runtimeOrigin) {
2442
2442
  return original;
2443
2443
  }
2444
2444
 
2445
2445
  const source = new URL(original);
2446
- const target = new URL(options.okraOrigin);
2446
+ const target = new URL(options.runtimeOrigin);
2447
2447
  source.protocol = target.protocol;
2448
2448
  source.host = target.host;
2449
2449
  return source.toString();
@@ -24,8 +24,8 @@ interface SmokeOptions {
24
24
  local: boolean;
25
25
  apiUrl: string;
26
26
  notebookId: string;
27
- okraUrl?: string;
28
- okraOrigin?: string;
27
+ runtimeUrl?: string;
28
+ runtimeOrigin?: string;
29
29
  includeExec: boolean;
30
30
  includeDownload: boolean;
31
31
  includePreview: boolean;
@@ -67,8 +67,8 @@ interface SmokeArtifact {
67
67
  apiUrl: string;
68
68
  local: boolean;
69
69
  notebookId: string;
70
- okraUrl?: string;
71
- okraOrigin?: string;
70
+ runtimeUrl?: string;
71
+ runtimeOrigin?: string;
72
72
  includeExec: boolean;
73
73
  includeDownload: boolean;
74
74
  includePreview: boolean;
@@ -77,7 +77,7 @@ interface SmokeArtifact {
77
77
  id: string;
78
78
  title?: string;
79
79
  type?: string;
80
- okraUrl: string;
80
+ runtimeUrl: string;
81
81
  };
82
82
  initTrace: InitTraceArtifact;
83
83
  steps: StepArtifact[];
@@ -103,13 +103,13 @@ async function main(): Promise<void> {
103
103
  const result = await client.get<NotebookData>(`/notebook/${options.notebookId}`);
104
104
  return {
105
105
  data: result.data,
106
- detail: `id=${result.data.id} type=${result.data.type ?? ''} title=${result.data.title ?? ''} okraUrl=${redactUrl(result.data.okraUrl)}`,
106
+ detail: `id=${result.data.id} type=${result.data.type ?? ''} title=${result.data.title ?? ''} runtimeUrl=${redactUrl(result.data.runtimeUrl ?? result.data.okraUrl)}`,
107
107
  };
108
108
  });
109
109
 
110
110
  notebookData = {
111
111
  ...response.data,
112
- okraUrl: resolveOkraUrl(response.data.okraUrl, options),
112
+ runtimeUrl: resolveRuntimeUrl(response.data.runtimeUrl ?? response.data.okraUrl, options),
113
113
  };
114
114
 
115
115
  notebook = new NotebookInstance(notebookData, client);
@@ -289,8 +289,8 @@ async function main(): Promise<void> {
289
289
  apiUrl: options.apiUrl,
290
290
  local: options.local,
291
291
  notebookId: options.notebookId,
292
- okraUrl: options.okraUrl ? redactUrl(options.okraUrl) : undefined,
293
- okraOrigin: options.okraOrigin,
292
+ runtimeUrl: options.runtimeUrl ? redactUrl(options.runtimeUrl) : undefined,
293
+ runtimeOrigin: options.runtimeOrigin,
294
294
  includeExec: options.includeExec,
295
295
  includeDownload: options.includeDownload,
296
296
  includePreview: options.includePreview,
@@ -300,7 +300,7 @@ async function main(): Promise<void> {
300
300
  id: notebookData.id,
301
301
  title: notebookData.title,
302
302
  type: notebookData.type,
303
- okraUrl: redactUrl(notebookData.okraUrl),
303
+ runtimeUrl: redactUrl(notebookData.runtimeUrl ?? notebookData.okraUrl),
304
304
  }
305
305
  : undefined,
306
306
  initTrace,
@@ -471,17 +471,17 @@ async function readStreamToString(stream: ReadableStream<string>, timeoutMs: num
471
471
  }
472
472
  }
473
473
 
474
- function resolveOkraUrl(original: string, options: SmokeOptions): string {
475
- if (options.okraUrl) {
476
- return options.okraUrl;
474
+ function resolveRuntimeUrl(original: string, options: SmokeOptions): string {
475
+ if (options.runtimeUrl) {
476
+ return options.runtimeUrl;
477
477
  }
478
478
 
479
- if (!options.okraOrigin) {
479
+ if (!options.runtimeOrigin) {
480
480
  return original;
481
481
  }
482
482
 
483
483
  const source = new URL(original);
484
- const target = new URL(options.okraOrigin);
484
+ const target = new URL(options.runtimeOrigin);
485
485
  source.protocol = target.protocol;
486
486
  source.host = target.host;
487
487
  return source.toString();
@@ -508,8 +508,8 @@ function parseOptions(argv: string[]): SmokeOptions {
508
508
  process.env.PHPSANDBOX_API_URL ??
509
509
  ((booleanArg(args, 'local') ?? false) ? DEFAULT_LOCAL_API_URL : DEFAULT_API_URL),
510
510
  notebookId: stringArg(args, 'notebook-id') ?? 'gypjc',
511
- okraUrl: stringArg(args, 'okra-url'),
512
- okraOrigin: stringArg(args, 'okra-origin'),
511
+ runtimeUrl: stringArg(args, 'runtime-url'),
512
+ runtimeOrigin: stringArg(args, 'runtime-origin'),
513
513
  includeExec: booleanArg(args, 'include-exec') ?? false,
514
514
  includeDownload: booleanArg(args, 'include-download') ?? false,
515
515
  includePreview: booleanArg(args, 'include-preview') ?? false,
@@ -786,7 +786,7 @@ function printSummary(artifact: SmokeArtifact): void {
786
786
  console.log('\n=== Notebook readonly smoke summary ===');
787
787
  console.log(`Status: ${artifact.status}`);
788
788
  console.log(`Notebook: ${artifact.options.notebookId}`);
789
- console.log(`Okra URL: ${artifact.notebook?.okraUrl ?? 'unresolved'}`);
789
+ console.log(`Runtime URL: ${artifact.notebook?.runtimeUrl ?? 'unresolved'}`);
790
790
  console.log(`Duration: ${formatDuration(artifact.durationMs)}`);
791
791
  console.log(`Steps: passed=${passed} failed=${failed} skipped=${skipped}`);
792
792
 
@@ -806,8 +806,8 @@ Options:
806
806
  --local Use ${DEFAULT_LOCAL_API_URL} and default to insecure TLS.
807
807
  --insecure-tls Disable TLS verification for local HTTPS/WSS.
808
808
  --ca-file=<path> Trust a custom CA for local HTTPS/WSS.
809
- --okra-url=<url> Replace notebook okraUrl with a full WebSocket URL.
810
- --okra-origin=<origin> Replace only the okraUrl origin, preserving path/query auth.
809
+ --runtime-url=<url> Replace the notebook runtimeUrl with a full WebSocket URL.
810
+ --runtime-origin=<origin> Replace only the runtimeUrl origin, preserving path/query auth.
811
811
  --include-exec Also run shell, terminal, and REPL read-safe execution checks.
812
812
  --include-download Also run a read-only file.download() check for a known file.
813
813
  --include-preview Fetch the initialized preview URL.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phpsandbox/sdk",
3
- "version": "0.0.65",
3
+ "version": "0.0.67",
4
4
  "description": "A comprehensive TypeScript SDK for interacting with cloud-based PHP development environments",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",