@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.
Files changed (114) hide show
  1. package/README.md +96 -0
  2. package/bin/intel.mjs +134 -0
  3. package/dist/adapters/cloudflare/cloudflare-flow-workflow.d.ts +21 -0
  4. package/dist/adapters/cloudflare/cloudflare-flow-workflow.js +39 -0
  5. package/dist/adapters/cloudflare/cloudflare.d.ts +8 -0
  6. package/dist/adapters/cloudflare/cloudflare.js +200 -0
  7. package/dist/adapters/cloudflare/cloudflare.types.d.ts +29 -0
  8. package/dist/adapters/cloudflare/cloudflare.types.js +1 -0
  9. package/dist/adapters/content/content.d.ts +3 -0
  10. package/dist/adapters/content/content.js +22 -0
  11. package/dist/adapters/content/content.types.d.ts +14 -0
  12. package/dist/adapters/content/content.types.js +1 -0
  13. package/dist/adapters/db/db-flows.d.ts +6 -0
  14. package/dist/adapters/db/db-flows.js +466 -0
  15. package/dist/adapters/db/db-indexing.d.ts +3 -0
  16. package/dist/adapters/db/db-indexing.js +70 -0
  17. package/dist/adapters/db/db-oauth.d.ts +3 -0
  18. package/dist/adapters/db/db-oauth.js +26 -0
  19. package/dist/adapters/db/db-tools.d.ts +6 -0
  20. package/dist/adapters/db/db-tools.js +200 -0
  21. package/dist/adapters/db/db.d.ts +6 -0
  22. package/dist/adapters/db/db.js +755 -0
  23. package/dist/adapters/db/db.types.d.ts +16 -0
  24. package/dist/adapters/db/db.types.js +1 -0
  25. package/dist/adapters/document-converter/document-converter.d.ts +2 -0
  26. package/dist/adapters/document-converter/document-converter.js +25 -0
  27. package/dist/adapters/document-converter/document-converter.types.d.ts +9 -0
  28. package/dist/adapters/document-converter/document-converter.types.js +1 -0
  29. package/dist/adapters/flow-runtime/flow-runtime.d.ts +3 -0
  30. package/dist/adapters/flow-runtime/flow-runtime.js +22 -0
  31. package/dist/adapters/flow-runtime/flow-runtime.types.d.ts +19 -0
  32. package/dist/adapters/flow-runtime/flow-runtime.types.js +1 -0
  33. package/dist/adapters/index-queue/index-queue.d.ts +10 -0
  34. package/dist/adapters/index-queue/index-queue.js +12 -0
  35. package/dist/adapters/index-queue/index-queue.types.d.ts +11 -0
  36. package/dist/adapters/index-queue/index-queue.types.js +1 -0
  37. package/dist/adapters/json-schema/json-schema.d.ts +2 -0
  38. package/dist/adapters/json-schema/json-schema.js +31 -0
  39. package/dist/adapters/openid/openid.d.ts +5 -0
  40. package/dist/adapters/openid/openid.js +159 -0
  41. package/dist/adapters/portal-tokens/portal-tokens.d.ts +8 -0
  42. package/dist/adapters/portal-tokens/portal-tokens.js +71 -0
  43. package/dist/adapters/remote-tools/remote-tools.d.ts +5 -0
  44. package/dist/adapters/remote-tools/remote-tools.js +109 -0
  45. package/dist/adapters/semantic-index/semantic-index.d.ts +2 -0
  46. package/dist/adapters/semantic-index/semantic-index.js +57 -0
  47. package/dist/adapters/semantic-index/semantic-index.types.d.ts +32 -0
  48. package/dist/adapters/semantic-index/semantic-index.types.js +1 -0
  49. package/dist/adapters/session-cookie/session-cookie.d.ts +5 -0
  50. package/dist/adapters/session-cookie/session-cookie.js +81 -0
  51. package/dist/adapters/tool-source-policy/tool-source-policy.d.ts +1 -0
  52. package/dist/adapters/tool-source-policy/tool-source-policy.js +22 -0
  53. package/dist/auth/auth.d.ts +2 -0
  54. package/dist/auth/auth.js +176 -0
  55. package/dist/auth/auth.types.d.ts +104 -0
  56. package/dist/auth/auth.types.js +1 -0
  57. package/dist/build/build.d.ts +2 -0
  58. package/dist/build/build.js +125 -0
  59. package/dist/build/build.types.d.ts +17 -0
  60. package/dist/build/build.types.js +1 -0
  61. package/dist/cli/cli.d.ts +4 -0
  62. package/dist/cli/cli.js +141 -0
  63. package/dist/cli/cli.types.d.ts +14 -0
  64. package/dist/cli/cli.types.js +1 -0
  65. package/dist/flows/flows.d.ts +4 -0
  66. package/dist/flows/flows.js +409 -0
  67. package/dist/flows/flows.types.d.ts +108 -0
  68. package/dist/flows/flows.types.js +1 -0
  69. package/dist/http/http.d.ts +8 -0
  70. package/dist/http/http.js +286 -0
  71. package/dist/http/http.types.d.ts +14 -0
  72. package/dist/http/http.types.js +1 -0
  73. package/dist/indexing/indexing.d.ts +6 -0
  74. package/dist/indexing/indexing.js +48 -0
  75. package/dist/indexing/indexing.types.d.ts +10 -0
  76. package/dist/indexing/indexing.types.js +1 -0
  77. package/dist/intel/intel.d.ts +3 -0
  78. package/dist/intel/intel.js +77 -0
  79. package/dist/intel/intel.types.d.ts +14 -0
  80. package/dist/intel/intel.types.js +1 -0
  81. package/dist/knowledge/knowledge.d.ts +2 -0
  82. package/dist/knowledge/knowledge.js +479 -0
  83. package/dist/knowledge/knowledge.types.d.ts +162 -0
  84. package/dist/knowledge/knowledge.types.js +1 -0
  85. package/dist/mcp/mcp.d.ts +2 -0
  86. package/dist/mcp/mcp.js +484 -0
  87. package/dist/mcp/mcp.types.d.ts +10 -0
  88. package/dist/mcp/mcp.types.js +1 -0
  89. package/dist/prepare/prepare.d.ts +4 -0
  90. package/dist/prepare/prepare.js +16 -0
  91. package/dist/prepare/prepare.types.d.ts +9 -0
  92. package/dist/prepare/prepare.types.js +1 -0
  93. package/dist/shared/gate-authorization/gate-authorization.d.ts +4 -0
  94. package/dist/shared/gate-authorization/gate-authorization.js +19 -0
  95. package/dist/shared/intel-error/intel-error.d.ts +5 -0
  96. package/dist/shared/intel-error/intel-error.js +10 -0
  97. package/dist/shared/problem-details/problem-details.d.ts +8 -0
  98. package/dist/shared/problem-details/problem-details.js +4 -0
  99. package/dist/shared/safe-return-path/safe-return-path.d.ts +2 -0
  100. package/dist/shared/safe-return-path/safe-return-path.js +11 -0
  101. package/dist/shared/sha256/sha256.d.ts +1 -0
  102. package/dist/shared/sha256/sha256.js +9 -0
  103. package/dist/tools/tools.d.ts +2 -0
  104. package/dist/tools/tools.js +107 -0
  105. package/dist/tools/tools.types.d.ts +56 -0
  106. package/dist/tools/tools.types.js +1 -0
  107. package/examples/branding/favicon.svg +5 -0
  108. package/examples/branding/logo.svg +5 -0
  109. package/examples/branding/theme.css +16 -0
  110. package/examples/dev.vars.example +7 -0
  111. package/examples/intel.json +9 -0
  112. package/migrations/0000_intel_foundation.sql +203 -0
  113. package/migrations/0001_portal_native_tools.sql +21 -0
  114. 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,2 @@
1
+ import type { BrowserAuth, BrowserAuthDeps } from "./auth.types.js";
2
+ export declare function createBrowserAuth(deps: BrowserAuthDeps): BrowserAuth;
@@ -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,2 @@
1
+ import type { BuildDeps, BuildResult } from "./build.types.js";
2
+ export declare function createBuild(deps: BuildDeps): BuildResult;
@@ -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 {};
@@ -0,0 +1,4 @@
1
+ import type { CliDeps } from "./cli.types.js";
2
+ export declare function createCli(deps: CliDeps): {
3
+ run(argv: string[]): Promise<number>;
4
+ };