visionos-monorepo 0.1.0
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/.claude/worktrees/competent-burnell-8d1330/README.md +138 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/package.json +35 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/scripts/copy-web-assets.mjs +12 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/src/commands/logout.ts +12 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/src/commands/open.ts +19 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/src/commands/start.ts +97 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/src/commands/status.ts +23 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/src/commands/userinfo.ts +47 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/src/index.ts +23 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/src/lib/auth.ts +84 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/src/lib/browser.ts +37 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/src/lib/localState.ts +80 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/src/lib/runtime.ts +203 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/src/runtime/index.ts +36 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/src/types/inquirer.d.ts +9 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/tsconfig.json +19 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/index.html +15 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/package.json +27 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/postcss.config.cjs +7 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/src/App.tsx +57 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/src/components/CliAuthPage.tsx +385 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/src/components/ManifestoPage.tsx +946 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/src/components/TrackCard.tsx +19 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/src/lib/api.ts +58 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/src/main.tsx +11 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/src/styles/index.css +33 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/src/styles/manifesto.css +1398 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/tailwind.config.ts +36 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/tsconfig.json +25 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/vite.config.ts +20 -0
- package/.claude/worktrees/competent-burnell-8d1330/package-lock.json +5278 -0
- package/.claude/worktrees/competent-burnell-8d1330/package.json +24 -0
- package/.claude/worktrees/competent-burnell-8d1330/server/package.json +25 -0
- package/.claude/worktrees/competent-burnell-8d1330/server/src/app.ts +71 -0
- package/.claude/worktrees/competent-burnell-8d1330/server/src/config/env.ts +14 -0
- package/.claude/worktrees/competent-burnell-8d1330/server/src/features/auth/sessionStore.ts +74 -0
- package/.claude/worktrees/competent-burnell-8d1330/server/src/index.ts +8 -0
- package/.claude/worktrees/competent-burnell-8d1330/server/src/routes/auth.ts +112 -0
- package/.claude/worktrees/competent-burnell-8d1330/server/src/routes/health.ts +14 -0
- package/.claude/worktrees/competent-burnell-8d1330/server/tsconfig.json +19 -0
- package/.claude/worktrees/competent-burnell-8d1330/shared/package.json +24 -0
- package/.claude/worktrees/competent-burnell-8d1330/shared/src/index.ts +91 -0
- package/.claude/worktrees/competent-burnell-8d1330/shared/tsconfig.json +16 -0
- package/.claude/worktrees/competent-burnell-8d1330/tsconfig.base.json +12 -0
- package/.claude/worktrees/competent-burnell-8d1330/visionos-manifesto/index.html +392 -0
- package/.claude/worktrees/competent-burnell-8d1330/visionos-manifesto/script.js +146 -0
- package/.claude/worktrees/competent-burnell-8d1330/visionos-manifesto/styles.css +1082 -0
- package/.claude/worktrees/vigilant-napier-0de76f/README.md +138 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/package.json +35 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/scripts/copy-web-assets.mjs +12 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/src/commands/logout.ts +12 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/src/commands/open.ts +19 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/src/commands/start.ts +97 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/src/commands/status.ts +23 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/src/commands/userinfo.ts +47 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/src/index.ts +23 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/src/lib/auth.ts +84 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/src/lib/browser.ts +37 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/src/lib/localState.ts +80 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/src/lib/runtime.ts +203 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/src/runtime/index.ts +36 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/src/types/inquirer.d.ts +9 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/tsconfig.json +19 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/index.html +15 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/package.json +27 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/postcss.config.cjs +7 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/src/App.tsx +57 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/src/components/CliAuthPage.tsx +385 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/src/components/ManifestoPage.tsx +946 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/src/components/TrackCard.tsx +19 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/src/lib/api.ts +58 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/src/main.tsx +11 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/src/styles/index.css +33 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/src/styles/manifesto.css +1398 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/tailwind.config.ts +36 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/tsconfig.json +25 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/vite.config.ts +20 -0
- package/.claude/worktrees/vigilant-napier-0de76f/package-lock.json +5278 -0
- package/.claude/worktrees/vigilant-napier-0de76f/package.json +24 -0
- package/.claude/worktrees/vigilant-napier-0de76f/server/package.json +25 -0
- package/.claude/worktrees/vigilant-napier-0de76f/server/src/app.ts +71 -0
- package/.claude/worktrees/vigilant-napier-0de76f/server/src/config/env.ts +14 -0
- package/.claude/worktrees/vigilant-napier-0de76f/server/src/features/auth/sessionStore.ts +74 -0
- package/.claude/worktrees/vigilant-napier-0de76f/server/src/index.ts +8 -0
- package/.claude/worktrees/vigilant-napier-0de76f/server/src/routes/auth.ts +112 -0
- package/.claude/worktrees/vigilant-napier-0de76f/server/src/routes/health.ts +14 -0
- package/.claude/worktrees/vigilant-napier-0de76f/server/tsconfig.json +19 -0
- package/.claude/worktrees/vigilant-napier-0de76f/shared/package.json +24 -0
- package/.claude/worktrees/vigilant-napier-0de76f/shared/src/index.ts +91 -0
- package/.claude/worktrees/vigilant-napier-0de76f/shared/tsconfig.json +16 -0
- package/.claude/worktrees/vigilant-napier-0de76f/tsconfig.base.json +12 -0
- package/.claude/worktrees/vigilant-napier-0de76f/visionos-manifesto/index.html +392 -0
- package/.claude/worktrees/vigilant-napier-0de76f/visionos-manifesto/script.js +146 -0
- package/.claude/worktrees/vigilant-napier-0de76f/visionos-manifesto/styles.css +1082 -0
- package/.github/workflows/publish.yml +30 -0
- package/README.md +175 -0
- package/cli/README.md +165 -0
- package/cli/package.json +36 -0
- package/cli/scripts/copy-web-assets.mjs +12 -0
- package/cli/src/commands/lessons.ts +68 -0
- package/cli/src/commands/login.ts +46 -0
- package/cli/src/commands/logout.ts +12 -0
- package/cli/src/commands/open.ts +29 -0
- package/cli/src/commands/start.ts +146 -0
- package/cli/src/commands/status.ts +28 -0
- package/cli/src/commands/userinfo.ts +59 -0
- package/cli/src/index.ts +109 -0
- package/cli/src/lib/auth.ts +84 -0
- package/cli/src/lib/browser.ts +37 -0
- package/cli/src/lib/content.ts +57 -0
- package/cli/src/lib/lessonPrinter.ts +38 -0
- package/cli/src/lib/lessonRunner.ts +381 -0
- package/cli/src/lib/localState.ts +114 -0
- package/cli/src/lib/loginFlow.ts +74 -0
- package/cli/src/lib/progress.ts +94 -0
- package/cli/src/lib/runtime.ts +220 -0
- package/cli/src/lib/validator.ts +401 -0
- package/cli/src/runtime/index.ts +108 -0
- package/cli/src/types/inquirer.d.ts +9 -0
- package/cli/tsconfig.json +19 -0
- package/client/index.html +15 -0
- package/client/package.json +27 -0
- package/client/postcss.config.cjs +7 -0
- package/client/src/App.tsx +102 -0
- package/client/src/components/AccountPage.tsx +79 -0
- package/client/src/components/AuthPanel.tsx +312 -0
- package/client/src/components/CliAuthPage.tsx +367 -0
- package/client/src/components/CreatorPortal.tsx +885 -0
- package/client/src/components/ErrorBoundary.tsx +92 -0
- package/client/src/components/ManifestoPage.tsx +1126 -0
- package/client/src/components/TrackCard.tsx +19 -0
- package/client/src/lib/api.ts +215 -0
- package/client/src/main.tsx +14 -0
- package/client/src/styles/index.css +33 -0
- package/client/src/styles/manifesto.css +1828 -0
- package/client/tailwind.config.ts +36 -0
- package/client/tsconfig.json +25 -0
- package/client/vercel.json +8 -0
- package/client/vite.config.ts +33 -0
- package/package.json +27 -0
- package/server/package.json +26 -0
- package/server/src/app.ts +132 -0
- package/server/src/config/env.ts +135 -0
- package/server/src/features/accounts/accountStore.ts +359 -0
- package/server/src/features/accounts/contentStore.ts +264 -0
- package/server/src/features/accounts/password.ts +26 -0
- package/server/src/features/auth/sessionStore.ts +79 -0
- package/server/src/index.ts +8 -0
- package/server/src/routes/auth.ts +328 -0
- package/server/src/routes/content.ts +174 -0
- package/server/src/routes/health.ts +14 -0
- package/server/src/routes/progress.ts +105 -0
- package/server/tsconfig.json +19 -0
- package/shared/package.json +24 -0
- package/shared/src/index.ts +455 -0
- package/shared/tsconfig.json +16 -0
- package/tsconfig.base.json +12 -0
- package/visionos-manifesto/index.html +392 -0
- package/visionos-manifesto/script.js +146 -0
- package/visionos-manifesto/styles.css +1082 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "visionos-monorepo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"packageManager": "npm@10",
|
|
6
|
+
"workspaces": [
|
|
7
|
+
"cli",
|
|
8
|
+
"server",
|
|
9
|
+
"client",
|
|
10
|
+
"shared"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "npm run build -w shared && npm run build -w server && npm run build -w client && npm run build -w cli",
|
|
14
|
+
"pack:cli": "npm run build && npm pack -w cli",
|
|
15
|
+
"dev": "concurrently -k -n server,client -c blue,magenta \"npm run dev:server\" \"npm run dev:client\"",
|
|
16
|
+
"dev:cli": "npm run dev -w cli",
|
|
17
|
+
"dev:server": "npm run dev -w server",
|
|
18
|
+
"dev:client": "npm run dev -w client",
|
|
19
|
+
"lint": "npm run lint --workspaces --if-present"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"concurrently": "^9.2.1"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@visionos/server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsup src/index.ts --format esm --clean --out-dir dist",
|
|
8
|
+
"dev": "tsx watch src/index.ts",
|
|
9
|
+
"start": "node dist/index.js",
|
|
10
|
+
"lint": "tsc --noEmit -p tsconfig.json"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"cors": "^2.8.5",
|
|
14
|
+
"dotenv": "^16.4.5",
|
|
15
|
+
"express": "^4.21.1"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/cors": "^2.8.17",
|
|
19
|
+
"@types/express": "^4.17.21",
|
|
20
|
+
"@types/node": "^22.10.1",
|
|
21
|
+
"tsup": "^8.3.0",
|
|
22
|
+
"tsx": "^4.19.1",
|
|
23
|
+
"typescript": "^5.6.3"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import cors from "cors";
|
|
3
|
+
import express from "express";
|
|
4
|
+
import { env } from "./config/env.js";
|
|
5
|
+
import { createAuthRouter, type CreateAuthRouterOptions } from "./routes/auth.js";
|
|
6
|
+
import { healthRouter } from "./routes/health.js";
|
|
7
|
+
|
|
8
|
+
export interface CreateAppOptions extends CreateAuthRouterOptions {
|
|
9
|
+
apiBaseUrl?: string;
|
|
10
|
+
clientOrigin?: string;
|
|
11
|
+
corsOrigin?: string;
|
|
12
|
+
mode?: "development" | "embedded";
|
|
13
|
+
staticSiteDir?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createApp(options: CreateAppOptions = {}) {
|
|
17
|
+
const app = express();
|
|
18
|
+
const clientOrigin = options.clientOrigin ?? env.clientOrigin;
|
|
19
|
+
const corsOrigin = options.corsOrigin ?? env.corsOrigin;
|
|
20
|
+
const apiBaseUrl = options.apiBaseUrl ?? `http://${env.host}:${env.port}`;
|
|
21
|
+
const authRouter = createAuthRouter({
|
|
22
|
+
clientOrigin,
|
|
23
|
+
getCurrentUser: options.getCurrentUser,
|
|
24
|
+
onAuthenticated: options.onAuthenticated
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
app.use(
|
|
28
|
+
cors({
|
|
29
|
+
origin: corsOrigin
|
|
30
|
+
})
|
|
31
|
+
);
|
|
32
|
+
app.use(express.json());
|
|
33
|
+
app.use(express.urlencoded({ extended: true }));
|
|
34
|
+
|
|
35
|
+
app.use("/auth", authRouter);
|
|
36
|
+
app.use("/health", healthRouter);
|
|
37
|
+
|
|
38
|
+
app.get("/runtime/info", (_req, res) => {
|
|
39
|
+
res.status(200).json({
|
|
40
|
+
runtime: {
|
|
41
|
+
appUrl: clientOrigin,
|
|
42
|
+
apiBaseUrl,
|
|
43
|
+
mode: options.mode ?? "development"
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (options.staticSiteDir) {
|
|
49
|
+
app.use(express.static(options.staticSiteDir));
|
|
50
|
+
|
|
51
|
+
app.get("*", (_req, res) => {
|
|
52
|
+
res.sendFile(path.join(options.staticSiteDir as string, "index.html"));
|
|
53
|
+
});
|
|
54
|
+
} else {
|
|
55
|
+
app.get("/", (_req, res) => {
|
|
56
|
+
res.status(200).json({
|
|
57
|
+
message: "VisionOS API bootstrap is running."
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
app.use((error: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
|
|
63
|
+
const message = error instanceof Error ? error.message : "Unexpected VisionOS server error.";
|
|
64
|
+
|
|
65
|
+
res.status(500).json({
|
|
66
|
+
message
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return app;
|
|
71
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
2
|
+
|
|
3
|
+
dotenv.config();
|
|
4
|
+
|
|
5
|
+
const rawPort = Number(process.env.PORT ?? 4000);
|
|
6
|
+
const host = process.env.HOST ?? "127.0.0.1";
|
|
7
|
+
const clientOrigin = process.env.CLIENT_ORIGIN ?? "http://localhost:5173";
|
|
8
|
+
|
|
9
|
+
export const env = {
|
|
10
|
+
host,
|
|
11
|
+
port: Number.isNaN(rawPort) ? 4000 : rawPort,
|
|
12
|
+
clientOrigin,
|
|
13
|
+
corsOrigin: process.env.CORS_ORIGIN ?? clientOrigin
|
|
14
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
CLI_AUTH_POLL_INTERVAL_MS,
|
|
4
|
+
CLI_AUTH_SESSION_TTL_MS,
|
|
5
|
+
type CliAuthSession,
|
|
6
|
+
type CreateCliAuthSessionResponse,
|
|
7
|
+
type LearnerIdentity,
|
|
8
|
+
type LearningTrackId
|
|
9
|
+
} from "../../../../shared/src/index.js";
|
|
10
|
+
|
|
11
|
+
export class InMemoryCliAuthSessionStore {
|
|
12
|
+
private readonly sessions = new Map<string, CliAuthSession>();
|
|
13
|
+
|
|
14
|
+
constructor(private readonly clientOrigin: string) {}
|
|
15
|
+
|
|
16
|
+
createSession(trackId: LearningTrackId): CreateCliAuthSessionResponse {
|
|
17
|
+
const createdAt = new Date();
|
|
18
|
+
const session: CliAuthSession = {
|
|
19
|
+
sessionId: randomUUID(),
|
|
20
|
+
trackId,
|
|
21
|
+
status: "pending",
|
|
22
|
+
createdAt: createdAt.toISOString(),
|
|
23
|
+
expiresAt: new Date(createdAt.getTime() + CLI_AUTH_SESSION_TTL_MS).toISOString()
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
this.sessions.set(session.sessionId, session);
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
session,
|
|
30
|
+
browserUrl: new URL(`/?cliAuthSession=${session.sessionId}`, this.clientOrigin).toString(),
|
|
31
|
+
pollIntervalMs: CLI_AUTH_POLL_INTERVAL_MS
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getSession(sessionId: string): CliAuthSession | undefined {
|
|
36
|
+
const session = this.sessions.get(sessionId);
|
|
37
|
+
|
|
38
|
+
if (!session) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (session.status === "pending" && Date.now() >= Date.parse(session.expiresAt)) {
|
|
43
|
+
const expiredSession: CliAuthSession = {
|
|
44
|
+
...session,
|
|
45
|
+
status: "expired"
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
this.sessions.set(sessionId, expiredSession);
|
|
49
|
+
|
|
50
|
+
return expiredSession;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return session;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
completeSession(sessionId: string, learner: LearnerIdentity): CliAuthSession | undefined {
|
|
57
|
+
const session = this.getSession(sessionId);
|
|
58
|
+
|
|
59
|
+
if (!session || session.status !== "pending") {
|
|
60
|
+
return session;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const authenticatedSession: CliAuthSession = {
|
|
64
|
+
...session,
|
|
65
|
+
status: "authenticated",
|
|
66
|
+
authenticatedAt: new Date().toISOString(),
|
|
67
|
+
learner
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
this.sessions.set(sessionId, authenticatedSession);
|
|
71
|
+
|
|
72
|
+
return authenticatedSession;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import {
|
|
3
|
+
type CliAuthSession,
|
|
4
|
+
type CompleteCliAuthSessionRequest,
|
|
5
|
+
type CreateCliAuthSessionRequest,
|
|
6
|
+
LEARNING_TRACKS,
|
|
7
|
+
type LearnerIdentity,
|
|
8
|
+
type LearningTrackId
|
|
9
|
+
} from "../../../shared/src/index.js";
|
|
10
|
+
import { env } from "../config/env.js";
|
|
11
|
+
import { InMemoryCliAuthSessionStore } from "../features/auth/sessionStore.js";
|
|
12
|
+
|
|
13
|
+
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
14
|
+
|
|
15
|
+
function isLearningTrackId(value: unknown): value is LearningTrackId {
|
|
16
|
+
return typeof value === "string" && LEARNING_TRACKS.some((track) => track.id === value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CreateAuthRouterOptions {
|
|
20
|
+
clientOrigin?: string;
|
|
21
|
+
getCurrentUser?: () => Promise<LearnerIdentity | null> | LearnerIdentity | null;
|
|
22
|
+
onAuthenticated?: (session: CliAuthSession) => Promise<void> | void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createAuthRouter(options: CreateAuthRouterOptions = {}) {
|
|
26
|
+
const router = Router();
|
|
27
|
+
const cliAuthSessionStore = new InMemoryCliAuthSessionStore(options.clientOrigin ?? env.clientOrigin);
|
|
28
|
+
|
|
29
|
+
router.get("/me", async (_req, res, next) => {
|
|
30
|
+
try {
|
|
31
|
+
const user = (await options.getCurrentUser?.()) ?? null;
|
|
32
|
+
|
|
33
|
+
return res.status(200).json({ user });
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return next(error);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
router.post("/cli/sessions", (req, res) => {
|
|
40
|
+
const body = req.body as Partial<CreateCliAuthSessionRequest>;
|
|
41
|
+
|
|
42
|
+
if (!isLearningTrackId(body.trackId)) {
|
|
43
|
+
return res.status(400).json({
|
|
44
|
+
message: "A valid learning track is required to start browser authentication."
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return res.status(201).json(cliAuthSessionStore.createSession(body.trackId));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
router.get("/cli/sessions/:sessionId", (req, res) => {
|
|
52
|
+
const session = cliAuthSessionStore.getSession(req.params.sessionId);
|
|
53
|
+
|
|
54
|
+
if (!session) {
|
|
55
|
+
return res.status(404).json({
|
|
56
|
+
message: "Auth session not found."
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return res.status(200).json({ session });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
router.post("/cli/sessions/:sessionId/complete", async (req, res, next) => {
|
|
64
|
+
try {
|
|
65
|
+
const session = cliAuthSessionStore.getSession(req.params.sessionId);
|
|
66
|
+
|
|
67
|
+
if (!session) {
|
|
68
|
+
return res.status(404).json({
|
|
69
|
+
message: "Auth session not found."
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (session.status === "expired") {
|
|
74
|
+
return res.status(410).json({
|
|
75
|
+
message: "Auth session expired.",
|
|
76
|
+
session
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (session.status === "authenticated") {
|
|
81
|
+
return res.status(200).json({ session });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const body = req.body as Partial<CompleteCliAuthSessionRequest>;
|
|
85
|
+
const learnerName = body.learnerName?.trim();
|
|
86
|
+
const learnerEmail = body.learnerEmail?.trim().toLowerCase();
|
|
87
|
+
|
|
88
|
+
if (!learnerName || !learnerEmail || !emailPattern.test(learnerEmail)) {
|
|
89
|
+
return res.status(400).json({
|
|
90
|
+
message: "A learner name and valid email address are required to complete sign-in."
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const authenticatedSession = cliAuthSessionStore.completeSession(req.params.sessionId, {
|
|
95
|
+
name: learnerName,
|
|
96
|
+
email: learnerEmail
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (authenticatedSession) {
|
|
100
|
+
await options.onAuthenticated?.(authenticatedSession);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return res.status(200).json({ session: authenticatedSession });
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return next(error);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return router;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const authRouter = createAuthRouter();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { APP_NAME, APP_VERSION } from "../../../shared/src/index.js";
|
|
3
|
+
|
|
4
|
+
export const healthRouter = Router();
|
|
5
|
+
|
|
6
|
+
healthRouter.get("/", (_req, res) => {
|
|
7
|
+
res.status(200).json({
|
|
8
|
+
status: "ok",
|
|
9
|
+
service: APP_NAME,
|
|
10
|
+
version: APP_VERSION,
|
|
11
|
+
timestamp: new Date().toISOString()
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": [
|
|
7
|
+
"ES2022"
|
|
8
|
+
],
|
|
9
|
+
"types": [
|
|
10
|
+
"node"
|
|
11
|
+
],
|
|
12
|
+
"noEmit": true
|
|
13
|
+
},
|
|
14
|
+
"include": [
|
|
15
|
+
"src/**/*.ts",
|
|
16
|
+
"../shared/src/**/*.ts"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@visionos/shared",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
16
|
+
"dev": "tsup src/index.ts --format esm --dts --watch",
|
|
17
|
+
"lint": "tsc --noEmit -p tsconfig.json"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"tsup": "^8.3.0",
|
|
21
|
+
"typescript": "^5.6.3"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export const APP_NAME = "VisionOS";
|
|
2
|
+
export const APP_VERSION = "0.1.0";
|
|
3
|
+
|
|
4
|
+
export type LearningTrackId = "git" | "linux" | "react-cli" | "os-commands";
|
|
5
|
+
|
|
6
|
+
export interface LearningTrack {
|
|
7
|
+
id: LearningTrackId;
|
|
8
|
+
label: string;
|
|
9
|
+
description: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface LearnerIdentity {
|
|
13
|
+
name: string;
|
|
14
|
+
email: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface VisionOsUserProfile extends LearnerIdentity {
|
|
18
|
+
sessionId: string;
|
|
19
|
+
trackId: LearningTrackId;
|
|
20
|
+
authenticatedAt: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type CliAuthSessionStatus = "pending" | "authenticated" | "expired";
|
|
24
|
+
|
|
25
|
+
export interface CliAuthSession {
|
|
26
|
+
sessionId: string;
|
|
27
|
+
trackId: LearningTrackId;
|
|
28
|
+
status: CliAuthSessionStatus;
|
|
29
|
+
createdAt: string;
|
|
30
|
+
expiresAt: string;
|
|
31
|
+
authenticatedAt?: string;
|
|
32
|
+
learner?: LearnerIdentity;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CliAuthSessionEnvelope {
|
|
36
|
+
session: CliAuthSession;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CreateCliAuthSessionRequest {
|
|
40
|
+
trackId: LearningTrackId;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface CreateCliAuthSessionResponse extends CliAuthSessionEnvelope {
|
|
44
|
+
browserUrl: string;
|
|
45
|
+
pollIntervalMs: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface CompleteCliAuthSessionRequest {
|
|
49
|
+
learnerName: string;
|
|
50
|
+
learnerEmail: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface VisionOsUserInfoResponse {
|
|
54
|
+
user: VisionOsUserProfile | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface VisionOsRuntimeInfo {
|
|
58
|
+
appUrl: string;
|
|
59
|
+
apiBaseUrl: string;
|
|
60
|
+
mode: "development" | "embedded";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface VisionOsRuntimeInfoResponse {
|
|
64
|
+
runtime: VisionOsRuntimeInfo;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const CLI_AUTH_POLL_INTERVAL_MS = 1_500;
|
|
68
|
+
export const CLI_AUTH_SESSION_TTL_MS = 10 * 60 * 1_000;
|
|
69
|
+
|
|
70
|
+
export const LEARNING_TRACKS: LearningTrack[] = [
|
|
71
|
+
{
|
|
72
|
+
id: "git",
|
|
73
|
+
label: "Git",
|
|
74
|
+
description: "Learn branching, commits, rebasing, and collaboration commands."
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "linux",
|
|
78
|
+
label: "Linux",
|
|
79
|
+
description: "Build confidence with filesystem navigation, permissions, and shell basics."
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "react-cli",
|
|
83
|
+
label: "React CLI",
|
|
84
|
+
description: "Practice project scaffolding, package scripts, and frontend workflows."
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: "os-commands",
|
|
88
|
+
label: "OS Commands",
|
|
89
|
+
description: "Train with practical day-to-day terminal commands across common environments."
|
|
90
|
+
}
|
|
91
|
+
];
|