@aistrike-dev/ui-shell-dev 0.0.0 → 1.0.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 CHANGED
@@ -1,3 +1,69 @@
1
1
  # @aistrike-dev/ui-shell-dev
2
2
 
3
- Placeholder. This version contains no code; it only reserves the package name so releases can be published from CI by npm trusted publishing. Install 1.0.0 or later.
3
+ Serves the AiStrike UI shell from your micro front-end's (MFE's) own Vite dev server, and mints dev tokens.
4
+
5
+ ## Setup
6
+
7
+ Install it as a devDependency, next to `@aistrike-dev/ui-shell-sdk`, and add `shellDevHost()` after `mfe()`:
8
+
9
+ ```ts
10
+ import { mfe } from '@aistrike-dev/ui-shell-sdk/vite';
11
+ import { shellDevHost } from '@aistrike-dev/ui-shell-dev/vite';
12
+ import { defineConfig } from 'vite';
13
+
14
+ export default defineConfig({
15
+ plugins: [mfe({ id: 'hello' }), shellDevHost()],
16
+ });
17
+ ```
18
+
19
+ `vite` now serves the shell at `/` with a minted dev session, and your MFE at `/ui/latest/`, on one origin, with HMR.
20
+ Other domains resolve by the profile's convention unless `shell.dev.json` overrides them.
21
+
22
+ Keep Vite's `server.allowedHosts` at its default: the plugin derives your MFE's origin from the request's `Host`
23
+ header, which is only safe while Vite rejects unknown hosts. With `server.https` Vite skips host validation, so the
24
+ self-override trusts the `Host` header as-is.
25
+
26
+ ## `shell.dev.json`
27
+
28
+ Read from the Vite root when present (or from the plugin's `devFile` option).
29
+
30
+ ```json
31
+ {
32
+ "profile": "local",
33
+ "overrides": { "other": "http://127.0.0.1:5102" },
34
+ "domains": [{ "id": "other", "route": "/other", "nav": { "label": "Other" }, "devPort": 5102 }],
35
+ "idleTimeoutMs": 0,
36
+ "claims": { "permissions": ["hello:read"] }
37
+ }
38
+ ```
39
+
40
+ `profile` defaults to `local`, `overrides` point other domains at their own dev servers (bare origins, no path; your
41
+ own domain is overridden automatically), `idleTimeoutMs` defaults to `0` (idle timeout off), and `claims` is merged
42
+ over the default dev claims. The plugin's `claims`, `ttlMs` and `noToken` options win over the file.
43
+
44
+ ## Commands
45
+
46
+ ```
47
+ aistrike-shell token [--claims <path>] [--ttl 30d|12h|45m|90s] [--out <path>] [--json]
48
+ ```
49
+
50
+ `token` mints a dev token and prints it (or `{ token, session }` with `--json`), optionally writing it to `--out`.
51
+
52
+ The package's own e2e (`pnpm nx e2e dev-host`) serves the settings MFE on port 5101 and fails loudly if a settings dev server already occupies it.
53
+
54
+ Opening `/` in dev lands on your own MFE (the plugin injects `landing` with your domain id; `shell.dev.json` may set `landing` to another domain id; an id that is not in the shell's domain list falls back to `/home`). The other domains in the profile (for example `home` on port 5102) show as failed in the rail unless their dev servers are running too; that is expected.
55
+
56
+ ## Checking Fast Refresh (spike 1)
57
+
58
+ This is a manual check; no automated test covers it. From the repository root:
59
+
60
+ 1. `pnpm nx build dev-host` once (builds the shell and copies it into the dev host package).
61
+ 2. `pnpm --filter @aistrike-dev/app-settings dev`.
62
+ 3. Open `http://localhost:5101/settings/profile`.
63
+ 4. Edit `apps/settings/src/ProfilePage.tsx` (for example, the `Profile` heading) and save.
64
+ 5. Confirm the page updates without a full reload: text typed into a field beforehand is still there.
65
+
66
+ ## The dev token
67
+
68
+ The dev token is signed with a dev-only key stored under `~/.config/aistrike-shell/`. It exists so the shell has a
69
+ session locally. Deployed environments never trust it.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import process from 'node:process';
3
+
4
+ const { main } = await import('../dist/cli.js');
5
+ process.exitCode = await main(process.argv.slice(2));
package/dist/args.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ export declare const USAGE = "Usage: aistrike-shell token [options]\n\nMint a dev token for local back ends.\n --claims <path> JSON claims for the token\n --ttl <30d|12h|45m|90s> token lifetime (default 30d)\n --out <path> also write the token to a file (mode 0600)\n --json print { token, session } instead of the bare token\n\nThe shell itself is served by shellDevHost() from @aistrike-dev/ui-shell-dev/vite:\nadd it to your MFE's vite.config.ts after mfe(), and \"vite\" serves the shell at /.\nShell settings (profile, overrides, domains, claims) live in shell.dev.json.\n";
2
+ export declare const DOMAIN_ID: RegExp;
3
+ export declare function parseTtl(text: string): number;
4
+ /** `id=origin`. The origin must be a bare origin: scheme, host, optional port, nothing else (spec 4.5). */
5
+ export declare function parseOverride(text: string): [string, string];
6
+ export interface TokenArgs {
7
+ claims?: string;
8
+ ttl?: string;
9
+ out?: string;
10
+ json: boolean;
11
+ help: boolean;
12
+ }
13
+ export declare function parseTokenArgs(argv: string[]): TokenArgs;
package/dist/args.js ADDED
@@ -0,0 +1,83 @@
1
+ export const USAGE = `Usage: aistrike-shell token [options]
2
+
3
+ Mint a dev token for local back ends.
4
+ --claims <path> JSON claims for the token
5
+ --ttl <30d|12h|45m|90s> token lifetime (default 30d)
6
+ --out <path> also write the token to a file (mode 0600)
7
+ --json print { token, session } instead of the bare token
8
+
9
+ The shell itself is served by shellDevHost() from @aistrike-dev/ui-shell-dev/vite:
10
+ add it to your MFE's vite.config.ts after mfe(), and "vite" serves the shell at /.
11
+ Shell settings (profile, overrides, domains, claims) live in shell.dev.json.
12
+ `;
13
+ const TTL = /^(\d+)([dhms])$/;
14
+ const UNIT_MS = { d: 86_400_000, h: 3_600_000, m: 60_000, s: 1000 };
15
+ // Mirrors the contract's DNS-label rule for domain ids. Duplicated (not imported) so the dev host has no runtime dependencies.
16
+ export const DOMAIN_ID = /^[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
17
+ const SCHEME = /^[a-z][a-z0-9+.-]*:\/\//i;
18
+ export function parseTtl(text) {
19
+ const match = TTL.exec(text);
20
+ const amount = match ? Number(match[1]) : 0;
21
+ if (!match || amount <= 0) {
22
+ throw new Error(`invalid ttl "${text}": use <n>d, <n>h, <n>m or <n>s`);
23
+ }
24
+ return amount * (UNIT_MS[match[2]] ?? 0);
25
+ }
26
+ /** `id=origin`. The origin must be a bare origin: scheme, host, optional port, nothing else (spec 4.5). */
27
+ export function parseOverride(text) {
28
+ const eq = text.indexOf('=');
29
+ if (eq === -1) {
30
+ throw new Error(`invalid override "${text}": expected id=origin`);
31
+ }
32
+ const id = text.slice(0, eq);
33
+ const raw = text.slice(eq + 1).replace(/\/+$/, '');
34
+ if (!DOMAIN_ID.test(id)) {
35
+ throw new Error(`invalid override id "${id}": must be a DNS label`);
36
+ }
37
+ if (!SCHEME.test(raw)) {
38
+ throw new Error(`invalid override origin "${raw}": must include a scheme, e.g. http://127.0.0.1:5102`);
39
+ }
40
+ let url;
41
+ try {
42
+ url = new URL(raw);
43
+ }
44
+ catch {
45
+ throw new Error(`invalid override origin "${raw}": not a valid URL`);
46
+ }
47
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
48
+ throw new Error(`invalid override origin "${raw}": must be http or https`);
49
+ }
50
+ if (url.origin !== raw || url.origin === 'null') {
51
+ throw new Error(`invalid override origin "${raw}": must be a bare origin without a path`);
52
+ }
53
+ return [id, url.origin];
54
+ }
55
+ function takeValue(argv, index, flag) {
56
+ const value = argv[index + 1];
57
+ if (value === undefined || value.startsWith('--')) {
58
+ throw new Error(`${flag} needs a value`);
59
+ }
60
+ return value;
61
+ }
62
+ const TOKEN_VALUE_FLAGS = new Map([['--claims', 'claims'], ['--ttl', 'ttl'], ['--out', 'out']]);
63
+ export function parseTokenArgs(argv) {
64
+ const args = { json: false, help: false };
65
+ for (let i = 0; i < argv.length; i += 1) {
66
+ const flag = argv[i];
67
+ const key = TOKEN_VALUE_FLAGS.get(flag);
68
+ if (flag === '--help') {
69
+ args.help = true;
70
+ }
71
+ else if (flag === '--json') {
72
+ args.json = true;
73
+ }
74
+ else if (key) {
75
+ args[key] = takeValue(argv, i, flag);
76
+ i += 1;
77
+ }
78
+ else {
79
+ throw new Error(`unknown option "${flag}"`);
80
+ }
81
+ }
82
+ return args;
83
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ export interface CliIo {
2
+ stdout: (text: string) => void;
3
+ stderr: (text: string) => void;
4
+ cwd?: string;
5
+ keyDir?: string;
6
+ }
7
+ export declare function main(argv: string[], io?: CliIo): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,65 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import process from 'node:process';
4
+ import { USAGE, parseTokenArgs, parseTtl } from './args.js';
5
+ import { assertClaimsInput } from './devfile.js';
6
+ import { DEFAULT_TTL_MS, loadOrCreateKey, mintToken } from './token.js';
7
+ const defaultIo = { stdout: (t) => process.stdout.write(t), stderr: (t) => process.stderr.write(t) };
8
+ function readClaims(path, cwd, fallback) {
9
+ if (path === undefined) {
10
+ return fallback;
11
+ }
12
+ const file = resolve(cwd, path);
13
+ if (!existsSync(file)) {
14
+ throw new Error(`claims file not found: ${path}`);
15
+ }
16
+ let parsed;
17
+ try {
18
+ parsed = JSON.parse(readFileSync(file, 'utf8'));
19
+ }
20
+ catch (error) {
21
+ throw new Error(`claims file ${path} is not valid JSON: ${String(error)}`, { cause: error });
22
+ }
23
+ return assertClaimsInput(parsed, `--claims ${path}`);
24
+ }
25
+ function mint(io, claims, ttl) {
26
+ const key = loadOrCreateKey(io.keyDir);
27
+ return mintToken({ key, claims, ttlMs: ttl === undefined ? DEFAULT_TTL_MS : parseTtl(ttl) }).session;
28
+ }
29
+ function runToken(argv, io) {
30
+ const args = parseTokenArgs(argv);
31
+ if (args.help) {
32
+ io.stdout(USAGE);
33
+ return 0;
34
+ }
35
+ const cwd = io.cwd ?? process.cwd();
36
+ const session = mint(io, readClaims(args.claims, cwd, undefined), args.ttl);
37
+ const output = args.json ? `${JSON.stringify({ token: session.accessToken, session }, null, 2)}\n` : `${session.accessToken}\n`;
38
+ if (args.out !== undefined) {
39
+ // The token is a credential for local back ends: owner read/write only.
40
+ writeFileSync(resolve(cwd, args.out), output, { mode: 0o600 });
41
+ }
42
+ io.stdout(output);
43
+ return 0;
44
+ }
45
+ export async function main(argv, io = defaultIo) {
46
+ const [command, ...rest] = argv;
47
+ try {
48
+ switch (command) {
49
+ case undefined:
50
+ case '--help':
51
+ case 'help':
52
+ io.stdout(USAGE);
53
+ return 0;
54
+ case 'token':
55
+ return runToken(rest, io);
56
+ default:
57
+ io.stderr(`unknown command "${command}"\n\n${USAGE}`);
58
+ return 2;
59
+ }
60
+ }
61
+ catch (error) {
62
+ io.stderr(`${error instanceof Error ? error.message : String(error)}\n`);
63
+ return 2;
64
+ }
65
+ }
@@ -0,0 +1,27 @@
1
+ import type { DevConfig, DevDomain } from '@aistrike-dev/ui-shell-sdk/shell';
2
+ import type { Session } from '@aistrike-dev/ui-shell-sdk';
3
+ import type { DevClaimsInput } from './token.js';
4
+ export declare const DEV_FILE = "shell.dev.json";
5
+ export interface DevFile {
6
+ profile?: string;
7
+ overrides?: {
8
+ [id: string]: string;
9
+ };
10
+ domains?: DevDomain[];
11
+ idleTimeoutMs?: number;
12
+ landing?: string;
13
+ claims?: DevClaimsInput;
14
+ }
15
+ /** Dev claims must be a plain object of known keys; `iss`/`exp`/`iat` and friends cannot be smuggled into the token. */
16
+ export declare function assertClaimsInput(value: unknown, source: string): DevClaimsInput;
17
+ /** An explicit `--config` must exist; the default `shell.dev.json` is optional. */
18
+ export declare function loadDevFile(path: string | undefined, cwd?: string): DevFile | null;
19
+ export interface DevOverrides {
20
+ profile?: string;
21
+ landing?: string;
22
+ overrides: {
23
+ [id: string]: string;
24
+ };
25
+ }
26
+ /** Explicit overrides win over the file; the profile defaults to `local`; the idle timeout defaults to off for local work. */
27
+ export declare function buildDevConfig(file: DevFile | null, args: DevOverrides, session: Session | undefined): DevConfig;
@@ -0,0 +1,182 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import process from 'node:process';
4
+ import { DOMAIN_ID, parseOverride } from './args.js';
5
+ export const DEV_FILE = 'shell.dev.json';
6
+ const CLAIM_STRINGS = ['sub', 'name', 'defaultTenant'];
7
+ const CLAIM_LISTS = ['permissions', 'modules', 'tenants'];
8
+ /** Dev claims must be a plain object of known keys; `iss`/`exp`/`iat` and friends cannot be smuggled into the token. */
9
+ export function assertClaimsInput(value, source) {
10
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
11
+ throw new Error(`${source}: claims must be an object`);
12
+ }
13
+ for (const [key, entry] of Object.entries(value)) {
14
+ if (CLAIM_STRINGS.includes(key)) {
15
+ if (typeof entry !== 'string') {
16
+ throw new Error(`${source}: ${key} must be a string`);
17
+ }
18
+ }
19
+ else if (CLAIM_LISTS.includes(key)) {
20
+ if (!Array.isArray(entry) || !entry.every((item) => typeof item === 'string')) {
21
+ throw new Error(`${source}: ${key} must be an array of strings`);
22
+ }
23
+ }
24
+ else {
25
+ throw new Error(`${source}: unknown claim "${key}"`);
26
+ }
27
+ }
28
+ return value;
29
+ }
30
+ function normalizeOverrides(overrides) {
31
+ if (typeof overrides !== 'object' || overrides === null || Array.isArray(overrides)) {
32
+ throw new Error('overrides must be an object of id: origin');
33
+ }
34
+ const result = {};
35
+ for (const [id, origin] of Object.entries(overrides)) {
36
+ if (typeof origin !== 'string') {
37
+ throw new Error(`override "${id}" must be a string origin`);
38
+ }
39
+ const [, normalized] = parseOverride(`${id}=${origin}`);
40
+ result[id] = normalized;
41
+ }
42
+ return result;
43
+ }
44
+ const FILE_KEYS = ['profile', 'overrides', 'domains', 'idleTimeoutMs', 'landing', 'claims'];
45
+ const DOMAIN_KEYS = ['id', 'route', 'nav', 'devPort'];
46
+ const NAV_KEYS = ['label', 'icon'];
47
+ const ROUTE = /^\/[a-z0-9-]+$/;
48
+ function isPlainObject(value) {
49
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
50
+ }
51
+ function rejectUnknownKeys(value, allowed, path) {
52
+ for (const key of Object.keys(value)) {
53
+ if (!allowed.includes(key)) {
54
+ throw new Error(`unknown key "${key}"${path === '' ? '' : ` in ${path}`}`);
55
+ }
56
+ }
57
+ }
58
+ function validateNav(nav, path) {
59
+ if (!isPlainObject(nav)) {
60
+ throw new Error(`${path} must be an object`);
61
+ }
62
+ rejectUnknownKeys(nav, NAV_KEYS, path);
63
+ const { label, icon } = nav;
64
+ if (typeof label !== 'string') {
65
+ throw new Error(`${path}.label must be a string`);
66
+ }
67
+ if (icon === undefined) {
68
+ return { label };
69
+ }
70
+ if (typeof icon !== 'string') {
71
+ throw new Error(`${path}.icon must be a string`);
72
+ }
73
+ return { label, icon };
74
+ }
75
+ function isPort(value) {
76
+ return typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= 65535;
77
+ }
78
+ function validateDomain(value, path) {
79
+ if (!isPlainObject(value)) {
80
+ throw new Error(`${path} must be an object`);
81
+ }
82
+ rejectUnknownKeys(value, DOMAIN_KEYS, path);
83
+ const { id, route, nav, devPort } = value;
84
+ if (typeof id !== 'string' || !DOMAIN_ID.test(id)) {
85
+ throw new Error(`${path}.id must be a DNS label`);
86
+ }
87
+ if (typeof route !== 'string' || !ROUTE.test(route)) {
88
+ throw new Error(`${path}.route must look like /name`);
89
+ }
90
+ const validNav = validateNav(nav, `${path}.nav`);
91
+ if (!isPort(devPort)) {
92
+ throw new Error(`${path}.devPort must be an integer from 1 to 65535`);
93
+ }
94
+ return { id, route, nav: validNav, devPort };
95
+ }
96
+ function validateDomains(value) {
97
+ if (!Array.isArray(value)) {
98
+ throw new Error('domains must be an array');
99
+ }
100
+ return value.map((entry, index) => validateDomain(entry, `domains[${index}]`));
101
+ }
102
+ function validateIdle(value) {
103
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
104
+ throw new Error('idleTimeoutMs must be a finite number of at least 0');
105
+ }
106
+ return value;
107
+ }
108
+ function validateLanding(value) {
109
+ if (typeof value !== 'string' || value === '') {
110
+ throw new Error('landing must be a non-empty domain id');
111
+ }
112
+ return value;
113
+ }
114
+ function readExtras(parsed, result) {
115
+ if (parsed.idleTimeoutMs !== undefined) {
116
+ result.idleTimeoutMs = validateIdle(parsed.idleTimeoutMs);
117
+ }
118
+ if (parsed.landing !== undefined) {
119
+ result.landing = validateLanding(parsed.landing);
120
+ }
121
+ if (parsed.claims !== undefined) {
122
+ result.claims = assertClaimsInput(parsed.claims, 'claims');
123
+ }
124
+ return result;
125
+ }
126
+ function validateFile(parsed, name) {
127
+ try {
128
+ if (!isPlainObject(parsed)) {
129
+ throw new Error('must be a JSON object');
130
+ }
131
+ rejectUnknownKeys(parsed, FILE_KEYS, '');
132
+ const result = {};
133
+ if (parsed.profile !== undefined) {
134
+ if (typeof parsed.profile !== 'string') {
135
+ throw new Error('profile must be a string');
136
+ }
137
+ result.profile = parsed.profile;
138
+ }
139
+ if (parsed.overrides !== undefined) {
140
+ result.overrides = normalizeOverrides(parsed.overrides);
141
+ }
142
+ if (parsed.domains !== undefined) {
143
+ result.domains = validateDomains(parsed.domains);
144
+ }
145
+ return readExtras(parsed, result);
146
+ }
147
+ catch (error) {
148
+ throw new Error(`${name}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
149
+ }
150
+ }
151
+ /** An explicit `--config` must exist; the default `shell.dev.json` is optional. */
152
+ export function loadDevFile(path, cwd = process.cwd()) {
153
+ const file = resolve(cwd, path ?? DEV_FILE);
154
+ if (!existsSync(file)) {
155
+ if (path === undefined) {
156
+ return null;
157
+ }
158
+ throw new Error(`dev file not found: ${path}`);
159
+ }
160
+ let parsed;
161
+ try {
162
+ parsed = JSON.parse(readFileSync(file, 'utf8'));
163
+ }
164
+ catch (error) {
165
+ throw new Error(`dev file ${path ?? DEV_FILE} is not valid JSON: ${String(error)}`, { cause: error });
166
+ }
167
+ return validateFile(parsed, path ?? DEV_FILE);
168
+ }
169
+ function landingOf(file, args) {
170
+ return args.landing ?? file?.landing;
171
+ }
172
+ /** Explicit overrides win over the file; the profile defaults to `local`; the idle timeout defaults to off for local work. */
173
+ export function buildDevConfig(file, args, session) {
174
+ return {
175
+ profile: args.profile ?? file?.profile ?? 'local',
176
+ overrides: { ...(file?.overrides ?? {}), ...args.overrides },
177
+ domains: file?.domains ?? [],
178
+ idleTimeoutMs: file?.idleTimeoutMs ?? 0,
179
+ landing: landingOf(file, args),
180
+ session,
181
+ };
182
+ }
@@ -0,0 +1,2 @@
1
+ /** Makes JSON safe to embed inside a <script> element; the result is still valid JSON. */
2
+ export declare function escapeJsonForScript(json: string): string;
package/dist/escape.js ADDED
@@ -0,0 +1,12 @@
1
+ /* eslint-disable @typescript-eslint/naming-convention -- keys are the literal characters being escaped */
2
+ const REPLACEMENTS = {
3
+ '<': '\\u003c',
4
+ '>': '\\u003e',
5
+ '&': '\\u0026',
6
+ '\u2028': '\\u2028',
7
+ '\u2029': '\\u2029',
8
+ };
9
+ /** Makes JSON safe to embed inside a <script> element; the result is still valid JSON. */
10
+ export function escapeJsonForScript(json) {
11
+ return json.replace(/[<>&\u2028\u2029]/g, (char) => REPLACEMENTS[char] ?? char);
12
+ }
@@ -0,0 +1,9 @@
1
+ export { DEFAULT_CLAIMS, DEFAULT_TTL_MS, DEV_AUDIENCE, DEV_ISSUER, configDir, decodeToken, loadOrCreateKey, mintToken, verifyToken, } from './token.js';
2
+ export type { DevClaimsInput, DevKey, MintOptions, Minted } from './token.js';
3
+ export { escapeJsonForScript } from './escape.js';
4
+ export { main } from './cli.js';
5
+ export type { CliIo } from './cli.js';
6
+ export { buildDevConfig, loadDevFile } from './devfile.js';
7
+ export type { DevFile, DevOverrides } from './devfile.js';
8
+ export { parseOverride, parseTokenArgs, parseTtl } from './args.js';
9
+ export type { TokenArgs } from './args.js';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { DEFAULT_CLAIMS, DEFAULT_TTL_MS, DEV_AUDIENCE, DEV_ISSUER, configDir, decodeToken, loadOrCreateKey, mintToken, verifyToken, } from './token.js';
2
+ export { escapeJsonForScript } from './escape.js';
3
+ export { main } from './cli.js';
4
+ export { buildDevConfig, loadDevFile } from './devfile.js';
5
+ export { parseOverride, parseTokenArgs, parseTtl } from './args.js';
@@ -0,0 +1,12 @@
1
+ import type { ServerResponse } from 'node:http';
2
+ import type { DevConfig } from '@aistrike-dev/ui-shell-sdk/shell';
3
+ /** Mirrors the contract's DEV_CONFIG_GLOBAL (the contract is a devDependency, types only; a test asserts equality). */
4
+ export declare const DEV_CONFIG_GLOBAL = "__AISTRIKE_SHELL_DEV__";
5
+ export declare const CONTENT_TYPES: {
6
+ [ext: string]: string;
7
+ };
8
+ export declare function validateDevConfig(devConfig: DevConfig): void;
9
+ /** Puts `window.__AISTRIKE_SHELL_DEV__ = <escaped json>` before the page's first module script. */
10
+ export declare function injectDevConfig(html: string, devConfig: DevConfig, source: string): string;
11
+ /** Streams a file with its content type and `no-store`, or answers 404. */
12
+ export declare function sendFile(file: string, res: ServerResponse): void;
package/dist/inject.js ADDED
@@ -0,0 +1,55 @@
1
+ /* eslint-disable @typescript-eslint/naming-convention -- file extensions and HTTP header names are fixed external keys */
2
+ import { createReadStream, existsSync, statSync } from 'node:fs';
3
+ import { extname } from 'node:path';
4
+ import { escapeJsonForScript } from './escape.js';
5
+ /** Mirrors the contract's DEV_CONFIG_GLOBAL (the contract is a devDependency, types only; a test asserts equality). */
6
+ export const DEV_CONFIG_GLOBAL = '__AISTRIKE_SHELL_DEV__';
7
+ export const CONTENT_TYPES = {
8
+ '.html': 'text/html; charset=utf-8',
9
+ '.js': 'text/javascript; charset=utf-8',
10
+ '.mjs': 'text/javascript; charset=utf-8',
11
+ '.css': 'text/css; charset=utf-8',
12
+ '.json': 'application/json; charset=utf-8',
13
+ '.svg': 'image/svg+xml',
14
+ '.png': 'image/png',
15
+ '.ico': 'image/x-icon',
16
+ '.woff2': 'font/woff2',
17
+ '.txt': 'text/plain; charset=utf-8',
18
+ };
19
+ export function validateDevConfig(devConfig) {
20
+ if (devConfig.profile === undefined || devConfig.profile === '') {
21
+ throw new Error('dev config must name a profile');
22
+ }
23
+ if (devConfig.session !== undefined && !(devConfig.session.expiresAt > Date.now())) {
24
+ throw new Error('dev session is already expired; mint a new token');
25
+ }
26
+ }
27
+ /** Puts `window.__AISTRIKE_SHELL_DEV__ = <escaped json>` before the page's first module script. */
28
+ export function injectDevConfig(html, devConfig, source) {
29
+ const marker = '<script type="module"';
30
+ const at = html.indexOf(marker);
31
+ if (at === -1) {
32
+ throw new Error(`${source} has no module script to inject before`);
33
+ }
34
+ const tag = `<script>window.${DEV_CONFIG_GLOBAL} = ${escapeJsonForScript(JSON.stringify(devConfig))};</script>\n `;
35
+ return `${html.slice(0, at)}${tag}${html.slice(at)}`;
36
+ }
37
+ /** Streams a file with its content type and `no-store`, or answers 404. */
38
+ export function sendFile(file, res) {
39
+ if (!existsSync(file) || !statSync(file).isFile()) {
40
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
41
+ res.end('not found');
42
+ return;
43
+ }
44
+ res.writeHead(200, { 'Content-Type': CONTENT_TYPES[extname(file)] ?? 'application/octet-stream', 'Cache-Control': 'no-store' });
45
+ createReadStream(file)
46
+ .on('error', () => {
47
+ if (res.headersSent) {
48
+ res.destroy();
49
+ return;
50
+ }
51
+ res.writeHead(500);
52
+ res.end('error');
53
+ })
54
+ .pipe(res);
55
+ }
@@ -0,0 +1,44 @@
1
+ import type { Session } from '@aistrike-dev/ui-shell-sdk';
2
+ export declare const DEV_ISSUER = "aistrike-dev-host";
3
+ export declare const DEV_AUDIENCE = "aistrike-dev";
4
+ export declare const DEFAULT_TTL_MS: number;
5
+ export interface DevKey {
6
+ kid: string;
7
+ privateKeyPem: string;
8
+ publicKeyPem: string;
9
+ }
10
+ export interface DevClaimsInput {
11
+ sub?: string;
12
+ name?: string;
13
+ permissions?: string[];
14
+ modules?: string[];
15
+ tenants?: string[];
16
+ defaultTenant?: string;
17
+ }
18
+ export declare const DEFAULT_CLAIMS: {
19
+ sub: string;
20
+ name: string;
21
+ permissions: string[];
22
+ modules: string[];
23
+ tenants: string[];
24
+ defaultTenant: string;
25
+ };
26
+ export declare function configDir(env?: NodeJS.ProcessEnv): string;
27
+ /** One ES256 key per machine. No deployed environment trusts it; it exists so local back ends can verify if they want to. */
28
+ export declare function loadOrCreateKey(dir?: string): DevKey;
29
+ export interface MintOptions {
30
+ key: DevKey;
31
+ claims?: DevClaimsInput;
32
+ ttlMs?: number;
33
+ now?: number;
34
+ }
35
+ export interface Minted {
36
+ token: string;
37
+ session: Session;
38
+ }
39
+ export declare function mintToken({ key, claims, ttlMs, now }: MintOptions): Minted;
40
+ export declare function decodeToken(token: string): {
41
+ header: unknown;
42
+ payload: unknown;
43
+ };
44
+ export declare function verifyToken(token: string, publicKeyPem: string): boolean;