@jskit-ai/auth-web 0.1.159 → 0.1.161
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 +29 -135
- package/patterns/auth-surface/PATTERN.md +84 -0
- package/patterns/auth-surface/example/.vibe64/bin/preview-identity +5 -0
- package/src/client/index.js +1 -4
- package/src/client/providers/AuthWebClientProvider.js +32 -60
- package/src/client/runtime/authClient.js +79 -0
- package/src/client/runtime/authGuardRuntime.js +28 -7
- package/src/server/AuthWebFeature.js +41 -0
- package/src/server/managedPreviewIdentity.js +344 -0
- package/src/server/services/AuthWebService.js +4 -16
- package/test/clientBoot.test.js +3 -3
- package/test/clientSurface.test.js +6 -16
- package/test/managedPreviewIdentity.test.js +89 -0
- package/test/packageMetadataOwnership.test.js +3 -15
- package/test/provider.test.js +30 -220
- package/test/providerRuntime.test.js +92 -307
- package/src/client/providers/bootAuthClientProvider.js +0 -49
- package/src/server/providers/AuthRouteServiceProvider.js +0 -31
- package/src/server/providers/AuthWebServiceProvider.js +0 -38
- /package/{templates → patterns/auth-surface/example}/src/pages/auth/login.vue +0 -0
- /package/{templates → patterns/auth-surface/example}/src/pages/auth/reset-password.vue +0 -0
- /package/{templates → patterns/auth-surface/example}/src/pages/auth/signout.vue +0 -0
- /package/{templates → patterns/auth-surface/example}/src/runtime/authGuardRuntime.js +0 -0
- /package/{templates → patterns/auth-surface/example}/src/runtime/authHttpClient.js +0 -0
- /package/{templates → patterns/auth-surface/example}/src/runtime/useSignOut.js +0 -0
- /package/{templates → patterns/auth-surface/example}/src/views/auth/LoginView.vue +0 -0
- /package/{templates → patterns/auth-surface/example}/src/views/auth/ResetPasswordView.vue +0 -0
- /package/{templates → patterns/auth-surface/example}/src/views/auth/SignOutView.vue +0 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { DEV_AUTH_SECRET_HEADER } from "@jskit-ai/auth-core/server/devAuth";
|
|
2
|
+
import { AUTH_PATHS } from "@jskit-ai/auth-core/shared/authPaths";
|
|
3
|
+
|
|
4
|
+
const MANAGED_PREVIEW_IDENTITY_PROTOCOL = "vibe64.preview-identity.command.v1";
|
|
5
|
+
const MANAGED_PREVIEW_IDENTITY_ENABLED_ENV = "VIBE64_PREVIEW_IDENTITY_ENABLED";
|
|
6
|
+
const MANAGED_PREVIEW_IDENTITY_SECRET_ENV = "VIBE64_PREVIEW_IDENTITY_SECRET";
|
|
7
|
+
const MAX_MESSAGE_BYTES = 64 * 1024;
|
|
8
|
+
|
|
9
|
+
function commandError(message, code = "jskit_managed_preview_identity_failed", details = {}) {
|
|
10
|
+
return Object.assign(new Error(message), { code, ...details });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function response(requestId, values) {
|
|
14
|
+
return {
|
|
15
|
+
protocol: MANAGED_PREVIEW_IDENTITY_PROTOCOL,
|
|
16
|
+
requestId: String(requestId || ""),
|
|
17
|
+
...values
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function failure(requestId, error) {
|
|
22
|
+
return response(requestId, {
|
|
23
|
+
code: String(error?.code || "jskit_managed_preview_identity_failed"),
|
|
24
|
+
error: String(error?.message || error || "Managed preview identity failed."),
|
|
25
|
+
ok: false,
|
|
26
|
+
setCookie: Array.isArray(error?.setCookie) ? error.setCookie : [],
|
|
27
|
+
signedOut: error?.signedOut === true,
|
|
28
|
+
statusCode: Number.isInteger(Number(error?.statusCode)) ? Number(error.statusCode) : 400
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function readBoundedStream(stream, label) {
|
|
33
|
+
let bytes = 0;
|
|
34
|
+
const chunks = [];
|
|
35
|
+
for await (const chunk of stream) {
|
|
36
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
37
|
+
bytes += buffer.length;
|
|
38
|
+
if (bytes > MAX_MESSAGE_BYTES) {
|
|
39
|
+
throw commandError(
|
|
40
|
+
`${label} is too large.`,
|
|
41
|
+
"jskit_managed_preview_identity_message_too_large",
|
|
42
|
+
{ statusCode: 413 }
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
chunks.push(buffer);
|
|
46
|
+
}
|
|
47
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function readCommandInput(stream) {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(await readBoundedStream(stream, "Managed preview identity request"));
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error?.code) {
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
throw commandError(
|
|
58
|
+
"Managed preview identity request is invalid JSON.",
|
|
59
|
+
"jskit_managed_preview_identity_request_invalid"
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function localTargetOrigin(value) {
|
|
65
|
+
let target;
|
|
66
|
+
try {
|
|
67
|
+
target = new URL(String(value || ""));
|
|
68
|
+
} catch {
|
|
69
|
+
throw commandError(
|
|
70
|
+
"Managed preview identity target must be a local application.",
|
|
71
|
+
"jskit_managed_preview_identity_target_invalid"
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const hostname = target.hostname.toLowerCase().replace(/^\[|\]$/gu, "");
|
|
75
|
+
const local = hostname === "localhost" ||
|
|
76
|
+
hostname.endsWith(".localhost") ||
|
|
77
|
+
/^127(?:\.\d{1,3}){3}$/u.test(hostname) ||
|
|
78
|
+
hostname === "::1" ||
|
|
79
|
+
/^vibe64-launch-[a-f0-9]{12}$/u.test(hostname);
|
|
80
|
+
if (target.protocol !== "http:" || !local) {
|
|
81
|
+
throw commandError(
|
|
82
|
+
"Managed preview identity target must be a local application.",
|
|
83
|
+
"jskit_managed_preview_identity_target_invalid"
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return target.origin;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizeRequest(value) {
|
|
90
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
91
|
+
throw commandError(
|
|
92
|
+
"Managed preview identity request must be an object.",
|
|
93
|
+
"jskit_managed_preview_identity_request_invalid"
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const requestId = String(value.requestId || "").trim();
|
|
97
|
+
const operation = String(value.operation || "").trim();
|
|
98
|
+
if (value.protocol !== MANAGED_PREVIEW_IDENTITY_PROTOCOL || !requestId) {
|
|
99
|
+
throw commandError(
|
|
100
|
+
"Managed preview identity request protocol is invalid.",
|
|
101
|
+
"jskit_managed_preview_identity_protocol_invalid"
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (!["login-as", "logout"].includes(operation)) {
|
|
105
|
+
throw commandError(
|
|
106
|
+
"Managed preview identity operation is invalid.",
|
|
107
|
+
"jskit_managed_preview_identity_operation_invalid"
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
operation,
|
|
112
|
+
requestId,
|
|
113
|
+
subject: value.subject,
|
|
114
|
+
targetOrigin: localTargetOrigin(value.target?.origin || value.target?.href)
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function identityFromSubject(subject) {
|
|
119
|
+
if (subject?.kind === "selector") {
|
|
120
|
+
const type = String(subject.selector?.type || "").trim();
|
|
121
|
+
const value = String(subject.selector?.value || "").trim();
|
|
122
|
+
if (type === "email" && value) {
|
|
123
|
+
return { email: value };
|
|
124
|
+
}
|
|
125
|
+
if (type === "user-id" && value) {
|
|
126
|
+
return { userId: value };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
throw commandError(
|
|
130
|
+
"Managed preview identity requires an existing application email or user ID.",
|
|
131
|
+
"jskit_managed_preview_identity_selector_unsupported"
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function responseCookies(fetchResponse) {
|
|
136
|
+
if (typeof fetchResponse?.headers?.getSetCookie === "function") {
|
|
137
|
+
return fetchResponse.headers.getSetCookie().map(String).filter(Boolean);
|
|
138
|
+
}
|
|
139
|
+
const value = String(fetchResponse?.headers?.get?.("set-cookie") || "").trim();
|
|
140
|
+
return value ? [value] : [];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function cookieHeader(setCookie) {
|
|
144
|
+
const cookies = new Map();
|
|
145
|
+
for (const entry of setCookie) {
|
|
146
|
+
const pair = String(entry || "").split(";", 1)[0].trim();
|
|
147
|
+
const separator = pair.indexOf("=");
|
|
148
|
+
if (separator > 0) {
|
|
149
|
+
cookies.set(pair.slice(0, separator).trim(), pair);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return [...cookies.values()].join("; ");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function responsePayload(fetchResponse) {
|
|
156
|
+
const text = fetchResponse?.body
|
|
157
|
+
? await readBoundedStream(fetchResponse.body, "Managed preview identity response")
|
|
158
|
+
: "";
|
|
159
|
+
if (!text) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const value = JSON.parse(text);
|
|
164
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function rejected(payload, fetchResponse, details = {}) {
|
|
171
|
+
const fieldErrors = payload?.details?.fieldErrors || payload?.fieldErrors || {};
|
|
172
|
+
const fieldMessage = Object.values(
|
|
173
|
+
fieldErrors && typeof fieldErrors === "object" && !Array.isArray(fieldErrors) ? fieldErrors : {}
|
|
174
|
+
).find(Boolean);
|
|
175
|
+
const firstError = Array.isArray(payload?.errors) ? payload.errors.find(Boolean) : null;
|
|
176
|
+
return commandError(
|
|
177
|
+
String(
|
|
178
|
+
fieldMessage ||
|
|
179
|
+
firstError?.message ||
|
|
180
|
+
firstError ||
|
|
181
|
+
payload?.error ||
|
|
182
|
+
payload?.message ||
|
|
183
|
+
"Managed preview identity exchange failed."
|
|
184
|
+
),
|
|
185
|
+
String(firstError?.code || payload?.code || "jskit_managed_preview_identity_rejected"),
|
|
186
|
+
{ statusCode: Number(fetchResponse?.status || 502), ...details }
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function fetchRequest(fetchImpl, href, options) {
|
|
191
|
+
try {
|
|
192
|
+
return await fetchImpl(href, options);
|
|
193
|
+
} catch {
|
|
194
|
+
throw commandError(
|
|
195
|
+
"Managed preview identity could not reach the application.",
|
|
196
|
+
"jskit_managed_preview_identity_unreachable",
|
|
197
|
+
{ statusCode: 502 }
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function postJson(fetchImpl, href, body, headers = {}) {
|
|
203
|
+
return fetchRequest(fetchImpl, href, {
|
|
204
|
+
body: JSON.stringify(body),
|
|
205
|
+
headers: { "content-type": "application/json", ...headers },
|
|
206
|
+
method: "POST",
|
|
207
|
+
redirect: "manual"
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function bootstrapSession(fetchImpl, targetOrigin) {
|
|
212
|
+
const fetchResponse = await fetchRequest(fetchImpl, `${targetOrigin}${AUTH_PATHS.SESSION}`, {
|
|
213
|
+
method: "GET",
|
|
214
|
+
redirect: "manual"
|
|
215
|
+
});
|
|
216
|
+
const payload = await responsePayload(fetchResponse);
|
|
217
|
+
const setCookie = responseCookies(fetchResponse);
|
|
218
|
+
if (!fetchResponse.ok) {
|
|
219
|
+
throw rejected(payload, fetchResponse, { setCookie });
|
|
220
|
+
}
|
|
221
|
+
const csrfToken = String(payload?.csrfToken || "").trim();
|
|
222
|
+
if (!csrfToken) {
|
|
223
|
+
throw commandError(
|
|
224
|
+
"Session bootstrap did not return a CSRF token.",
|
|
225
|
+
"jskit_managed_preview_identity_csrf_missing",
|
|
226
|
+
{ setCookie, statusCode: 502 }
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return { csrfToken, setCookie };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function logout(fetchImpl, targetOrigin, session) {
|
|
233
|
+
const fetchResponse = await postJson(fetchImpl, `${targetOrigin}${AUTH_PATHS.LOGOUT}`, {}, {
|
|
234
|
+
cookie: cookieHeader(session.setCookie),
|
|
235
|
+
"csrf-token": session.csrfToken
|
|
236
|
+
});
|
|
237
|
+
const payload = await responsePayload(fetchResponse);
|
|
238
|
+
const setCookie = [...session.setCookie, ...responseCookies(fetchResponse)];
|
|
239
|
+
if (!fetchResponse.ok || payload?.ok !== true) {
|
|
240
|
+
throw rejected(payload, fetchResponse, { setCookie, signedOut: false });
|
|
241
|
+
}
|
|
242
|
+
return { csrfToken: session.csrfToken, setCookie };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function login(fetchImpl, targetOrigin, identity, secret, session) {
|
|
246
|
+
const fetchResponse = await postJson(fetchImpl, `${targetOrigin}${AUTH_PATHS.DEV_LOGIN_AS}`, identity, {
|
|
247
|
+
cookie: cookieHeader(session.setCookie),
|
|
248
|
+
"csrf-token": session.csrfToken,
|
|
249
|
+
[DEV_AUTH_SECRET_HEADER]: secret
|
|
250
|
+
});
|
|
251
|
+
const payload = await responsePayload(fetchResponse);
|
|
252
|
+
const setCookie = [...session.setCookie, ...responseCookies(fetchResponse)];
|
|
253
|
+
if (!fetchResponse.ok || payload?.ok !== true) {
|
|
254
|
+
throw rejected(payload, fetchResponse, { setCookie, signedOut: true });
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
identity: {
|
|
258
|
+
displayName: String(payload.displayName || payload.username || "").trim(),
|
|
259
|
+
email: String(payload.email || identity.email || "").trim().toLowerCase(),
|
|
260
|
+
userId: String(payload.userId || identity.userId || "").trim(),
|
|
261
|
+
username: String(payload.username || "").trim()
|
|
262
|
+
},
|
|
263
|
+
setCookie
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function executeManagedPreviewIdentityRequest(value, {
|
|
268
|
+
env = process.env,
|
|
269
|
+
fetchImpl = globalThis.fetch
|
|
270
|
+
} = {}) {
|
|
271
|
+
let requestId = String(value?.requestId || "").trim();
|
|
272
|
+
try {
|
|
273
|
+
const request = normalizeRequest(value);
|
|
274
|
+
requestId = request.requestId;
|
|
275
|
+
if (typeof fetchImpl !== "function") {
|
|
276
|
+
throw commandError(
|
|
277
|
+
"Managed preview identity requires fetch support.",
|
|
278
|
+
"jskit_managed_preview_identity_fetch_unavailable",
|
|
279
|
+
{ statusCode: 500 }
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
if (String(env[MANAGED_PREVIEW_IDENTITY_ENABLED_ENV] || "").trim().toLowerCase() !== "true") {
|
|
283
|
+
throw commandError(
|
|
284
|
+
"Managed preview identity is not enabled.",
|
|
285
|
+
"jskit_managed_preview_identity_disabled",
|
|
286
|
+
{ statusCode: 403 }
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
const secret = String(env[MANAGED_PREVIEW_IDENTITY_SECRET_ENV] || "").trim();
|
|
290
|
+
if (!/^[a-f0-9]{64}$/u.test(secret)) {
|
|
291
|
+
throw commandError(
|
|
292
|
+
"Managed preview identity secret is unavailable.",
|
|
293
|
+
"jskit_managed_preview_identity_secret_missing",
|
|
294
|
+
{ statusCode: 500 }
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
const identity = request.operation === "login-as"
|
|
298
|
+
? identityFromSubject(request.subject)
|
|
299
|
+
: null;
|
|
300
|
+
const session = await bootstrapSession(fetchImpl, request.targetOrigin);
|
|
301
|
+
const signedOutSession = await logout(fetchImpl, request.targetOrigin, session);
|
|
302
|
+
if (request.operation === "logout") {
|
|
303
|
+
return response(requestId, {
|
|
304
|
+
identity: null,
|
|
305
|
+
ok: true,
|
|
306
|
+
setCookie: signedOutSession.setCookie,
|
|
307
|
+
signedOut: true
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
const result = await login(fetchImpl, request.targetOrigin, identity, secret, signedOutSession);
|
|
311
|
+
return response(requestId, {
|
|
312
|
+
identity: result.identity,
|
|
313
|
+
ok: true,
|
|
314
|
+
setCookie: result.setCookie,
|
|
315
|
+
signedOut: false
|
|
316
|
+
});
|
|
317
|
+
} catch (error) {
|
|
318
|
+
return failure(requestId, error);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function runManagedPreviewIdentityCommand({
|
|
323
|
+
env = process.env,
|
|
324
|
+
fetchImpl = globalThis.fetch,
|
|
325
|
+
stdin = process.stdin,
|
|
326
|
+
stdout = process.stdout
|
|
327
|
+
} = {}) {
|
|
328
|
+
let request;
|
|
329
|
+
try {
|
|
330
|
+
request = await readCommandInput(stdin);
|
|
331
|
+
} catch (error) {
|
|
332
|
+
stdout.write(`${JSON.stringify(failure("", error))}\n`);
|
|
333
|
+
return 0;
|
|
334
|
+
}
|
|
335
|
+
const result = await executeManagedPreviewIdentityRequest(request, { env, fetchImpl });
|
|
336
|
+
stdout.write(`${JSON.stringify(result)}\n`);
|
|
337
|
+
return 0;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export {
|
|
341
|
+
MANAGED_PREVIEW_IDENTITY_PROTOCOL,
|
|
342
|
+
executeManagedPreviewIdentityRequest,
|
|
343
|
+
runManagedPreviewIdentityCommand
|
|
344
|
+
};
|
|
@@ -2,12 +2,11 @@ import { normalizeAuthCapabilities } from "@jskit-ai/auth-core/shared/authCapabi
|
|
|
2
2
|
import { AUTH_ACTION_IDS } from "../constants/authActionIds.js";
|
|
3
3
|
|
|
4
4
|
class AuthWebService {
|
|
5
|
-
constructor({ authService,
|
|
6
|
-
if (!authService
|
|
7
|
-
throw new Error("authService
|
|
5
|
+
constructor({ authService, devAuthBootstrapEnabled } = {}) {
|
|
6
|
+
if (!authService || typeof authService !== "object") {
|
|
7
|
+
throw new Error("authService is required.");
|
|
8
8
|
}
|
|
9
|
-
this.authService = authService
|
|
10
|
-
this.getAuthService = typeof getAuthService === "function" ? getAuthService : null;
|
|
9
|
+
this.authService = authService;
|
|
11
10
|
this.devAuthBootstrapEnabled =
|
|
12
11
|
typeof devAuthBootstrapEnabled === "boolean" ? devAuthBootstrapEnabled : null;
|
|
13
12
|
}
|
|
@@ -73,17 +72,6 @@ class AuthWebService {
|
|
|
73
72
|
}
|
|
74
73
|
|
|
75
74
|
resolveAuthService() {
|
|
76
|
-
if (this.authService) {
|
|
77
|
-
return this.authService;
|
|
78
|
-
}
|
|
79
|
-
if (typeof this.getAuthService !== "function") {
|
|
80
|
-
throw new Error("authService is required.");
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
this.authService = this.getAuthService();
|
|
84
|
-
if (!this.authService) {
|
|
85
|
-
throw new Error("authService is required.");
|
|
86
|
-
}
|
|
87
75
|
return this.authService;
|
|
88
76
|
}
|
|
89
77
|
|
package/test/clientBoot.test.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import test from "node:test";
|
|
5
5
|
|
|
6
|
-
test("auth-web client index
|
|
6
|
+
test("auth-web client index exports its routes and declarative provider", () => {
|
|
7
7
|
const source = readFileSync(fileURLToPath(new URL("../src/client/index.js", import.meta.url)), "utf8");
|
|
8
8
|
|
|
9
9
|
assert.equal(source.includes('export { useAuthStore } from "./stores/useAuthStore.js";'), true);
|
|
@@ -14,7 +14,7 @@ test("auth-web client index defines provider-based client routes surface", () =>
|
|
|
14
14
|
assert.equal(source.includes('"auth-login": DefaultLoginView'), true);
|
|
15
15
|
assert.equal(source.includes('"auth-signout": DefaultSignOutView'), true);
|
|
16
16
|
assert.equal(source.includes('"auth-default-login": DefaultLoginView'), true);
|
|
17
|
-
assert.equal(source.includes(
|
|
17
|
+
assert.equal(source.includes('export { AuthWebClientProvider } from "./providers/AuthWebClientProvider.js";'), true);
|
|
18
18
|
assert.equal(source.includes("async function bootClient(context) {"), false);
|
|
19
|
-
assert.equal(source.includes("export { routeComponents
|
|
19
|
+
assert.equal(source.includes("export { routeComponents };"), true);
|
|
20
20
|
});
|
|
@@ -25,19 +25,6 @@ test("auth-web packageMetadata declares auth surface ui routes", () => {
|
|
|
25
25
|
assert.equal(resetRoute?.autoRegister, false);
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
test("auth-web auth page templates declare public route guard", () => {
|
|
29
|
-
const loginTemplatePath = fileURLToPath(new URL("../templates/src/pages/auth/login.vue", import.meta.url));
|
|
30
|
-
const signOutTemplatePath = fileURLToPath(new URL("../templates/src/pages/auth/signout.vue", import.meta.url));
|
|
31
|
-
const resetTemplatePath = fileURLToPath(new URL("../templates/src/pages/auth/reset-password.vue", import.meta.url));
|
|
32
|
-
const loginTemplateSource = readFileSync(loginTemplatePath, "utf8");
|
|
33
|
-
const signOutTemplateSource = readFileSync(signOutTemplatePath, "utf8");
|
|
34
|
-
const resetTemplateSource = readFileSync(resetTemplatePath, "utf8");
|
|
35
|
-
|
|
36
|
-
assert.match(loginTemplateSource, /"guard"\s*:\s*\{\s*"policy"\s*:\s*"public"\s*\}/);
|
|
37
|
-
assert.match(signOutTemplateSource, /"guard"\s*:\s*\{\s*"policy"\s*:\s*"public"\s*\}/);
|
|
38
|
-
assert.match(resetTemplateSource, /"guard"\s*:\s*\{\s*"policy"\s*:\s*"public"\s*\}/);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
28
|
test("auth-web exports runtime signout helpers directly", () => {
|
|
42
29
|
assert.equal(typeof fromRuntimeUseSignOut, "function");
|
|
43
30
|
assert.equal(typeof fromRuntimeCreateSignOutAction, "function");
|
|
@@ -72,12 +59,15 @@ test("auth-web runtime/useLoginView composes login view state, validation, and a
|
|
|
72
59
|
assert.match(runtimeUseLoginViewSource, /export\s+\{\s*useLoginView\s*\};/);
|
|
73
60
|
});
|
|
74
61
|
|
|
75
|
-
test("auth-web client
|
|
62
|
+
test("auth-web client capability includes mobile callback completion", () => {
|
|
76
63
|
const providerPath = fileURLToPath(new URL("../src/client/providers/AuthWebClientProvider.js", import.meta.url));
|
|
64
|
+
const authClientPath = fileURLToPath(new URL("../src/client/runtime/authClient.js", import.meta.url));
|
|
77
65
|
const providerSource = readFileSync(providerPath, "utf8");
|
|
66
|
+
const authClientSource = readFileSync(authClientPath, "utf8");
|
|
78
67
|
|
|
79
|
-
assert.match(providerSource, /auth\.
|
|
80
|
-
assert.match(
|
|
68
|
+
assert.match(providerSource, /auth: "client\.auth"/);
|
|
69
|
+
assert.match(authClientSource, /mobileCallback/);
|
|
70
|
+
assert.match(authClientSource, /completeOAuthCallbackFromUrl/);
|
|
81
71
|
});
|
|
82
72
|
|
|
83
73
|
test("auth profile activator preserves the generated tap-target contract", () => {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
MANAGED_PREVIEW_IDENTITY_PROTOCOL,
|
|
6
|
+
executeManagedPreviewIdentityRequest
|
|
7
|
+
} from "../src/server/managedPreviewIdentity.js";
|
|
8
|
+
|
|
9
|
+
const SECRET = "a".repeat(64);
|
|
10
|
+
|
|
11
|
+
function jsonResponse(payload, { cookie = "", status = 200 } = {}) {
|
|
12
|
+
return new Response(JSON.stringify(payload), {
|
|
13
|
+
headers: {
|
|
14
|
+
"content-type": "application/json",
|
|
15
|
+
...(cookie ? { "set-cookie": cookie } : {})
|
|
16
|
+
},
|
|
17
|
+
status
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test("managed preview identity signs out before selecting an existing application user", async () => {
|
|
22
|
+
const calls = [];
|
|
23
|
+
const responses = [
|
|
24
|
+
jsonResponse({ csrfToken: "csrf-token" }, { cookie: "session=initial; Path=/" }),
|
|
25
|
+
jsonResponse({ ok: true }, { cookie: "session=; Max-Age=0; Path=/" }),
|
|
26
|
+
jsonResponse({
|
|
27
|
+
displayName: "Ada Lovelace",
|
|
28
|
+
email: "ada@example.com",
|
|
29
|
+
ok: true,
|
|
30
|
+
userId: "user-1"
|
|
31
|
+
}, { cookie: "session=selected; Path=/" })
|
|
32
|
+
];
|
|
33
|
+
const result = await executeManagedPreviewIdentityRequest({
|
|
34
|
+
operation: "login-as",
|
|
35
|
+
protocol: MANAGED_PREVIEW_IDENTITY_PROTOCOL,
|
|
36
|
+
requestId: "request-1",
|
|
37
|
+
subject: {
|
|
38
|
+
kind: "selector",
|
|
39
|
+
selector: { type: "email", value: "ada@example.com" }
|
|
40
|
+
},
|
|
41
|
+
target: { origin: "http://vibe64-launch-deadbeefcafe" }
|
|
42
|
+
}, {
|
|
43
|
+
env: {
|
|
44
|
+
VIBE64_PREVIEW_IDENTITY_ENABLED: "true",
|
|
45
|
+
VIBE64_PREVIEW_IDENTITY_SECRET: SECRET
|
|
46
|
+
},
|
|
47
|
+
fetchImpl: async (href, options) => {
|
|
48
|
+
calls.push({ href, options });
|
|
49
|
+
return responses.shift();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
assert.equal(result.ok, true);
|
|
54
|
+
assert.equal(result.identity.email, "ada@example.com");
|
|
55
|
+
assert.equal(result.signedOut, false);
|
|
56
|
+
assert.deepEqual(calls.map(({ href }) => new URL(href).pathname), [
|
|
57
|
+
"/api/session",
|
|
58
|
+
"/api/logout",
|
|
59
|
+
"/api/dev-auth/login-as"
|
|
60
|
+
]);
|
|
61
|
+
assert.equal(calls[2].options.headers["x-jskit-dev-auth-secret"], SECRET);
|
|
62
|
+
assert.equal(JSON.parse(calls[2].options.body).email, "ada@example.com");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("managed preview identity rejects a disabled or non-local exchange", async () => {
|
|
66
|
+
const disabled = await executeManagedPreviewIdentityRequest({
|
|
67
|
+
operation: "logout",
|
|
68
|
+
protocol: MANAGED_PREVIEW_IDENTITY_PROTOCOL,
|
|
69
|
+
requestId: "request-2",
|
|
70
|
+
target: { origin: "http://localhost:3000" }
|
|
71
|
+
}, { env: {}, fetchImpl: async () => assert.fail("fetch must not run") });
|
|
72
|
+
assert.equal(disabled.ok, false);
|
|
73
|
+
assert.equal(disabled.code, "jskit_managed_preview_identity_disabled");
|
|
74
|
+
|
|
75
|
+
const remote = await executeManagedPreviewIdentityRequest({
|
|
76
|
+
operation: "logout",
|
|
77
|
+
protocol: MANAGED_PREVIEW_IDENTITY_PROTOCOL,
|
|
78
|
+
requestId: "request-3",
|
|
79
|
+
target: { origin: "https://example.com" }
|
|
80
|
+
}, {
|
|
81
|
+
env: {
|
|
82
|
+
VIBE64_PREVIEW_IDENTITY_ENABLED: "true",
|
|
83
|
+
VIBE64_PREVIEW_IDENTITY_SECRET: SECRET
|
|
84
|
+
},
|
|
85
|
+
fetchImpl: async () => assert.fail("fetch must not run")
|
|
86
|
+
});
|
|
87
|
+
assert.equal(remote.ok, false);
|
|
88
|
+
assert.equal(remote.code, "jskit_managed_preview_identity_target_invalid");
|
|
89
|
+
});
|
|
@@ -4,19 +4,7 @@ import packageJson from "../package.json" with { type: "json" };
|
|
|
4
4
|
|
|
5
5
|
const packageMetadata = packageJson.jskit;
|
|
6
6
|
|
|
7
|
-
test("auth-web
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"auth-view-signout",
|
|
11
|
-
"auth-view-reset-password",
|
|
12
|
-
"auth-page-login",
|
|
13
|
-
"auth-page-signout",
|
|
14
|
-
"auth-page-reset-password"
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
for (const id of expectedIds) {
|
|
18
|
-
const mutation = packageMetadata.mutations.files.find((entry) => entry.id === id);
|
|
19
|
-
assert.ok(mutation, `Missing auth-web scaffold mutation ${id}.`);
|
|
20
|
-
assert.equal(mutation.ownership, "app", `${id} must remain app-owned across package updates.`);
|
|
21
|
-
}
|
|
7
|
+
test("auth-web ships reusable views without mutating application source", () => {
|
|
8
|
+
assert.equal(Object.hasOwn(packageMetadata, "mutations"), false);
|
|
9
|
+
assert.equal(Object.keys(packageJson.exports).some((subpath) => subpath.includes("templates")), false);
|
|
22
10
|
});
|