@anchrd/intel-api 0.2.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/README.md +96 -0
- package/bin/intel.mjs +134 -0
- package/dist/adapters/cloudflare/cloudflare-flow-workflow.d.ts +21 -0
- package/dist/adapters/cloudflare/cloudflare-flow-workflow.js +39 -0
- package/dist/adapters/cloudflare/cloudflare.d.ts +8 -0
- package/dist/adapters/cloudflare/cloudflare.js +200 -0
- package/dist/adapters/cloudflare/cloudflare.types.d.ts +29 -0
- package/dist/adapters/cloudflare/cloudflare.types.js +1 -0
- package/dist/adapters/content/content.d.ts +3 -0
- package/dist/adapters/content/content.js +22 -0
- package/dist/adapters/content/content.types.d.ts +14 -0
- package/dist/adapters/content/content.types.js +1 -0
- package/dist/adapters/db/db-flows.d.ts +6 -0
- package/dist/adapters/db/db-flows.js +466 -0
- package/dist/adapters/db/db-indexing.d.ts +3 -0
- package/dist/adapters/db/db-indexing.js +70 -0
- package/dist/adapters/db/db-oauth.d.ts +3 -0
- package/dist/adapters/db/db-oauth.js +26 -0
- package/dist/adapters/db/db-tools.d.ts +6 -0
- package/dist/adapters/db/db-tools.js +200 -0
- package/dist/adapters/db/db.d.ts +6 -0
- package/dist/adapters/db/db.js +755 -0
- package/dist/adapters/db/db.types.d.ts +16 -0
- package/dist/adapters/db/db.types.js +1 -0
- package/dist/adapters/document-converter/document-converter.d.ts +2 -0
- package/dist/adapters/document-converter/document-converter.js +25 -0
- package/dist/adapters/document-converter/document-converter.types.d.ts +9 -0
- package/dist/adapters/document-converter/document-converter.types.js +1 -0
- package/dist/adapters/flow-runtime/flow-runtime.d.ts +3 -0
- package/dist/adapters/flow-runtime/flow-runtime.js +22 -0
- package/dist/adapters/flow-runtime/flow-runtime.types.d.ts +19 -0
- package/dist/adapters/flow-runtime/flow-runtime.types.js +1 -0
- package/dist/adapters/index-queue/index-queue.d.ts +10 -0
- package/dist/adapters/index-queue/index-queue.js +12 -0
- package/dist/adapters/index-queue/index-queue.types.d.ts +11 -0
- package/dist/adapters/index-queue/index-queue.types.js +1 -0
- package/dist/adapters/json-schema/json-schema.d.ts +2 -0
- package/dist/adapters/json-schema/json-schema.js +31 -0
- package/dist/adapters/openid/openid.d.ts +5 -0
- package/dist/adapters/openid/openid.js +159 -0
- package/dist/adapters/portal-tokens/portal-tokens.d.ts +8 -0
- package/dist/adapters/portal-tokens/portal-tokens.js +71 -0
- package/dist/adapters/remote-tools/remote-tools.d.ts +5 -0
- package/dist/adapters/remote-tools/remote-tools.js +109 -0
- package/dist/adapters/semantic-index/semantic-index.d.ts +2 -0
- package/dist/adapters/semantic-index/semantic-index.js +57 -0
- package/dist/adapters/semantic-index/semantic-index.types.d.ts +32 -0
- package/dist/adapters/semantic-index/semantic-index.types.js +1 -0
- package/dist/adapters/session-cookie/session-cookie.d.ts +5 -0
- package/dist/adapters/session-cookie/session-cookie.js +81 -0
- package/dist/adapters/tool-source-policy/tool-source-policy.d.ts +1 -0
- package/dist/adapters/tool-source-policy/tool-source-policy.js +22 -0
- package/dist/auth/auth.d.ts +2 -0
- package/dist/auth/auth.js +176 -0
- package/dist/auth/auth.types.d.ts +104 -0
- package/dist/auth/auth.types.js +1 -0
- package/dist/build/build.d.ts +2 -0
- package/dist/build/build.js +125 -0
- package/dist/build/build.types.d.ts +17 -0
- package/dist/build/build.types.js +1 -0
- package/dist/cli/cli.d.ts +4 -0
- package/dist/cli/cli.js +141 -0
- package/dist/cli/cli.types.d.ts +14 -0
- package/dist/cli/cli.types.js +1 -0
- package/dist/flows/flows.d.ts +4 -0
- package/dist/flows/flows.js +409 -0
- package/dist/flows/flows.types.d.ts +108 -0
- package/dist/flows/flows.types.js +1 -0
- package/dist/http/http.d.ts +8 -0
- package/dist/http/http.js +286 -0
- package/dist/http/http.types.d.ts +14 -0
- package/dist/http/http.types.js +1 -0
- package/dist/indexing/indexing.d.ts +6 -0
- package/dist/indexing/indexing.js +48 -0
- package/dist/indexing/indexing.types.d.ts +10 -0
- package/dist/indexing/indexing.types.js +1 -0
- package/dist/intel/intel.d.ts +3 -0
- package/dist/intel/intel.js +77 -0
- package/dist/intel/intel.types.d.ts +14 -0
- package/dist/intel/intel.types.js +1 -0
- package/dist/knowledge/knowledge.d.ts +2 -0
- package/dist/knowledge/knowledge.js +479 -0
- package/dist/knowledge/knowledge.types.d.ts +162 -0
- package/dist/knowledge/knowledge.types.js +1 -0
- package/dist/mcp/mcp.d.ts +2 -0
- package/dist/mcp/mcp.js +484 -0
- package/dist/mcp/mcp.types.d.ts +10 -0
- package/dist/mcp/mcp.types.js +1 -0
- package/dist/prepare/prepare.d.ts +4 -0
- package/dist/prepare/prepare.js +16 -0
- package/dist/prepare/prepare.types.d.ts +9 -0
- package/dist/prepare/prepare.types.js +1 -0
- package/dist/shared/gate-authorization/gate-authorization.d.ts +4 -0
- package/dist/shared/gate-authorization/gate-authorization.js +19 -0
- package/dist/shared/intel-error/intel-error.d.ts +5 -0
- package/dist/shared/intel-error/intel-error.js +10 -0
- package/dist/shared/problem-details/problem-details.d.ts +8 -0
- package/dist/shared/problem-details/problem-details.js +4 -0
- package/dist/shared/safe-return-path/safe-return-path.d.ts +2 -0
- package/dist/shared/safe-return-path/safe-return-path.js +11 -0
- package/dist/shared/sha256/sha256.d.ts +1 -0
- package/dist/shared/sha256/sha256.js +9 -0
- package/dist/tools/tools.d.ts +2 -0
- package/dist/tools/tools.js +107 -0
- package/dist/tools/tools.types.d.ts +56 -0
- package/dist/tools/tools.types.js +1 -0
- package/examples/branding/favicon.svg +5 -0
- package/examples/branding/logo.svg +5 -0
- package/examples/branding/theme.css +16 -0
- package/examples/dev.vars.example +7 -0
- package/examples/intel.json +9 -0
- package/migrations/0000_intel_foundation.sql +203 -0
- package/migrations/0001_portal_native_tools.sql +21 -0
- package/package.json +62 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { SafeReturnPath } from "../../shared/safe-return-path/safe-return-path.js";
|
|
3
|
+
const PendingSession = z.strictObject({
|
|
4
|
+
version: z.literal(1),
|
|
5
|
+
kind: z.literal("pending"),
|
|
6
|
+
clientId: z.string().min(1),
|
|
7
|
+
codeVerifier: z.string().min(43),
|
|
8
|
+
state: z.string().min(16),
|
|
9
|
+
returnTo: SafeReturnPath,
|
|
10
|
+
expiresAt: z.number().int().positive(),
|
|
11
|
+
});
|
|
12
|
+
const UserSession = z.strictObject({
|
|
13
|
+
version: z.literal(1),
|
|
14
|
+
kind: z.literal("user"),
|
|
15
|
+
clientId: z.string().min(1),
|
|
16
|
+
accessToken: z.string().min(1),
|
|
17
|
+
expiresAt: z.number().int().positive(),
|
|
18
|
+
});
|
|
19
|
+
const ConnectionPendingSession = z.strictObject({
|
|
20
|
+
version: z.literal(1),
|
|
21
|
+
kind: z.literal("connection-pending"),
|
|
22
|
+
clientId: z.string().min(1),
|
|
23
|
+
codeVerifier: z.string().min(43),
|
|
24
|
+
state: z.string().min(16),
|
|
25
|
+
returnTo: SafeReturnPath,
|
|
26
|
+
issuer: z.url(),
|
|
27
|
+
resource: z.url(),
|
|
28
|
+
userId: z.string().min(1),
|
|
29
|
+
user: UserSession,
|
|
30
|
+
expiresAt: z.number().int().positive(),
|
|
31
|
+
});
|
|
32
|
+
const Session = z.discriminatedUnion("kind", [
|
|
33
|
+
PendingSession,
|
|
34
|
+
ConnectionPendingSession,
|
|
35
|
+
UserSession,
|
|
36
|
+
]);
|
|
37
|
+
const additionalData = new TextEncoder().encode("anchrd-intel-session-v1");
|
|
38
|
+
function encode(bytes) {
|
|
39
|
+
let value = "";
|
|
40
|
+
for (const byte of bytes)
|
|
41
|
+
value += String.fromCharCode(byte);
|
|
42
|
+
return btoa(value).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
43
|
+
}
|
|
44
|
+
function decode(value) {
|
|
45
|
+
const padded = value
|
|
46
|
+
.replaceAll("-", "+")
|
|
47
|
+
.replaceAll("_", "/")
|
|
48
|
+
.padEnd(Math.ceil(value.length / 4) * 4, "=");
|
|
49
|
+
return Uint8Array.from(atob(padded), (character) => character.charCodeAt(0));
|
|
50
|
+
}
|
|
51
|
+
export function createSessionCodec(deps) {
|
|
52
|
+
if (new TextEncoder().encode(deps.secret).byteLength < 32) {
|
|
53
|
+
throw new Error("INTEL_SESSION_SECRET must contain at least 32 bytes");
|
|
54
|
+
}
|
|
55
|
+
const key = deps.crypto.subtle
|
|
56
|
+
.digest("SHA-256", new TextEncoder().encode(deps.secret))
|
|
57
|
+
.then((hash) => deps.crypto.subtle.importKey("raw", hash, "AES-GCM", false, ["encrypt", "decrypt"]));
|
|
58
|
+
return {
|
|
59
|
+
async seal(value) {
|
|
60
|
+
const session = Session.parse(value);
|
|
61
|
+
const iv = deps.crypto.getRandomValues(new Uint8Array(12));
|
|
62
|
+
const encrypted = new Uint8Array(await deps.crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData }, await key, new TextEncoder().encode(JSON.stringify(session))));
|
|
63
|
+
const output = new Uint8Array(iv.byteLength + encrypted.byteLength);
|
|
64
|
+
output.set(iv);
|
|
65
|
+
output.set(encrypted, iv.byteLength);
|
|
66
|
+
return encode(output);
|
|
67
|
+
},
|
|
68
|
+
async open(value) {
|
|
69
|
+
try {
|
|
70
|
+
const input = decode(value);
|
|
71
|
+
if (input.byteLength <= 28)
|
|
72
|
+
return null;
|
|
73
|
+
const decrypted = await deps.crypto.subtle.decrypt({ name: "AES-GCM", iv: input.slice(0, 12), additionalData }, await key, input.slice(12));
|
|
74
|
+
return Session.parse(JSON.parse(new TextDecoder().decode(decrypted)));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function createToolSourcePolicy(configuredOrigins: string | undefined): (sourceUrl: string) => boolean;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function createToolSourcePolicy(configuredOrigins) {
|
|
2
|
+
const origins = new Set((configuredOrigins ?? "")
|
|
3
|
+
.split(",")
|
|
4
|
+
.map((value) => value.trim())
|
|
5
|
+
.filter(Boolean)
|
|
6
|
+
.map((value) => {
|
|
7
|
+
const url = new URL(value);
|
|
8
|
+
if (url.username || url.password || (url.pathname !== "/" && url.pathname !== "")) {
|
|
9
|
+
throw new Error("TOOL_SOURCE_ORIGINS must contain exact origins without credentials or paths");
|
|
10
|
+
}
|
|
11
|
+
return url.origin;
|
|
12
|
+
}));
|
|
13
|
+
// A malformed URL is not an allowed origin, so the policy answers false instead of throwing.
|
|
14
|
+
return (sourceUrl) => {
|
|
15
|
+
try {
|
|
16
|
+
return origins.has(new URL(sourceUrl).origin);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { calculatePKCECodeChallenge, randomPKCECodeVerifier, randomState } from "openid-client";
|
|
2
|
+
import { IntelError } from "../shared/intel-error/intel-error.js";
|
|
3
|
+
import { SafeReturnPath } from "../shared/safe-return-path/safe-return-path.js";
|
|
4
|
+
const cookieName = "intel_session";
|
|
5
|
+
function cookieValue(headers) {
|
|
6
|
+
const values = headers.get("cookie")?.split(";") ?? [];
|
|
7
|
+
for (const value of values) {
|
|
8
|
+
const [name, ...rest] = value.trim().split("=");
|
|
9
|
+
if (name === cookieName)
|
|
10
|
+
return rest.join("=") || null;
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
function sessionCookie(value, secure, maxAge) {
|
|
15
|
+
return `${cookieName}=${value}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${Math.max(0, Math.floor(maxAge))}${secure ? "; Secure" : ""}`;
|
|
16
|
+
}
|
|
17
|
+
function redirect(location, cookie) {
|
|
18
|
+
const headers = new Headers({ location, "cache-control": "no-store" });
|
|
19
|
+
if (cookie)
|
|
20
|
+
headers.set("set-cookie", cookie);
|
|
21
|
+
return new Response(null, { status: 302, headers });
|
|
22
|
+
}
|
|
23
|
+
async function pkce() {
|
|
24
|
+
for (;;) {
|
|
25
|
+
const verifier = randomPKCECodeVerifier();
|
|
26
|
+
const challenge = await calculatePKCECodeChallenge(verifier);
|
|
27
|
+
if (/^[A-Za-z0-9]/.test(challenge))
|
|
28
|
+
return { verifier, challenge };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function createBrowserAuth(deps) {
|
|
32
|
+
const baseUrl = deps.baseUrl.replace(/\/+$/, "");
|
|
33
|
+
const gateIssuer = deps.gateUrl.replace(/\/+$/, "");
|
|
34
|
+
const redirectUri = `${baseUrl}/auth/callback`;
|
|
35
|
+
const secure = new URL(baseUrl).protocol === "https:";
|
|
36
|
+
async function clientId(issuer, resource) {
|
|
37
|
+
const registrationKey = `${issuer}#${resource}`;
|
|
38
|
+
const stored = await deps.clients.get(registrationKey, redirectUri);
|
|
39
|
+
if (stored)
|
|
40
|
+
return stored;
|
|
41
|
+
const registered = await deps.oauth.register({
|
|
42
|
+
issuer,
|
|
43
|
+
redirectUri,
|
|
44
|
+
clientName: "Intel",
|
|
45
|
+
resource,
|
|
46
|
+
});
|
|
47
|
+
return await deps.clients.put({
|
|
48
|
+
issuer: registrationKey,
|
|
49
|
+
redirectUri,
|
|
50
|
+
clientId: registered,
|
|
51
|
+
createdAt: deps.now().toISOString(),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async function userSession(client, tokens) {
|
|
55
|
+
return {
|
|
56
|
+
version: 1,
|
|
57
|
+
kind: "user",
|
|
58
|
+
clientId: client,
|
|
59
|
+
accessToken: tokens.accessToken,
|
|
60
|
+
expiresAt: deps.now().getTime() + Math.max(30, tokens.expiresIn) * 1_000,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
async login(requestUrl) {
|
|
65
|
+
const returnTo = SafeReturnPath.catch("/").parse(requestUrl.searchParams.get("returnTo") ?? "/");
|
|
66
|
+
const client = await clientId(gateIssuer, deps.resource);
|
|
67
|
+
const { verifier: codeVerifier, challenge: codeChallenge } = await pkce();
|
|
68
|
+
const state = randomState();
|
|
69
|
+
const pending = {
|
|
70
|
+
version: 1,
|
|
71
|
+
kind: "pending",
|
|
72
|
+
clientId: client,
|
|
73
|
+
codeVerifier,
|
|
74
|
+
state,
|
|
75
|
+
returnTo,
|
|
76
|
+
expiresAt: deps.now().getTime() + 10 * 60 * 1_000,
|
|
77
|
+
};
|
|
78
|
+
const authorizationUrl = await deps.oauth.authorizationUrl({
|
|
79
|
+
issuer: gateIssuer,
|
|
80
|
+
clientId: client,
|
|
81
|
+
redirectUri,
|
|
82
|
+
resource: deps.resource,
|
|
83
|
+
codeChallenge,
|
|
84
|
+
state,
|
|
85
|
+
scope: "openid profile email",
|
|
86
|
+
});
|
|
87
|
+
return redirect(authorizationUrl.href, sessionCookie(await deps.sessions.seal(pending), secure, 10 * 60));
|
|
88
|
+
},
|
|
89
|
+
async connect(requestUrl, headers, userId) {
|
|
90
|
+
const encoded = cookieValue(headers);
|
|
91
|
+
const user = encoded ? await deps.sessions.open(encoded) : null;
|
|
92
|
+
if (user?.kind !== "user" || user.expiresAt <= deps.now().getTime() + 30_000) {
|
|
93
|
+
throw new IntelError(401, "authentication_required", "Authentication is required");
|
|
94
|
+
}
|
|
95
|
+
const returnTo = SafeReturnPath.catch("/tools").parse(requestUrl.searchParams.get("returnTo") ?? "/tools");
|
|
96
|
+
if (!deps.portalUrl) {
|
|
97
|
+
throw new IntelError(503, "portal_not_configured", "No MCP portal is configured for this deployment");
|
|
98
|
+
}
|
|
99
|
+
const discovered = await deps.oauth.discoverResource(deps.portalUrl).catch(() => {
|
|
100
|
+
throw new IntelError(409, "portal_oauth_unavailable", "The MCP portal does not publish usable OAuth metadata");
|
|
101
|
+
});
|
|
102
|
+
const connectionClientId = await clientId(discovered.issuer, discovered.resource);
|
|
103
|
+
const { verifier: codeVerifier, challenge: codeChallenge } = await pkce();
|
|
104
|
+
const state = randomState();
|
|
105
|
+
const pending = {
|
|
106
|
+
version: 1,
|
|
107
|
+
kind: "connection-pending",
|
|
108
|
+
clientId: connectionClientId,
|
|
109
|
+
codeVerifier,
|
|
110
|
+
state,
|
|
111
|
+
returnTo,
|
|
112
|
+
issuer: discovered.issuer,
|
|
113
|
+
resource: discovered.resource,
|
|
114
|
+
userId,
|
|
115
|
+
user,
|
|
116
|
+
expiresAt: deps.now().getTime() + 10 * 60 * 1_000,
|
|
117
|
+
};
|
|
118
|
+
const authorizationUrl = await deps.oauth.authorizationUrl({
|
|
119
|
+
issuer: discovered.issuer,
|
|
120
|
+
clientId: connectionClientId,
|
|
121
|
+
redirectUri,
|
|
122
|
+
resource: discovered.resource,
|
|
123
|
+
codeChallenge,
|
|
124
|
+
state,
|
|
125
|
+
...(discovered.scope ? { scope: discovered.scope } : {}),
|
|
126
|
+
});
|
|
127
|
+
return redirect(authorizationUrl.href, sessionCookie(await deps.sessions.seal(pending), secure, 10 * 60));
|
|
128
|
+
},
|
|
129
|
+
async callback(requestUrl, headers) {
|
|
130
|
+
const encoded = cookieValue(headers);
|
|
131
|
+
const pending = encoded ? await deps.sessions.open(encoded) : null;
|
|
132
|
+
if ((pending?.kind !== "pending" && pending?.kind !== "connection-pending") ||
|
|
133
|
+
pending.expiresAt <= deps.now().getTime()) {
|
|
134
|
+
throw new IntelError(400, "oauth_session_invalid", "OAuth session is missing or expired");
|
|
135
|
+
}
|
|
136
|
+
const tokens = await deps.oauth.exchange({
|
|
137
|
+
issuer: pending.kind === "connection-pending" ? pending.issuer : gateIssuer,
|
|
138
|
+
clientId: pending.clientId,
|
|
139
|
+
callbackUrl: requestUrl,
|
|
140
|
+
redirectUri,
|
|
141
|
+
resource: pending.kind === "connection-pending" ? pending.resource : deps.resource,
|
|
142
|
+
codeVerifier: pending.codeVerifier,
|
|
143
|
+
state: pending.state,
|
|
144
|
+
});
|
|
145
|
+
if (pending.kind === "connection-pending") {
|
|
146
|
+
await deps.portalTokens.write(pending.userId, {
|
|
147
|
+
accessToken: tokens.accessToken,
|
|
148
|
+
refreshToken: tokens.refreshToken ?? null,
|
|
149
|
+
expiresAt: deps.now().getTime() + Math.max(30, tokens.expiresIn) * 1_000,
|
|
150
|
+
issuer: pending.issuer,
|
|
151
|
+
clientId: pending.clientId,
|
|
152
|
+
resource: pending.resource,
|
|
153
|
+
});
|
|
154
|
+
const remaining = (pending.user.expiresAt - deps.now().getTime()) / 1_000;
|
|
155
|
+
if (remaining <= 0) {
|
|
156
|
+
return redirect(`/auth/login?returnTo=${encodeURIComponent(pending.returnTo)}`, sessionCookie("", secure, 0));
|
|
157
|
+
}
|
|
158
|
+
return redirect(pending.returnTo, sessionCookie(await deps.sessions.seal(pending.user), secure, remaining));
|
|
159
|
+
}
|
|
160
|
+
const session = await userSession(pending.clientId, tokens);
|
|
161
|
+
return redirect(pending.returnTo, sessionCookie(await deps.sessions.seal(session), secure, (session.expiresAt - deps.now().getTime()) / 1_000));
|
|
162
|
+
},
|
|
163
|
+
logout() {
|
|
164
|
+
return redirect("/", sessionCookie("", secure, 0));
|
|
165
|
+
},
|
|
166
|
+
async resolve(headers) {
|
|
167
|
+
const encoded = cookieValue(headers);
|
|
168
|
+
const session = encoded ? await deps.sessions.open(encoded) : null;
|
|
169
|
+
if (session?.kind !== "user")
|
|
170
|
+
return null;
|
|
171
|
+
return session.expiresAt > deps.now().getTime() + 30_000
|
|
172
|
+
? { bearer: session.accessToken }
|
|
173
|
+
: null;
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { PortalTokenStore } from "../tools/tools.types.js";
|
|
2
|
+
export interface OAuthTokens {
|
|
3
|
+
accessToken: string;
|
|
4
|
+
expiresIn: number;
|
|
5
|
+
refreshToken?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface OAuthPort {
|
|
8
|
+
discoverResource(resourceUrl: string): Promise<{
|
|
9
|
+
resource: string;
|
|
10
|
+
issuer: string;
|
|
11
|
+
scope?: string;
|
|
12
|
+
}>;
|
|
13
|
+
register(input: {
|
|
14
|
+
issuer: string;
|
|
15
|
+
redirectUri: string;
|
|
16
|
+
clientName: string;
|
|
17
|
+
resource: string;
|
|
18
|
+
}): Promise<string>;
|
|
19
|
+
authorizationUrl(input: {
|
|
20
|
+
issuer: string;
|
|
21
|
+
clientId: string;
|
|
22
|
+
redirectUri: string;
|
|
23
|
+
resource: string;
|
|
24
|
+
codeChallenge: string;
|
|
25
|
+
state: string;
|
|
26
|
+
scope?: string;
|
|
27
|
+
}): Promise<URL>;
|
|
28
|
+
exchange(input: {
|
|
29
|
+
issuer: string;
|
|
30
|
+
clientId: string;
|
|
31
|
+
callbackUrl: URL;
|
|
32
|
+
redirectUri: string;
|
|
33
|
+
resource: string;
|
|
34
|
+
codeVerifier: string;
|
|
35
|
+
state: string;
|
|
36
|
+
}): Promise<OAuthTokens>;
|
|
37
|
+
refresh(input: {
|
|
38
|
+
issuer: string;
|
|
39
|
+
clientId: string;
|
|
40
|
+
resource: string;
|
|
41
|
+
refreshToken: string;
|
|
42
|
+
}): Promise<OAuthTokens>;
|
|
43
|
+
}
|
|
44
|
+
export interface OAuthClientStore {
|
|
45
|
+
get(issuer: string, redirectUri: string): Promise<string | null>;
|
|
46
|
+
put(input: {
|
|
47
|
+
issuer: string;
|
|
48
|
+
redirectUri: string;
|
|
49
|
+
clientId: string;
|
|
50
|
+
createdAt: string;
|
|
51
|
+
}): Promise<string>;
|
|
52
|
+
}
|
|
53
|
+
export interface UserAuthSession {
|
|
54
|
+
version: 1;
|
|
55
|
+
kind: "user";
|
|
56
|
+
clientId: string;
|
|
57
|
+
accessToken: string;
|
|
58
|
+
expiresAt: number;
|
|
59
|
+
}
|
|
60
|
+
export type AuthSession = {
|
|
61
|
+
version: 1;
|
|
62
|
+
kind: "pending";
|
|
63
|
+
clientId: string;
|
|
64
|
+
codeVerifier: string;
|
|
65
|
+
state: string;
|
|
66
|
+
returnTo: string;
|
|
67
|
+
expiresAt: number;
|
|
68
|
+
} | {
|
|
69
|
+
version: 1;
|
|
70
|
+
kind: "connection-pending";
|
|
71
|
+
clientId: string;
|
|
72
|
+
codeVerifier: string;
|
|
73
|
+
state: string;
|
|
74
|
+
returnTo: string;
|
|
75
|
+
issuer: string;
|
|
76
|
+
resource: string;
|
|
77
|
+
userId: string;
|
|
78
|
+
user: UserAuthSession;
|
|
79
|
+
expiresAt: number;
|
|
80
|
+
} | UserAuthSession;
|
|
81
|
+
export interface SessionCodec {
|
|
82
|
+
seal(session: AuthSession): Promise<string>;
|
|
83
|
+
open(value: string): Promise<AuthSession | null>;
|
|
84
|
+
}
|
|
85
|
+
export interface BrowserAuthDeps {
|
|
86
|
+
baseUrl: string;
|
|
87
|
+
gateUrl: string;
|
|
88
|
+
resource: string;
|
|
89
|
+
oauth: OAuthPort;
|
|
90
|
+
clients: OAuthClientStore;
|
|
91
|
+
sessions: SessionCodec;
|
|
92
|
+
portalUrl: string | null;
|
|
93
|
+
portalTokens: Pick<PortalTokenStore, "write">;
|
|
94
|
+
now(): Date;
|
|
95
|
+
}
|
|
96
|
+
export interface BrowserAuth {
|
|
97
|
+
login(requestUrl: URL): Promise<Response>;
|
|
98
|
+
connect(requestUrl: URL, headers: Headers, userId: string): Promise<Response>;
|
|
99
|
+
callback(requestUrl: URL, headers: Headers): Promise<Response>;
|
|
100
|
+
logout(): Response;
|
|
101
|
+
resolve(headers: Headers): Promise<{
|
|
102
|
+
bearer: string;
|
|
103
|
+
} | null>;
|
|
104
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const UiConfig = z.strictObject({
|
|
3
|
+
theme: z.string().min(1).optional(),
|
|
4
|
+
logo: z.string().min(1).optional(),
|
|
5
|
+
favicon: z.string().min(1).optional(),
|
|
6
|
+
defaultLanguage: z.string().min(1).default("en"),
|
|
7
|
+
languages: z.record(z.string().min(1), z.string().min(1)).default({}),
|
|
8
|
+
});
|
|
9
|
+
const IntelConfig = z.strictObject({
|
|
10
|
+
ui: UiConfig.default({ defaultLanguage: "en", languages: {} }),
|
|
11
|
+
});
|
|
12
|
+
const Catalog = z.record(z.string(), z.string());
|
|
13
|
+
const defaultTheme = `/* Generated by \`intel build\`. Intel defaults are active. */\n`;
|
|
14
|
+
const defaultLogo = `export const customLogoUrl: string | null = null;\n`;
|
|
15
|
+
function validationMessage(error) {
|
|
16
|
+
return error.issues
|
|
17
|
+
.map((issue) => `${issue.path.join(".") || "intel.json"}: ${issue.message}`)
|
|
18
|
+
.join("; ");
|
|
19
|
+
}
|
|
20
|
+
function validateCatalog(source, candidate) {
|
|
21
|
+
const expected = Object.keys(source).sort();
|
|
22
|
+
const actual = Object.keys(candidate).sort();
|
|
23
|
+
const missing = expected.filter((key) => !actual.includes(key));
|
|
24
|
+
const unknown = actual.filter((key) => !expected.includes(key));
|
|
25
|
+
return { missing, unknown };
|
|
26
|
+
}
|
|
27
|
+
function asSvgDataUrl(svg) {
|
|
28
|
+
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
|
29
|
+
}
|
|
30
|
+
export function createBuild(deps) {
|
|
31
|
+
async function readRequired(path, field) {
|
|
32
|
+
const content = await deps.readTextFile(path);
|
|
33
|
+
if (content === null)
|
|
34
|
+
throw new Error(`File not found: ${path} (${field})`);
|
|
35
|
+
return content;
|
|
36
|
+
}
|
|
37
|
+
async function readSvg(path, field) {
|
|
38
|
+
if (!path.toLowerCase().endsWith(".svg")) {
|
|
39
|
+
throw new Error(`${field} must point to an .svg file`);
|
|
40
|
+
}
|
|
41
|
+
return await readRequired(path, field);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
async build() {
|
|
45
|
+
const location = await deps.resolveUi();
|
|
46
|
+
if (!location) {
|
|
47
|
+
deps.log("Error: @anchrd/intel-ui was not found in node_modules or packages/ui.");
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const rawConfig = await deps.readTextFile("intel.json");
|
|
52
|
+
let value = {};
|
|
53
|
+
if (rawConfig !== null) {
|
|
54
|
+
try {
|
|
55
|
+
value = JSON.parse(rawConfig);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
throw new Error("intel.json is not valid JSON");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const parsed = IntelConfig.safeParse(value);
|
|
62
|
+
if (!parsed.success)
|
|
63
|
+
throw new Error(validationMessage(parsed.error));
|
|
64
|
+
const config = parsed.data.ui;
|
|
65
|
+
const englishPath = `${location.dir}/src/i18n/en.json`;
|
|
66
|
+
const englishRaw = await readRequired(englishPath, "built-in English catalog");
|
|
67
|
+
const englishParsed = Catalog.safeParse(JSON.parse(englishRaw));
|
|
68
|
+
if (!englishParsed.success)
|
|
69
|
+
throw new Error("Intel's built-in English catalog is invalid");
|
|
70
|
+
const english = englishParsed.data;
|
|
71
|
+
const catalogs = {};
|
|
72
|
+
for (const [language, path] of Object.entries(config.languages)) {
|
|
73
|
+
const candidateRaw = await readRequired(path, `ui.languages.${language}`);
|
|
74
|
+
let candidateJson;
|
|
75
|
+
try {
|
|
76
|
+
candidateJson = JSON.parse(candidateRaw);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
throw new Error(`${path} is not valid JSON`);
|
|
80
|
+
}
|
|
81
|
+
const candidateParsed = Catalog.safeParse(candidateJson);
|
|
82
|
+
if (!candidateParsed.success)
|
|
83
|
+
throw new Error(`${path} must contain only string values`);
|
|
84
|
+
const difference = validateCatalog(english, candidateParsed.data);
|
|
85
|
+
if (difference.missing.length || difference.unknown.length) {
|
|
86
|
+
throw new Error(`${path} does not match en.json; missing: ${difference.missing.join(", ") || "none"}; unknown: ${difference.unknown.join(", ") || "none"}`);
|
|
87
|
+
}
|
|
88
|
+
catalogs[language] = candidateParsed.data;
|
|
89
|
+
}
|
|
90
|
+
if (config.defaultLanguage !== "en" && !(config.defaultLanguage in catalogs)) {
|
|
91
|
+
throw new Error(`ui.defaultLanguage ${JSON.stringify(config.defaultLanguage)} is not listed in ui.languages`);
|
|
92
|
+
}
|
|
93
|
+
const theme = config.theme
|
|
94
|
+
? `/* Generated by \`intel build\` from ${config.theme}. */\n${await readRequired(config.theme, "ui.theme")}\n`
|
|
95
|
+
: defaultTheme;
|
|
96
|
+
const logoSvg = config.logo ? await readSvg(config.logo, "ui.logo") : null;
|
|
97
|
+
const faviconPath = config.favicon ?? config.logo;
|
|
98
|
+
const faviconSvg = faviconPath
|
|
99
|
+
? await readSvg(faviconPath, config.favicon ? "ui.favicon" : "ui.logo")
|
|
100
|
+
: await readRequired(`${location.dir}/src/branding/favicon.default.svg`, "default favicon");
|
|
101
|
+
const logoModule = logoSvg
|
|
102
|
+
? `/* Generated by \`intel build\` from ${config.logo}. */\nexport const customLogoUrl = ${JSON.stringify(asSvgDataUrl(logoSvg))};\n`
|
|
103
|
+
: defaultLogo;
|
|
104
|
+
const languageBundle = `${JSON.stringify({ defaultLanguage: config.defaultLanguage, catalogs }, null, 2)}\n`;
|
|
105
|
+
await deps.writeTextFile(`${location.dir}/src/theme/custom.css`, theme);
|
|
106
|
+
await deps.writeTextFile(`${location.dir}/src/branding/custom-logo.ts`, logoModule);
|
|
107
|
+
await deps.writeTextFile(`${location.dir}/src/branding/favicon.svg`, faviconSvg);
|
|
108
|
+
await deps.writeTextFile(`${location.dir}/src/i18n/custom.json`, languageBundle);
|
|
109
|
+
const temporaryOutDir = `${location.outDir}.tmp`;
|
|
110
|
+
const exitCode = await deps.exec("npm", ["run", "build", "--", "--outDir", temporaryOutDir, "--emptyOutDir"], { cwd: location.dir });
|
|
111
|
+
if (exitCode !== 0) {
|
|
112
|
+
deps.log(`Error: Intel UI build exited with ${exitCode}.`);
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
115
|
+
await deps.replaceDir(temporaryOutDir, location.outDir);
|
|
116
|
+
deps.log(`Intel UI built at ${location.outDir}.`);
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
deps.log(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface UiLocation {
|
|
2
|
+
dir: string;
|
|
3
|
+
outDir: string;
|
|
4
|
+
}
|
|
5
|
+
export interface BuildDeps {
|
|
6
|
+
readTextFile(path: string): Promise<string | null>;
|
|
7
|
+
writeTextFile(path: string, content: string): Promise<void>;
|
|
8
|
+
resolveUi(): Promise<UiLocation | null>;
|
|
9
|
+
exec(command: string, args: string[], options: {
|
|
10
|
+
cwd: string;
|
|
11
|
+
}): Promise<number>;
|
|
12
|
+
replaceDir(from: string, to: string): Promise<void>;
|
|
13
|
+
log(message: string): void;
|
|
14
|
+
}
|
|
15
|
+
export interface BuildResult {
|
|
16
|
+
build(): Promise<number>;
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|