@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.
- package/README.md +14 -8
- package/dist/auth.d.ts +3 -4
- package/dist/auth.js +7 -3
- package/dist/browser/phpsandbox-sdk.esm.js +374 -49
- package/dist/browser/phpsandbox-sdk.esm.min.js +24 -24
- package/dist/browser/phpsandbox-sdk.iife.js +374 -49
- package/dist/browser/phpsandbox-sdk.iife.min.js +24 -24
- package/dist/deployments.d.ts +2 -0
- package/dist/deployments.js +6 -5
- package/dist/errors/index.d.ts +3 -0
- package/dist/errors/index.js +6 -0
- package/dist/filesystem.d.ts +2 -2
- package/dist/filesystem.js +7 -4
- package/dist/index.d.ts +14 -0
- package/dist/index.js +110 -34
- package/dist/runtime/rest.d.ts +20 -0
- package/dist/runtime/rest.js +226 -0
- package/dist/socket/index.d.ts +2 -1
- package/dist/socket/index.js +12 -3
- package/dist/types.d.ts +1 -1
- package/docs/api-summary.md +9 -3
- package/examples/README.md +5 -5
- package/examples/laravel-load-test.ts +20 -20
- package/examples/notebook-readonly-smoke.ts +20 -20
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/socket/index.d.ts
CHANGED
|
@@ -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:
|
|
77
|
+
constructor(url: TransportUrlProvider, eventEmitter: EventDispatcher, options?: WsOptions);
|
|
77
78
|
/**
|
|
78
79
|
* Validate configuration options
|
|
79
80
|
*/
|
package/dist/socket/index.js
CHANGED
|
@@ -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(
|
|
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.
|
|
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 =
|
|
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;
|
package/docs/api-summary.md
CHANGED
|
@@ -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 =
|
|
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
|
|
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(
|
|
123
|
+
- `login(runtimeUrl)` using a fresh ticket-bearing runtime URL
|
|
118
124
|
- `logout()`
|
|
119
125
|
|
|
120
126
|
### Log (`notebook.log`)
|
package/examples/README.md
CHANGED
|
@@ -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
|
-
- `--
|
|
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
|
-
- `--
|
|
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
|
|
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
|
|
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
|
-
--
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
950
|
+
if (options.reauthorizationUrl) {
|
|
951
951
|
await runCheckedStep('auth.login-logout', async () => {
|
|
952
|
-
await notebook!.auth.login(options.
|
|
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 --
|
|
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.
|
|
1075
|
-
options.
|
|
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.
|
|
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 ('
|
|
2295
|
-
cloned.options.
|
|
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
|
-
|
|
2404
|
-
|
|
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
|
-
|
|
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
|
|
2437
|
-
if (options.
|
|
2438
|
-
return options.
|
|
2436
|
+
function resolveRuntimeUrl(original: string, options: LoadTestOptions): string {
|
|
2437
|
+
if (options.runtimeUrl) {
|
|
2438
|
+
return options.runtimeUrl;
|
|
2439
2439
|
}
|
|
2440
2440
|
|
|
2441
|
-
if (!options.
|
|
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.
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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 ?? ''}
|
|
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
|
-
|
|
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
|
-
|
|
293
|
-
|
|
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
|
-
|
|
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
|
|
475
|
-
if (options.
|
|
476
|
-
return options.
|
|
474
|
+
function resolveRuntimeUrl(original: string, options: SmokeOptions): string {
|
|
475
|
+
if (options.runtimeUrl) {
|
|
476
|
+
return options.runtimeUrl;
|
|
477
477
|
}
|
|
478
478
|
|
|
479
|
-
if (!options.
|
|
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.
|
|
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
|
-
|
|
512
|
-
|
|
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(`
|
|
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
|
-
--
|
|
810
|
-
--
|
|
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