@jskit-ai/jskit-cli 0.2.143 → 0.2.144

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/jskit-cli",
3
- "version": "0.2.143",
3
+ "version": "0.2.144",
4
4
  "description": "Bundle and package orchestration CLI for JSKIT apps.",
5
5
  "type": "module",
6
6
  "files": [
@@ -10,6 +10,7 @@ import {
10
10
  } from "./appCommandCatalog.js";
11
11
  import { runAppAdoptManagedScriptsCommand } from "./appCommands/adoptManagedScripts.js";
12
12
  import { runAppMigrateSourceMutationsCommand } from "./appCommands/migrateSourceMutations.js";
13
+ import { runAppPreviewIdentityCommand } from "./appCommands/previewIdentity.js";
13
14
  import { runAppReleaseCommand } from "./appCommands/release.js";
14
15
  import { runAppSyncCiCommand } from "./appCommands/syncCi.js";
15
16
  import { runAppUpdatePackagesCommand } from "./appCommands/updatePackages.js";
@@ -83,7 +84,7 @@ function createAppCommands(ctx = {}) {
83
84
  resolveAppRootFromCwd
84
85
  } = ctx;
85
86
 
86
- async function commandApp({ positional = [], options = {}, cwd = "", stdout, stderr }) {
87
+ async function commandApp({ positional = [], options = {}, cwd = "", stdout, stderr, io = {} }) {
87
88
  const appRoot = await resolveAppRootFromCwd(cwd);
88
89
  const firstToken = String(positional[0] || "").trim();
89
90
  const secondToken = String(positional[1] || "").trim();
@@ -137,6 +138,12 @@ function createAppCommands(ctx = {}) {
137
138
  if (definition.name === "verify") {
138
139
  return runAppVerifyCommand(ctx, { appRoot, options, stdout, stderr });
139
140
  }
141
+ if (definition.name === "preview-identity") {
142
+ return runAppPreviewIdentityCommand(ctx, {
143
+ stdin: io.stdin,
144
+ stdout
145
+ });
146
+ }
140
147
  if (definition.name === "verify-ui") {
141
148
  return runAppVerifyUiCommand(ctx, { appRoot, options, stdout, stderr });
142
149
  }
@@ -22,6 +22,17 @@ const COPIED_APP_SCRIPT_FILES = Object.freeze([
22
22
  ]);
23
23
 
24
24
  const APP_COMMAND_DEFINITIONS = Object.freeze({
25
+ "preview-identity": Object.freeze({
26
+ name: "preview-identity",
27
+ summary: "Run the app-owned Vibe64 preview-identity command protocol.",
28
+ usage: "jskit app preview-identity",
29
+ options: Object.freeze([]),
30
+ defaults: Object.freeze([
31
+ "Reads one versioned preview-identity request from stdin and writes one JSON response to stdout.",
32
+ "Uses the running local JSKIT app to create a native session for an existing user only.",
33
+ "This machine command is enabled only by the managed development-preview environment."
34
+ ])
35
+ }),
25
36
  verify: Object.freeze({
26
37
  name: "verify",
27
38
  summary: "Run the JSKIT baseline app verification flow.",
@@ -0,0 +1,420 @@
1
+ const PREVIEW_IDENTITY_PROTOCOL = "vibe64.preview-identity.command.v1";
2
+ const PREVIEW_IDENTITY_LOGIN_OPERATION = "login-as";
3
+ const PREVIEW_IDENTITY_LOGOUT_OPERATION = "logout";
4
+ const PREVIEW_IDENTITY_INPUT_LIMIT_BYTES = 64 * 1024;
5
+ const PREVIEW_IDENTITY_RESPONSE_LIMIT_BYTES = 64 * 1024;
6
+ const PREVIEW_IDENTITY_ENABLED_ENV = "VIBE64_PREVIEW_IDENTITY_ENABLED";
7
+ const PREVIEW_IDENTITY_SECRET_ENV = "VIBE64_PREVIEW_IDENTITY_SECRET";
8
+ const DEV_AUTH_SECRET_HEADER = "x-jskit-dev-auth-secret";
9
+ const AUTH_PATHS = Object.freeze({
10
+ devLoginAs: "/api/dev-auth/login-as",
11
+ logout: "/api/logout",
12
+ session: "/api/session"
13
+ });
14
+
15
+ function previewIdentityResponse(requestId = "", values = {}) {
16
+ return {
17
+ protocol: PREVIEW_IDENTITY_PROTOCOL,
18
+ requestId: String(requestId || ""),
19
+ ...values
20
+ };
21
+ }
22
+
23
+ function previewIdentityFailure(requestId = "", error = {}) {
24
+ return previewIdentityResponse(requestId, {
25
+ code: String(error.code || "jskit_preview_identity_failed"),
26
+ error: String(error.message || error || "JSKIT preview identity failed."),
27
+ ok: false,
28
+ setCookie: Array.isArray(error.setCookie) ? error.setCookie : [],
29
+ signedOut: error.signedOut === true,
30
+ statusCode: Number.isInteger(Number(error.statusCode)) ? Number(error.statusCode) : 400
31
+ });
32
+ }
33
+
34
+ function commandError(message = "", code = "jskit_preview_identity_failed", extra = {}) {
35
+ const error = new Error(message || "JSKIT preview identity failed.");
36
+ error.code = code;
37
+ Object.assign(error, extra);
38
+ return error;
39
+ }
40
+
41
+ async function readInput(stream) {
42
+ let bytes = 0;
43
+ const chunks = [];
44
+ for await (const chunk of stream) {
45
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
46
+ bytes += buffer.length;
47
+ if (bytes > PREVIEW_IDENTITY_INPUT_LIMIT_BYTES) {
48
+ throw commandError(
49
+ "Preview identity request is too large.",
50
+ "jskit_preview_identity_request_too_large",
51
+ { statusCode: 413 }
52
+ );
53
+ }
54
+ chunks.push(buffer);
55
+ }
56
+ try {
57
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
58
+ } catch {
59
+ throw commandError(
60
+ "Preview identity request is invalid JSON.",
61
+ "jskit_preview_identity_request_invalid"
62
+ );
63
+ }
64
+ }
65
+
66
+ function normalizeRequest(value = {}) {
67
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
68
+ throw commandError(
69
+ "Preview identity request must be an object.",
70
+ "jskit_preview_identity_request_invalid"
71
+ );
72
+ }
73
+ const requestId = String(value.requestId || "").trim();
74
+ const operation = String(value.operation || "").trim();
75
+ if (value.protocol !== PREVIEW_IDENTITY_PROTOCOL || !requestId) {
76
+ throw commandError(
77
+ "Preview identity request protocol is invalid.",
78
+ "jskit_preview_identity_protocol_invalid"
79
+ );
80
+ }
81
+ if (![PREVIEW_IDENTITY_LOGIN_OPERATION, PREVIEW_IDENTITY_LOGOUT_OPERATION].includes(operation)) {
82
+ throw commandError(
83
+ "Preview identity operation is invalid.",
84
+ "jskit_preview_identity_operation_invalid"
85
+ );
86
+ }
87
+ let target;
88
+ try {
89
+ target = new URL(String(value.target?.origin || value.target?.href || ""));
90
+ } catch {
91
+ throw commandError(
92
+ "Preview identity target must be a local JSKIT application.",
93
+ "jskit_preview_identity_target_invalid"
94
+ );
95
+ }
96
+ const hostname = target.hostname.toLowerCase().replace(/^\[|\]$/gu, "");
97
+ if (
98
+ target.protocol !== "http:" ||
99
+ !(
100
+ hostname === "localhost" ||
101
+ hostname.endsWith(".localhost") ||
102
+ /^127(?:\.\d{1,3}){3}$/u.test(hostname) ||
103
+ hostname === "::1" ||
104
+ /^vibe64-launch-[a-f0-9]{12}$/u.test(hostname)
105
+ )
106
+ ) {
107
+ throw commandError(
108
+ "Preview identity target must be a local JSKIT application.",
109
+ "jskit_preview_identity_target_invalid"
110
+ );
111
+ }
112
+ return {
113
+ operation,
114
+ requestId,
115
+ subject: value.subject,
116
+ targetOrigin: target.origin
117
+ };
118
+ }
119
+
120
+ function identityFromSubject(subject = {}) {
121
+ const source = subject && typeof subject === "object" && !Array.isArray(subject) ? subject : {};
122
+ if (source.kind === "selector") {
123
+ const type = String(source.selector?.type || "").trim();
124
+ const value = String(source.selector?.value || "").trim();
125
+ if (type === "email" && value) {
126
+ return { email: value };
127
+ }
128
+ if (type === "user-id" && value) {
129
+ return { userId: value };
130
+ }
131
+ }
132
+ if (source.kind === "viewer") {
133
+ const email = (Array.isArray(source.identifiers) ? source.identifiers : [])
134
+ .find((identifier) => identifier?.type === "email" && String(identifier.value || "").trim());
135
+ if (email) {
136
+ return { email: String(email.value).trim() };
137
+ }
138
+ }
139
+ throw commandError(
140
+ "JSKIT preview identity requires an existing application email or user ID.",
141
+ "jskit_preview_identity_selector_unsupported"
142
+ );
143
+ }
144
+
145
+ function responseSetCookie(response = {}) {
146
+ if (typeof response.headers?.getSetCookie === "function") {
147
+ return response.headers.getSetCookie().map(String).filter(Boolean);
148
+ }
149
+ const value = String(response.headers?.get?.("set-cookie") || "").trim();
150
+ return value ? [value] : [];
151
+ }
152
+
153
+ function cookieHeader(setCookie = []) {
154
+ const cookies = new Map();
155
+ for (const entry of Array.isArray(setCookie) ? setCookie : []) {
156
+ const pair = String(entry || "").split(";", 1)[0].trim();
157
+ const separatorIndex = pair.indexOf("=");
158
+ const name = separatorIndex > 0 ? pair.slice(0, separatorIndex).trim() : "";
159
+ if (name) {
160
+ cookies.set(name, pair);
161
+ }
162
+ }
163
+ return [...cookies.values()].join("; ");
164
+ }
165
+
166
+ async function responsePayload(response = {}) {
167
+ const reader = response.body?.getReader?.();
168
+ let text = "";
169
+ if (reader) {
170
+ const decoder = new TextDecoder();
171
+ let bytes = 0;
172
+ while (true) {
173
+ const { done, value } = await reader.read();
174
+ if (done) {
175
+ break;
176
+ }
177
+ bytes += value.byteLength;
178
+ if (bytes > PREVIEW_IDENTITY_RESPONSE_LIMIT_BYTES) {
179
+ await reader.cancel();
180
+ throw commandError(
181
+ "JSKIT preview identity response is too large.",
182
+ "jskit_preview_identity_response_too_large",
183
+ { statusCode: 502 }
184
+ );
185
+ }
186
+ text += decoder.decode(value, { stream: true });
187
+ }
188
+ text += decoder.decode();
189
+ } else {
190
+ text = await response.text();
191
+ if (Buffer.byteLength(text) > PREVIEW_IDENTITY_RESPONSE_LIMIT_BYTES) {
192
+ throw commandError(
193
+ "JSKIT preview identity response is too large.",
194
+ "jskit_preview_identity_response_too_large",
195
+ { statusCode: 502 }
196
+ );
197
+ }
198
+ }
199
+ if (!text) {
200
+ return null;
201
+ }
202
+ try {
203
+ const payload = JSON.parse(text);
204
+ return payload && typeof payload === "object" && !Array.isArray(payload)
205
+ ? payload
206
+ : null;
207
+ } catch {
208
+ return null;
209
+ }
210
+ }
211
+
212
+ function upstreamError(payload = {}, response = {}, extra = {}) {
213
+ const fieldErrors = payload?.details?.fieldErrors || payload?.fieldErrors || {};
214
+ const fieldMessage = Object.values(
215
+ fieldErrors && typeof fieldErrors === "object" && !Array.isArray(fieldErrors) ? fieldErrors : {}
216
+ ).find(Boolean);
217
+ const firstError = Array.isArray(payload?.errors) ? payload.errors.find(Boolean) : null;
218
+ return commandError(
219
+ String(
220
+ fieldMessage ||
221
+ firstError?.message ||
222
+ firstError ||
223
+ payload?.error ||
224
+ payload?.message ||
225
+ "JSKIT preview identity exchange failed."
226
+ ),
227
+ String(firstError?.code || payload?.code || "jskit_preview_identity_upstream_rejected"),
228
+ {
229
+ ...extra,
230
+ statusCode: Number(response.status || 502)
231
+ }
232
+ );
233
+ }
234
+
235
+ async function request(fetchImpl, href, options) {
236
+ try {
237
+ return await fetchImpl(href, options);
238
+ } catch {
239
+ throw commandError(
240
+ "JSKIT preview identity could not reach the application.",
241
+ "jskit_preview_identity_unreachable",
242
+ { statusCode: 502 }
243
+ );
244
+ }
245
+ }
246
+
247
+ function postJson(fetchImpl, href, body, headers = {}) {
248
+ return request(fetchImpl, href, {
249
+ body: JSON.stringify(body),
250
+ headers: {
251
+ "content-type": "application/json",
252
+ ...headers
253
+ },
254
+ method: "POST",
255
+ redirect: "manual"
256
+ });
257
+ }
258
+
259
+ async function readSession(fetchImpl, targetOrigin) {
260
+ const response = await request(fetchImpl, `${targetOrigin}${AUTH_PATHS.session}`, {
261
+ method: "GET",
262
+ redirect: "manual"
263
+ });
264
+ const payload = await responsePayload(response);
265
+ const responseCookies = responseSetCookie(response);
266
+ if (!response.ok) {
267
+ throw upstreamError(payload, response, {
268
+ setCookie: responseCookies,
269
+ signedOut: false
270
+ });
271
+ }
272
+ const csrfToken = String(payload?.csrfToken || "").trim();
273
+ if (!csrfToken) {
274
+ throw commandError(
275
+ "JSKIT session bootstrap did not return a CSRF token.",
276
+ "jskit_preview_identity_csrf_missing",
277
+ {
278
+ setCookie: responseCookies,
279
+ statusCode: 502
280
+ }
281
+ );
282
+ }
283
+ return {
284
+ csrfToken,
285
+ setCookie: responseCookies
286
+ };
287
+ }
288
+
289
+ async function logout(fetchImpl, targetOrigin, session) {
290
+ const response = await postJson(fetchImpl, `${targetOrigin}${AUTH_PATHS.logout}`, {}, {
291
+ cookie: cookieHeader(session.setCookie),
292
+ "csrf-token": session.csrfToken
293
+ });
294
+ const payload = await responsePayload(response);
295
+ const setCookie = [...session.setCookie, ...responseSetCookie(response)];
296
+ if (!response.ok || payload?.ok !== true) {
297
+ throw upstreamError(payload, response, {
298
+ setCookie,
299
+ signedOut: false
300
+ });
301
+ }
302
+ return {
303
+ csrfToken: session.csrfToken,
304
+ setCookie
305
+ };
306
+ }
307
+
308
+ async function login(fetchImpl, targetOrigin, identity, secret, session) {
309
+ const loginResponse = await postJson(fetchImpl, `${targetOrigin}${AUTH_PATHS.devLoginAs}`, identity, {
310
+ cookie: cookieHeader(session.setCookie),
311
+ "csrf-token": session.csrfToken,
312
+ [DEV_AUTH_SECRET_HEADER]: secret
313
+ });
314
+ const loginPayload = await responsePayload(loginResponse);
315
+ const loginSetCookie = responseSetCookie(loginResponse);
316
+ const setCookie = [...session.setCookie, ...loginSetCookie];
317
+ if (!loginResponse.ok || loginPayload?.ok !== true) {
318
+ throw upstreamError(loginPayload, loginResponse, {
319
+ setCookie,
320
+ signedOut: true
321
+ });
322
+ }
323
+ return {
324
+ identity: {
325
+ displayName: String(loginPayload.displayName || loginPayload.username || "").trim(),
326
+ email: String(loginPayload.email || identity.email || "").trim().toLowerCase(),
327
+ userId: String(loginPayload.userId || identity.userId || "").trim(),
328
+ username: String(loginPayload.username || "").trim()
329
+ },
330
+ setCookie
331
+ };
332
+ }
333
+
334
+ async function executeJskitPreviewIdentityRequest(value = {}, {
335
+ env = process.env,
336
+ fetchImpl = globalThis.fetch
337
+ } = {}) {
338
+ let requestId = String(value?.requestId || "").trim();
339
+ try {
340
+ const request = normalizeRequest(value);
341
+ requestId = request.requestId;
342
+ if (typeof fetchImpl !== "function") {
343
+ throw commandError(
344
+ "JSKIT preview identity requires fetch support.",
345
+ "jskit_preview_identity_fetch_unavailable",
346
+ { statusCode: 500 }
347
+ );
348
+ }
349
+ if (String(env[PREVIEW_IDENTITY_ENABLED_ENV] || "").trim().toLowerCase() !== "true") {
350
+ throw commandError(
351
+ "JSKIT development identity exchange is not enabled.",
352
+ "jskit_preview_identity_disabled",
353
+ { statusCode: 403 }
354
+ );
355
+ }
356
+ const secret = String(env[PREVIEW_IDENTITY_SECRET_ENV] || "").trim();
357
+ if (!/^[a-f0-9]{64}$/u.test(secret)) {
358
+ throw commandError(
359
+ "JSKIT development identity exchange secret is unavailable.",
360
+ "jskit_preview_identity_secret_missing",
361
+ { statusCode: 500 }
362
+ );
363
+ }
364
+ const identity = request.operation === PREVIEW_IDENTITY_LOGIN_OPERATION
365
+ ? identityFromSubject(request.subject)
366
+ : null;
367
+ const session = await readSession(fetchImpl, request.targetOrigin);
368
+ const signedOutSession = await logout(fetchImpl, request.targetOrigin, session);
369
+ if (request.operation === PREVIEW_IDENTITY_LOGOUT_OPERATION) {
370
+ return previewIdentityResponse(requestId, {
371
+ identity: null,
372
+ ok: true,
373
+ setCookie: signedOutSession.setCookie,
374
+ signedOut: true
375
+ });
376
+ }
377
+ const result = await login(
378
+ fetchImpl,
379
+ request.targetOrigin,
380
+ identity,
381
+ secret,
382
+ signedOutSession
383
+ );
384
+ return previewIdentityResponse(requestId, {
385
+ identity: result.identity,
386
+ ok: true,
387
+ setCookie: result.setCookie,
388
+ signedOut: false
389
+ });
390
+ } catch (error) {
391
+ return previewIdentityFailure(requestId, error);
392
+ }
393
+ }
394
+
395
+ async function runAppPreviewIdentityCommand(_ctx = {}, {
396
+ env = process.env,
397
+ fetchImpl = globalThis.fetch,
398
+ stdin = process.stdin,
399
+ stdout = process.stdout
400
+ } = {}) {
401
+ let request;
402
+ try {
403
+ request = await readInput(stdin);
404
+ } catch (error) {
405
+ stdout.write(`${JSON.stringify(previewIdentityFailure("", error))}\n`);
406
+ return 0;
407
+ }
408
+ const response = await executeJskitPreviewIdentityRequest(request, {
409
+ env,
410
+ fetchImpl
411
+ });
412
+ stdout.write(`${JSON.stringify(response)}\n`);
413
+ return 0;
414
+ }
415
+
416
+ export {
417
+ PREVIEW_IDENTITY_PROTOCOL,
418
+ executeJskitPreviewIdentityRequest,
419
+ runAppPreviewIdentityCommand
420
+ };
@@ -13,7 +13,7 @@ function normalizeBaseUrl(value) {
13
13
  function createJskitPlaywrightConfig({ env = process.env } = {}) {
14
14
  const managedBaseUrl = normalizeBaseUrl(env.PLAYWRIGHT_BASE_URL);
15
15
  const baseURL = managedBaseUrl || DEFAULT_LOCAL_BASE_URL;
16
- const storageState = String(env.JSKIT_PLAYWRIGHT_STORAGE_STATE || "").trim();
16
+ const storageState = String(env.VIBE64_PLAYWRIGHT_STORAGE_STATE || "").trim();
17
17
 
18
18
  return {
19
19
  testDir: "./tests/e2e",