@ekanos/cli 0.1.6 → 0.1.7
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 +2 -2
- package/dist/commands/init.d.ts +36 -0
- package/dist/commands/init.js +126 -10
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/upgrade.js +108 -7
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/copy-tree.d.ts +48 -0
- package/dist/copy-tree.js +55 -0
- package/dist/copy-tree.js.map +1 -0
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/parse-argv.d.ts +7 -1
- package/dist/parse-argv.js +10 -2
- package/dist/parse-argv.js.map +1 -1
- package/dist/project-checks.d.ts +9 -0
- package/dist/project-checks.js +29 -0
- package/dist/project-checks.js.map +1 -1
- package/dist/project.d.ts +23 -0
- package/dist/project.js +66 -1
- package/dist/project.js.map +1 -1
- package/dist/publish-api.d.ts +1 -1
- package/dist/publish-api.js +2 -2
- package/dist/publish-api.js.map +1 -1
- package/dist/safe-fs.d.ts +1 -1
- package/dist/safe-fs.js.map +1 -1
- package/dist/sources-api.d.ts +1 -1
- package/dist/sources-api.js.map +1 -1
- package/dist/validate-findings.js +2 -1
- package/dist/validate-findings.js.map +1 -1
- package/package.json +1 -1
- package/templates/AGENTS.md.tmpl +57 -0
- package/templates/claude-skill.md.tmpl +7 -0
- package/templates/docs/api-notes.md.tmpl +21 -0
- package/templates/mcp.json.tmpl +8 -0
- package/templates/skills/build-integration/SKILL.md +178 -0
- package/templates/skills/build-integration/references/api-docs-research.md +93 -0
- package/templates/skills/build-integration/references/api-wiring.md +94 -0
- package/templates/skills/build-integration/references/widget-design.md +132 -0
- package/templates/skills/polish-widgets/SKILL.md +139 -0
- package/templates/skills/polish-widgets/references/patterns.md +269 -0
package/dist/project-checks.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Finding } from '@ekanos/integration-schema';
|
|
2
|
+
import { type EkanosProject } from './project.js';
|
|
2
3
|
/**
|
|
3
4
|
* Project-level integrity checks that read files in the project root (next to
|
|
4
5
|
* ekanos.json), NOT the integration definition. These are deliberately kept
|
|
@@ -16,3 +17,11 @@ import type { Finding } from '@ekanos/integration-schema';
|
|
|
16
17
|
* runner on its own). If a config IS present, it MUST inline the SDK.
|
|
17
18
|
*/
|
|
18
19
|
export declare function collectProjectFindings(projectDir: string): Finding[];
|
|
20
|
+
/**
|
|
21
|
+
* Advisory check on the `docs` array: a relative entry that resolves to
|
|
22
|
+
* nothing on disk gets a WARNING — never an error, because a missing doc file
|
|
23
|
+
* must not block a publish (the gate refuses only error-severity findings).
|
|
24
|
+
* URLs are never checked: `validate` does no network, ever, and an agent
|
|
25
|
+
* dereferences them at authoring time instead.
|
|
26
|
+
*/
|
|
27
|
+
export declare function collectDocsFindings(project: EkanosProject, projectDir: string): Finding[];
|
package/dist/project-checks.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
+
import { isDocUrl } from './project.js';
|
|
3
4
|
const VITEST_CONFIG_NAMES = [
|
|
4
5
|
'vitest.config.ts',
|
|
5
6
|
'vitest.config.mts',
|
|
@@ -60,6 +61,34 @@ export function collectProjectFindings(projectDir) {
|
|
|
60
61
|
},
|
|
61
62
|
];
|
|
62
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Advisory check on the `docs` array: a relative entry that resolves to
|
|
66
|
+
* nothing on disk gets a WARNING — never an error, because a missing doc file
|
|
67
|
+
* must not block a publish (the gate refuses only error-severity findings).
|
|
68
|
+
* URLs are never checked: `validate` does no network, ever, and an agent
|
|
69
|
+
* dereferences them at authoring time instead.
|
|
70
|
+
*/
|
|
71
|
+
export function collectDocsFindings(project, projectDir) {
|
|
72
|
+
var _a;
|
|
73
|
+
const findings = [];
|
|
74
|
+
for (const entry of (_a = project.docs) !== null && _a !== void 0 ? _a : []) {
|
|
75
|
+
if (isDocUrl(entry))
|
|
76
|
+
continue;
|
|
77
|
+
const resolved = path.resolve(projectDir, entry);
|
|
78
|
+
if (fs.existsSync(resolved))
|
|
79
|
+
continue;
|
|
80
|
+
findings.push({
|
|
81
|
+
check: 'docs/missing-file',
|
|
82
|
+
severity: 'warn',
|
|
83
|
+
file: resolved,
|
|
84
|
+
message: `ekanos.json lists "${entry}" under "docs", but no file exists ` +
|
|
85
|
+
`there (resolved to ${resolved}).`,
|
|
86
|
+
hint: `Create ${entry}, or fix the path in ekanos.json's "docs" array so ` +
|
|
87
|
+
'it points at real API documentation.',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return findings;
|
|
91
|
+
}
|
|
63
92
|
/**
|
|
64
93
|
* True when the config source contains an `inline: [ … ]` array that lists
|
|
65
94
|
* `@ekanos/sdk`. Textual on purpose — evaluating a partner's config module
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-checks.js","sourceRoot":"","sources":["../src/project-checks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,MAAM,mBAAmB,GAAG;IAC1B,kBAAkB;IAClB,mBAAmB;IACnB,mBAAmB;IACnB,kBAAkB;IAClB,mBAAmB;IACnB,mBAAmB;IACnB,gBAAgB;IAChB,iBAAiB;IACjB,gBAAgB;IAChB,iBAAiB;CAClB,CAAC;AAEF,gFAAgF;AAChF,MAAM,qBAAqB,GAAG;IAC5B,SAAS;IACT,aAAa;IACb,aAAa;IACb,gCAAgC;IAChC,QAAQ;IACR,MAAM;IACN,GAAG;CACJ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,sBAAsB,CAAC,UAAkB;IACvD,MAAM,UAAU,GAAG,mBAAmB,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAClD,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAC5B,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;IAEhD,IAAI,CAAC,UAAU;QAAE,OAAO,EAAE,CAAC;IAE3B,MAAM,MAAM,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IACnD,IAAI,gBAAgB,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,CAAC;IAExC,OAAO;QACL;YACE,KAAK,EAAE,2BAA2B;YAClC,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,UAAU;YAChB,OAAO,EACL,GAAG,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,oCAAoC;gBAChE,mEAAmE;gBACnE,sEAAsE;gBACtE,mEAAmE;gBACnE,kBAAkB;YACpB,IAAI,EACF,8DAA8D;gBAC9D,uCAAuC,qBAAqB,EAAE;SACjE;KACF,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAS,gBAAgB,CAAC,MAAc;;IACtC,MAAM,kBAAkB,GAAG,8BAA8B,CAAC;IAC1D,KACE,IAAI,KAAK,GAAG,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,EAC3C,KAAK,KAAK,IAAI,EACd,KAAK,GAAG,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,EACvC,CAAC;QACD,IAAI,sBAAsB,CAAC,IAAI,CAAC,MAAA,KAAK,CAAC,CAAC,CAAC,mCAAI,EAAE,CAAC;YAAE,OAAO,IAAI,CAAC;IAC/D,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["import type { Finding } from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nconst VITEST_CONFIG_NAMES = [\n 'vitest.config.ts',\n 'vitest.config.mts',\n 'vitest.config.cts',\n 'vitest.config.js',\n 'vitest.config.mjs',\n 'vitest.config.cjs',\n 'vite.config.ts',\n 'vite.config.mts',\n 'vite.config.js',\n 'vite.config.mjs',\n];\n\n/** The exact block the hint quotes — the one config that makes the SDK load. */\nconst REQUIRED_INLINE_BLOCK = [\n 'test: {',\n ' server: {',\n ' deps: {',\n \" inline: ['@ekanos/sdk'],\",\n ' },',\n ' },',\n '}',\n].join('\\n');\n\n/**\n * Project-level integrity checks that read files in the project root (next to\n * ekanos.json), NOT the integration definition. These are deliberately kept\n * out of the schema package's definition collectors: the definition is valid or\n * not on its own; whether the partner's TEST runner is configured to load the\n * published SDK is a property of THIS project.\n *\n * The highest-value check: a partner's vitest must inline `@ekanos/sdk`. The\n * published SDK dist imports extensionless `next/link`, so without\n * `test.server.deps.inline: ['@ekanos/sdk']` vitest fails to load the SDK, the\n * test file silently imports nothing, and vitest reports green over zero tests.\n *\n * The check is conditional: if the project declares NO vitest/vite config at\n * all, there is nothing to assert (the `test` verb would surface a missing\n * runner on its own). If a config IS present, it MUST inline the SDK.\n */\nexport function collectProjectFindings(projectDir: string): Finding[] {\n const configFile = VITEST_CONFIG_NAMES.map((name) =>\n path.join(projectDir, name),\n ).find((candidate) => fs.existsSync(candidate));\n\n if (!configFile) return [];\n\n const source = fs.readFileSync(configFile, 'utf8');\n if (vitestInlinesSdk(source)) return [];\n\n return [\n {\n check: 'project.vitest-inline-sdk',\n severity: 'error',\n file: configFile,\n message:\n `${path.basename(configFile)} does not inline @ekanos/sdk. The ` +\n `published SDK dist imports extensionless \"next/link\", so without ` +\n `test.server.deps.inline including \"@ekanos/sdk\", vitest cannot load ` +\n `the SDK — the test file imports nothing and vitest reports green ` +\n `over zero tests.`,\n hint:\n `Add this to your vitest config's defineConfig so the SDK is ` +\n `transformed for the test runner:\\n\\n${REQUIRED_INLINE_BLOCK}`,\n },\n ];\n}\n\n/**\n * True when the config source contains an `inline: [ … ]` array that lists\n * `@ekanos/sdk`. Textual on purpose — evaluating a partner's config module\n * would run arbitrary code; a lint-style scan does not.\n */\nfunction vitestInlinesSdk(source: string): boolean {\n const inlineArrayPattern = /inline\\s*:\\s*\\[([\\s\\S]*?)\\]/g;\n for (\n let match = inlineArrayPattern.exec(source);\n match !== null;\n match = inlineArrayPattern.exec(source)\n ) {\n if (/['\"]@ekanos\\/sdk['\"]/.test(match[1] ?? '')) return true;\n }\n return false;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"project-checks.js","sourceRoot":"","sources":["../src/project-checks.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Finding } from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { type EkanosProject, isDocUrl } from './project';\n\nconst VITEST_CONFIG_NAMES = [\n 'vitest.config.ts',\n 'vitest.config.mts',\n 'vitest.config.cts',\n 'vitest.config.js',\n 'vitest.config.mjs',\n 'vitest.config.cjs',\n 'vite.config.ts',\n 'vite.config.mts',\n 'vite.config.js',\n 'vite.config.mjs',\n];\n\n/** The exact block the hint quotes — the one config that makes the SDK load. */\nconst REQUIRED_INLINE_BLOCK = [\n 'test: {',\n ' server: {',\n ' deps: {',\n \" inline: ['@ekanos/sdk'],\",\n ' },',\n ' },',\n '}',\n].join('\\n');\n\n/**\n * Project-level integrity checks that read files in the project root (next to\n * ekanos.json), NOT the integration definition. These are deliberately kept\n * out of the schema package's definition collectors: the definition is valid or\n * not on its own; whether the partner's TEST runner is configured to load the\n * published SDK is a property of THIS project.\n *\n * The highest-value check: a partner's vitest must inline `@ekanos/sdk`. The\n * published SDK dist imports extensionless `next/link`, so without\n * `test.server.deps.inline: ['@ekanos/sdk']` vitest fails to load the SDK, the\n * test file silently imports nothing, and vitest reports green over zero tests.\n *\n * The check is conditional: if the project declares NO vitest/vite config at\n * all, there is nothing to assert (the `test` verb would surface a missing\n * runner on its own). If a config IS present, it MUST inline the SDK.\n */\nexport function collectProjectFindings(projectDir: string): Finding[] {\n const configFile = VITEST_CONFIG_NAMES.map((name) =>\n path.join(projectDir, name),\n ).find((candidate) => fs.existsSync(candidate));\n\n if (!configFile) return [];\n\n const source = fs.readFileSync(configFile, 'utf8');\n if (vitestInlinesSdk(source)) return [];\n\n return [\n {\n check: 'project.vitest-inline-sdk',\n severity: 'error',\n file: configFile,\n message:\n `${path.basename(configFile)} does not inline @ekanos/sdk. The ` +\n `published SDK dist imports extensionless \"next/link\", so without ` +\n `test.server.deps.inline including \"@ekanos/sdk\", vitest cannot load ` +\n `the SDK — the test file imports nothing and vitest reports green ` +\n `over zero tests.`,\n hint:\n `Add this to your vitest config's defineConfig so the SDK is ` +\n `transformed for the test runner:\\n\\n${REQUIRED_INLINE_BLOCK}`,\n },\n ];\n}\n\n/**\n * Advisory check on the `docs` array: a relative entry that resolves to\n * nothing on disk gets a WARNING — never an error, because a missing doc file\n * must not block a publish (the gate refuses only error-severity findings).\n * URLs are never checked: `validate` does no network, ever, and an agent\n * dereferences them at authoring time instead.\n */\nexport function collectDocsFindings(\n project: EkanosProject,\n projectDir: string,\n): Finding[] {\n const findings: Finding[] = [];\n\n for (const entry of project.docs ?? []) {\n if (isDocUrl(entry)) continue;\n\n const resolved = path.resolve(projectDir, entry);\n if (fs.existsSync(resolved)) continue;\n\n findings.push({\n check: 'docs/missing-file',\n severity: 'warn',\n file: resolved,\n message:\n `ekanos.json lists \"${entry}\" under \"docs\", but no file exists ` +\n `there (resolved to ${resolved}).`,\n hint:\n `Create ${entry}, or fix the path in ekanos.json's \"docs\" array so ` +\n 'it points at real API documentation.',\n });\n }\n\n return findings;\n}\n\n/**\n * True when the config source contains an `inline: [ … ]` array that lists\n * `@ekanos/sdk`. Textual on purpose — evaluating a partner's config module\n * would run arbitrary code; a lint-style scan does not.\n */\nfunction vitestInlinesSdk(source: string): boolean {\n const inlineArrayPattern = /inline\\s*:\\s*\\[([\\s\\S]*?)\\]/g;\n for (\n let match = inlineArrayPattern.exec(source);\n match !== null;\n match = inlineArrayPattern.exec(source)\n ) {\n if (/['\"]@ekanos\\/sdk['\"]/.test(match[1] ?? '')) return true;\n }\n return false;\n}\n"]}
|
package/dist/project.d.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
/** True when a `docs` entry is a URL (never touched by local checks). */
|
|
3
|
+
export declare function isDocUrl(entry: string): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Why a `docs` entry is invalid, or null when it is fine. One rule, shared by
|
|
6
|
+
* the schema below and `init --docs`, so "valid on the command line" and
|
|
7
|
+
* "valid in ekanos.json" cannot drift apart: an entry is either an http(s)
|
|
8
|
+
* URL or a path relative to the project root. Absolute paths are rejected
|
|
9
|
+
* because they are meaningless on anyone else's machine, and non-http(s)
|
|
10
|
+
* schemes because nothing here will ever dereference them.
|
|
11
|
+
*/
|
|
12
|
+
export declare function docEntryProblem(entry: string): string | null;
|
|
2
13
|
/** One integration: the pair that addresses a definition module. */
|
|
3
14
|
declare const IntegrationEntrySchema: z.ZodObject<{
|
|
4
15
|
slug: z.ZodString;
|
|
@@ -37,6 +48,14 @@ export declare const EkanosProjectSchema: z.ZodEffects<z.ZodObject<{
|
|
|
37
48
|
* fallback after `--host` and `EKANOS_HOST`.
|
|
38
49
|
*/
|
|
39
50
|
host: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
51
|
+
/**
|
|
52
|
+
* Where the target API's documentation lives — http(s) URLs and
|
|
53
|
+
* project-relative paths, in the order an agent should try them. Written
|
|
54
|
+
* by `init --docs`, read by agents (see AGENTS.md's "API documentation")
|
|
55
|
+
* and advisory-checked by `validate`. Optional and allowed empty: a
|
|
56
|
+
* project that predates the field stays valid.
|
|
57
|
+
*/
|
|
58
|
+
docs: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
40
59
|
sourceGlobs: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>, "many">>;
|
|
41
60
|
}, "strict", z.ZodTypeAny, {
|
|
42
61
|
slug?: string | undefined;
|
|
@@ -48,6 +67,7 @@ export declare const EkanosProjectSchema: z.ZodEffects<z.ZodObject<{
|
|
|
48
67
|
}[] | undefined;
|
|
49
68
|
source?: string | undefined;
|
|
50
69
|
host?: string | undefined;
|
|
70
|
+
docs?: string[] | undefined;
|
|
51
71
|
sourceGlobs?: string[] | undefined;
|
|
52
72
|
}, {
|
|
53
73
|
slug?: string | undefined;
|
|
@@ -59,6 +79,7 @@ export declare const EkanosProjectSchema: z.ZodEffects<z.ZodObject<{
|
|
|
59
79
|
}[] | undefined;
|
|
60
80
|
source?: string | undefined;
|
|
61
81
|
host?: string | undefined;
|
|
82
|
+
docs?: string[] | undefined;
|
|
62
83
|
sourceGlobs?: string[] | undefined;
|
|
63
84
|
}>, {
|
|
64
85
|
slug?: string | undefined;
|
|
@@ -70,6 +91,7 @@ export declare const EkanosProjectSchema: z.ZodEffects<z.ZodObject<{
|
|
|
70
91
|
}[] | undefined;
|
|
71
92
|
source?: string | undefined;
|
|
72
93
|
host?: string | undefined;
|
|
94
|
+
docs?: string[] | undefined;
|
|
73
95
|
sourceGlobs?: string[] | undefined;
|
|
74
96
|
}, {
|
|
75
97
|
slug?: string | undefined;
|
|
@@ -81,6 +103,7 @@ export declare const EkanosProjectSchema: z.ZodEffects<z.ZodObject<{
|
|
|
81
103
|
}[] | undefined;
|
|
82
104
|
source?: string | undefined;
|
|
83
105
|
host?: string | undefined;
|
|
106
|
+
docs?: string[] | undefined;
|
|
84
107
|
sourceGlobs?: string[] | undefined;
|
|
85
108
|
}>;
|
|
86
109
|
export type EkanosProject = z.infer<typeof EkanosProjectSchema>;
|
package/dist/project.js
CHANGED
|
@@ -34,6 +34,63 @@ const SourceGlobSchema = z
|
|
|
34
34
|
.refine((glob) => !glob.split('/').includes('..'), {
|
|
35
35
|
message: 'a sourceGlobs entry must not escape the project root with "..".',
|
|
36
36
|
});
|
|
37
|
+
/** Anything with a scheme prefix — `https:`, `file:`, a Windows drive `C:`. */
|
|
38
|
+
const URL_SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/i;
|
|
39
|
+
/** True when a `docs` entry is a URL (never touched by local checks). */
|
|
40
|
+
export function isDocUrl(entry) {
|
|
41
|
+
return /^https?:\/\//i.test(entry);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Why a `docs` entry is invalid, or null when it is fine. One rule, shared by
|
|
45
|
+
* the schema below and `init --docs`, so "valid on the command line" and
|
|
46
|
+
* "valid in ekanos.json" cannot drift apart: an entry is either an http(s)
|
|
47
|
+
* URL or a path relative to the project root. Absolute paths are rejected
|
|
48
|
+
* because they are meaningless on anyone else's machine, and non-http(s)
|
|
49
|
+
* schemes because nothing here will ever dereference them.
|
|
50
|
+
*/
|
|
51
|
+
export function docEntryProblem(entry) {
|
|
52
|
+
if (entry.trim() === '')
|
|
53
|
+
return 'a docs entry must not be empty.';
|
|
54
|
+
if (URL_SCHEME_PATTERN.test(entry)) {
|
|
55
|
+
if (!isDocUrl(entry)) {
|
|
56
|
+
return (`"${entry}" is neither an http(s) URL nor a relative path — a docs ` +
|
|
57
|
+
'entry is a URL to API documentation or a path relative to the ' +
|
|
58
|
+
'project root.');
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
new URL(entry);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
catch (_a) {
|
|
65
|
+
return `"${entry}" is not a valid http(s) URL.`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (path.isAbsolute(entry)) {
|
|
69
|
+
return (`"${entry}" is an absolute path — a docs entry that is a file must be ` +
|
|
70
|
+
'relative to the project root, so it means the same thing on every ' +
|
|
71
|
+
'machine that clones the project.');
|
|
72
|
+
}
|
|
73
|
+
// Same rule as SourceGlobSchema, for the same reason: a `..` segment lets a
|
|
74
|
+
// relative entry escape the project root, and `validate` resolves these
|
|
75
|
+
// paths for the missing-file check.
|
|
76
|
+
if (entry.split(/[\\/]/).includes('..')) {
|
|
77
|
+
return (`"${entry}" escapes the project root with ".." — a docs entry that is ` +
|
|
78
|
+
'a file must stay inside the project.');
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* One `docs` entry: where the target API's documentation lives. An http(s)
|
|
84
|
+
* URL (an OpenAPI spec, the official docs site) or a project-relative path
|
|
85
|
+
* (a vendored spec, `docs/api-notes.md`). Local paths are advisory-checked by
|
|
86
|
+
* `validate` (`docs/missing-file`, a warning); URLs are never fetched.
|
|
87
|
+
*/
|
|
88
|
+
const DocEntrySchema = z.string().superRefine((entry, ctx) => {
|
|
89
|
+
const problem = docEntryProblem(entry);
|
|
90
|
+
if (problem) {
|
|
91
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: problem });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
37
94
|
/** One integration: the pair that addresses a definition module. */
|
|
38
95
|
const IntegrationEntrySchema = z
|
|
39
96
|
.object({ slug: SlugSchema, entry: EntrySchema })
|
|
@@ -108,6 +165,14 @@ export const EkanosProjectSchema = z
|
|
|
108
165
|
* fallback after `--host` and `EKANOS_HOST`.
|
|
109
166
|
*/
|
|
110
167
|
host: HostSchema.optional(),
|
|
168
|
+
/**
|
|
169
|
+
* Where the target API's documentation lives — http(s) URLs and
|
|
170
|
+
* project-relative paths, in the order an agent should try them. Written
|
|
171
|
+
* by `init --docs`, read by agents (see AGENTS.md's "API documentation")
|
|
172
|
+
* and advisory-checked by `validate`. Optional and allowed empty: a
|
|
173
|
+
* project that predates the field stays valid.
|
|
174
|
+
*/
|
|
175
|
+
docs: z.array(DocEntrySchema).optional(),
|
|
111
176
|
// There is deliberately no `harness` field. One existed, accepted by the
|
|
112
177
|
// schema and read by nothing — the same accept-and-ignore that made
|
|
113
178
|
// `sourceGlobs` cost a partner a silently unstyled integration. `.strict()`
|
|
@@ -199,7 +264,7 @@ export function loadProject(dir) {
|
|
|
199
264
|
const where = first && first.path.length > 0 ? first.path.join('.') : '(root)';
|
|
200
265
|
throw preconditionError(`${EKANOS_CONFIG_FILENAME} is invalid at "${where}": ${(_a = first === null || first === void 0 ? void 0 : first.message) !== null && _a !== void 0 ? _a : 'schema violation'}`, `Correct ${configPath} so it matches the ekanos.json schema ` +
|
|
201
266
|
`({ slug, entry } or { integrations: [...] }, plus optional ` +
|
|
202
|
-
`source, host and sourceGlobs).`);
|
|
267
|
+
`source, host, docs and sourceGlobs).`);
|
|
203
268
|
}
|
|
204
269
|
const project = result.data;
|
|
205
270
|
const integrations = projectIntegrations(project).map((declared) => {
|
package/dist/project.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project.js","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { z } from 'zod';\n\nimport { notFoundError, preconditionError } from './errors';\n\nconst SlugSchema = z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, {\n message:\n 'slug must be kebab-case ([a-z0-9] segments separated by single hyphens), e.g. \"acme-crm\".',\n});\n\nconst EntrySchema = z\n .string()\n .min(1, { message: 'entry must be a path to the integration module.' });\n\n/**\n * Extra Tailwind content globs, for an integration that does not live under\n * the default layout. They are interpolated into the generated\n * `styles/globals.css` as `@source` lines, so they are validated as\n * DATA-IN-CSS: project-relative, no escaping the project, and none of the\n * characters that could close the string or the surrounding comment.\n *\n * Being strict here is deliberate. A glob that matches nothing costs a partner\n * a silently unstyled integration; a glob that is rejected costs them one\n * clear error.\n */\nconst SOURCE_GLOB_PATTERN = /^[A-Za-z0-9_\\-./*{},[\\]!]+$/;\n\nconst SourceGlobSchema = z\n .string()\n .min(1, { message: 'a sourceGlobs entry must not be empty.' })\n .max(200, { message: 'a sourceGlobs entry must be under 200 characters.' })\n .regex(SOURCE_GLOB_PATTERN, {\n message:\n 'a sourceGlobs entry may contain only letters, digits and _-./*{},[]! ' +\n '— it is written verbatim into the generated stylesheet.',\n })\n .refine((glob) => !glob.startsWith('/'), {\n message: 'a sourceGlobs entry must be relative to the project root.',\n })\n .refine((glob) => !glob.split('/').includes('..'), {\n message: 'a sourceGlobs entry must not escape the project root with \"..\".',\n });\n\n/** One integration: the pair that addresses a definition module. */\nconst IntegrationEntrySchema = z\n .object({ slug: SlugSchema, entry: EntrySchema })\n .strict();\n\nexport type IntegrationEntry = z.infer<typeof IntegrationEntrySchema>;\n\n/**\n * `ekanos.json` — the project contract. Written by `init`, read by `validate`,\n * `dev` and `test`.\n *\n * Two shapes, because a project can hold more than one integration and the\n * single-integration form is what `init` writes and what most projects keep:\n *\n * ```json\n * { \"slug\": \"acme-crm\", \"entry\": \"src/integration.ts\" }\n *\n * { \"integrations\": [\n * { \"slug\": \"acme-crm\", \"entry\": \"src/crm.ts\" },\n * { \"slug\": \"acme-billing\", \"entry\": \"src/billing.ts\" }\n * ] }\n * ```\n *\n * A second integration used to be invisible to the CLI entirely — the harness\n * registry is an array and `collectCollisionFindings` exists precisely to\n * cross-check several definitions against each other, but `validate` could\n * only ever see one. Both forms normalise to the same list, so nothing\n * downstream has to know which was written.\n */\n/**\n * The Fusion source this project publishes to, by slug. Fusion source slugs\n * are opaque server-side identifiers, so the only local claims are \"one\n * token, sanely sized\" — the server resolves and authorizes it.\n */\nconst SourceSlugSchema = z\n .string()\n .min(1, { message: 'source must not be empty.' })\n .max(255, { message: 'source must be at most 255 characters.' })\n .regex(/^\\S+$/, { message: 'source must not contain whitespace.' });\n\n/**\n * The Fusion host this project publishes to. Origin-shaped: the same\n * normalization the credential store applies (`normalizeHost`) accepts it, so\n * \"valid here\" and \"usable as a session key\" cannot drift apart.\n */\nconst HostSchema = z\n .string()\n .min(1, { message: 'host must not be empty.' })\n .max(255, { message: 'host must be at most 255 characters.' })\n .refine(\n (value) => {\n try {\n const url = new URL(value);\n return url.protocol === 'https:' || url.protocol === 'http:';\n } catch {\n return false;\n }\n },\n {\n message:\n 'host must be a full http(s) origin, e.g. \"https://app.example.com\".',\n },\n );\n\nexport const EkanosProjectSchema = z\n .object({\n $schema: z.string().optional(),\n slug: SlugSchema.optional(),\n entry: EntrySchema.optional(),\n integrations: z.array(IntegrationEntrySchema).optional(),\n /**\n * Written by the first successful `ekanos publish --source <slug>` so\n * later publishes need no flag. Read only by `publish`.\n */\n source: SourceSlugSchema.optional(),\n /**\n * The Fusion host this project publishes to — an http(s) origin, e.g.\n * \"https://app.example.com\". Written by the first successful `ekanos\n * publish` (alongside `source`), read by every host-taking verb as the\n * fallback after `--host` and `EKANOS_HOST`.\n */\n host: HostSchema.optional(),\n // There is deliberately no `harness` field. One existed, accepted by the\n // schema and read by nothing — the same accept-and-ignore that made\n // `sourceGlobs` cost a partner a silently unstyled integration. `.strict()`\n // now rejects it by name, which is the honest answer until something\n // actually consumes it.\n sourceGlobs: z.array(SourceGlobSchema).optional(),\n })\n .strict()\n .superRefine((value, ctx) => {\n const hasSingle = value.slug !== undefined || value.entry !== undefined;\n const hasList = value.integrations !== undefined;\n\n if (hasSingle && hasList) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message:\n 'use either the single form ({ slug, entry }) or the list form ' +\n '({ integrations: [...] }), not both — two places to declare the ' +\n 'same thing is how they drift apart.',\n });\n return;\n }\n\n if (hasList) {\n if (value.integrations!.length === 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['integrations'],\n message: 'integrations must list at least one integration.',\n });\n }\n const seen = new Set<string>();\n value.integrations!.forEach((entry, index) => {\n if (seen.has(entry.slug)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['integrations', index, 'slug'],\n message:\n `duplicate slug \"${entry.slug}\" — every integration in a ` +\n 'project needs its own, since the slug addresses it everywhere.',\n });\n }\n seen.add(entry.slug);\n });\n return;\n }\n\n if (value.slug === undefined) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['slug'],\n message: 'slug is required (or use \"integrations\": [...]).',\n });\n }\n if (value.entry === undefined) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['entry'],\n message: 'entry is required (or use \"integrations\": [...]).',\n });\n }\n });\n\nexport type EkanosProject = z.infer<typeof EkanosProjectSchema>;\n\n/** Both `ekanos.json` shapes, flattened to the one the CLI works in. */\nexport function projectIntegrations(\n project: EkanosProject,\n): IntegrationEntry[] {\n if (project.integrations !== undefined) return [...project.integrations];\n return [{ slug: project.slug!, entry: project.entry! }];\n}\n\nexport const EKANOS_CONFIG_FILENAME = 'ekanos.json';\n\n/** One integration from `ekanos.json`, with its entry resolved on disk. */\nexport interface LoadedIntegration {\n slug: string;\n entry: string;\n /** Absolute path to the resolved entry module. */\n entryPath: string;\n}\n\nexport interface LoadedProject {\n project: EkanosProject;\n /** Absolute path to the directory containing ekanos.json (the project root). */\n projectDir: string;\n /** Absolute path to ekanos.json itself. */\n configPath: string;\n /** Every integration the project declares, in declaration order. */\n integrations: LoadedIntegration[];\n /**\n * The first integration. It names the generated harness shell and is what a\n * single-integration project means by \"the\" integration; nothing else\n * privileges it.\n */\n primary: LoadedIntegration;\n}\n\n/**\n * Read and validate ekanos.json from a directory. Missing file → not-found\n * (exit 6); malformed JSON or a schema violation → precondition failed\n * (exit 9), because a project the CLI cannot even read is a precondition for\n * every verb, not a definition-level validation finding.\n */\nexport function loadProject(dir: string): LoadedProject {\n const projectDir = path.resolve(dir);\n const configPath = path.join(projectDir, EKANOS_CONFIG_FILENAME);\n\n if (!fs.existsSync(configPath)) {\n throw notFoundError(\n `No ${EKANOS_CONFIG_FILENAME} found in ${projectDir}.`,\n `Run \"ekanos init\" here first, or change into the project directory ` +\n `that contains ${EKANOS_CONFIG_FILENAME}.`,\n );\n }\n\n let raw: unknown;\n try {\n raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));\n } catch (error) {\n throw preconditionError(\n `${EKANOS_CONFIG_FILENAME} is not valid JSON: ${\n error instanceof Error ? error.message : String(error)\n }`,\n `Fix the JSON syntax in ${configPath}.`,\n );\n }\n\n const result = EkanosProjectSchema.safeParse(raw);\n if (!result.success) {\n const first = result.error.issues[0];\n const where =\n first && first.path.length > 0 ? first.path.join('.') : '(root)';\n throw preconditionError(\n `${EKANOS_CONFIG_FILENAME} is invalid at \"${where}\": ${\n first?.message ?? 'schema violation'\n }`,\n `Correct ${configPath} so it matches the ekanos.json schema ` +\n `({ slug, entry } or { integrations: [...] }, plus optional ` +\n `source, host and sourceGlobs).`,\n );\n }\n\n const project = result.data;\n\n const integrations = projectIntegrations(project).map((declared) => {\n const entryPath = path.resolve(projectDir, declared.entry);\n if (!fs.existsSync(entryPath)) {\n throw notFoundError(\n `entry \"${declared.entry}\" for \"${declared.slug}\" resolves to ` +\n `${entryPath}, which does not exist.`,\n `Point it at your integration module (the file that exports the ` +\n `defineIntegration(...) result) in ${configPath}.`,\n );\n }\n return { slug: declared.slug, entry: declared.entry, entryPath };\n });\n\n assertEsmProject(projectDir);\n\n return {\n project,\n projectDir,\n configPath,\n integrations,\n primary: integrations[0]!,\n };\n}\n\n/**\n * Fail unless the project is an ES module package.\n *\n * `init` sets `\"type\": \"module\"` and says so, but a warning can be scrolled\n * past, and a partner who misses it gets a confusing module-resolution error\n * from deep inside Node or Next rather than a sentence naming the problem.\n * This is the backstop: one precondition, in the loader every verb after\n * `init` goes through, so the failure is loud and says what to do.\n *\n * `init` deliberately does NOT come through here — it is the thing that fixes\n * this state.\n *\n * A project with no package.json at all is left alone: `dev` reports missing\n * dependencies with its own better message, and `validate`/`test` have their\n * own preconditions. We only assert on a manifest that exists and disagrees.\n */\nexport function assertEsmProject(projectDir: string): void {\n const manifestPath = path.join(projectDir, 'package.json');\n if (!fs.existsSync(manifestPath)) return;\n\n let manifest: unknown;\n try {\n manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));\n } catch {\n // Malformed JSON is npm's problem to report, not ours to guess at; the\n // partner will hit it on their next install with a better message.\n return;\n }\n\n const declared =\n typeof manifest === 'object' && manifest !== null\n ? (manifest as { type?: unknown }).type\n : undefined;\n\n if (declared === 'module') return;\n\n throw preconditionError(\n declared === undefined\n ? `${manifestPath} does not declare \"type\": \"module\", so Node treats ` +\n 'your integration as CommonJS and cannot load it.'\n : `${manifestPath} declares \"type\": ${JSON.stringify(declared)}, but the ` +\n 'integration sources are ES modules and Node cannot load them.',\n `Set \"type\": \"module\" in ${manifestPath}. If this project has CommonJS ` +\n 'sources of its own, put your integration in its own package instead — ' +\n 'the two module systems cannot share one package.json. Re-running ' +\n '\"ekanos init\" also fixes this.',\n );\n}\n\n/** Serialize an ekanos.json body with a trailing newline. */\nexport function serializeProject(project: EkanosProject): string {\n return `${JSON.stringify(project, null, 2)}\\n`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"project.js","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { z } from 'zod';\n\nimport { notFoundError, preconditionError } from './errors';\n\nconst SlugSchema = z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, {\n message:\n 'slug must be kebab-case ([a-z0-9] segments separated by single hyphens), e.g. \"acme-crm\".',\n});\n\nconst EntrySchema = z\n .string()\n .min(1, { message: 'entry must be a path to the integration module.' });\n\n/**\n * Extra Tailwind content globs, for an integration that does not live under\n * the default layout. They are interpolated into the generated\n * `styles/globals.css` as `@source` lines, so they are validated as\n * DATA-IN-CSS: project-relative, no escaping the project, and none of the\n * characters that could close the string or the surrounding comment.\n *\n * Being strict here is deliberate. A glob that matches nothing costs a partner\n * a silently unstyled integration; a glob that is rejected costs them one\n * clear error.\n */\nconst SOURCE_GLOB_PATTERN = /^[A-Za-z0-9_\\-./*{},[\\]!]+$/;\n\nconst SourceGlobSchema = z\n .string()\n .min(1, { message: 'a sourceGlobs entry must not be empty.' })\n .max(200, { message: 'a sourceGlobs entry must be under 200 characters.' })\n .regex(SOURCE_GLOB_PATTERN, {\n message:\n 'a sourceGlobs entry may contain only letters, digits and _-./*{},[]! ' +\n '— it is written verbatim into the generated stylesheet.',\n })\n .refine((glob) => !glob.startsWith('/'), {\n message: 'a sourceGlobs entry must be relative to the project root.',\n })\n .refine((glob) => !glob.split('/').includes('..'), {\n message: 'a sourceGlobs entry must not escape the project root with \"..\".',\n });\n\n/** Anything with a scheme prefix — `https:`, `file:`, a Windows drive `C:`. */\nconst URL_SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/i;\n\n/** True when a `docs` entry is a URL (never touched by local checks). */\nexport function isDocUrl(entry: string): boolean {\n return /^https?:\\/\\//i.test(entry);\n}\n\n/**\n * Why a `docs` entry is invalid, or null when it is fine. One rule, shared by\n * the schema below and `init --docs`, so \"valid on the command line\" and\n * \"valid in ekanos.json\" cannot drift apart: an entry is either an http(s)\n * URL or a path relative to the project root. Absolute paths are rejected\n * because they are meaningless on anyone else's machine, and non-http(s)\n * schemes because nothing here will ever dereference them.\n */\nexport function docEntryProblem(entry: string): string | null {\n if (entry.trim() === '') return 'a docs entry must not be empty.';\n\n if (URL_SCHEME_PATTERN.test(entry)) {\n if (!isDocUrl(entry)) {\n return (\n `\"${entry}\" is neither an http(s) URL nor a relative path — a docs ` +\n 'entry is a URL to API documentation or a path relative to the ' +\n 'project root.'\n );\n }\n try {\n new URL(entry);\n return null;\n } catch {\n return `\"${entry}\" is not a valid http(s) URL.`;\n }\n }\n\n if (path.isAbsolute(entry)) {\n return (\n `\"${entry}\" is an absolute path — a docs entry that is a file must be ` +\n 'relative to the project root, so it means the same thing on every ' +\n 'machine that clones the project.'\n );\n }\n\n // Same rule as SourceGlobSchema, for the same reason: a `..` segment lets a\n // relative entry escape the project root, and `validate` resolves these\n // paths for the missing-file check.\n if (entry.split(/[\\\\/]/).includes('..')) {\n return (\n `\"${entry}\" escapes the project root with \"..\" — a docs entry that is ` +\n 'a file must stay inside the project.'\n );\n }\n\n return null;\n}\n\n/**\n * One `docs` entry: where the target API's documentation lives. An http(s)\n * URL (an OpenAPI spec, the official docs site) or a project-relative path\n * (a vendored spec, `docs/api-notes.md`). Local paths are advisory-checked by\n * `validate` (`docs/missing-file`, a warning); URLs are never fetched.\n */\nconst DocEntrySchema = z.string().superRefine((entry, ctx) => {\n const problem = docEntryProblem(entry);\n if (problem) {\n ctx.addIssue({ code: z.ZodIssueCode.custom, message: problem });\n }\n});\n\n/** One integration: the pair that addresses a definition module. */\nconst IntegrationEntrySchema = z\n .object({ slug: SlugSchema, entry: EntrySchema })\n .strict();\n\nexport type IntegrationEntry = z.infer<typeof IntegrationEntrySchema>;\n\n/**\n * `ekanos.json` — the project contract. Written by `init`, read by `validate`,\n * `dev` and `test`.\n *\n * Two shapes, because a project can hold more than one integration and the\n * single-integration form is what `init` writes and what most projects keep:\n *\n * ```json\n * { \"slug\": \"acme-crm\", \"entry\": \"src/integration.ts\" }\n *\n * { \"integrations\": [\n * { \"slug\": \"acme-crm\", \"entry\": \"src/crm.ts\" },\n * { \"slug\": \"acme-billing\", \"entry\": \"src/billing.ts\" }\n * ] }\n * ```\n *\n * A second integration used to be invisible to the CLI entirely — the harness\n * registry is an array and `collectCollisionFindings` exists precisely to\n * cross-check several definitions against each other, but `validate` could\n * only ever see one. Both forms normalise to the same list, so nothing\n * downstream has to know which was written.\n */\n/**\n * The Fusion source this project publishes to, by slug. Fusion source slugs\n * are opaque server-side identifiers, so the only local claims are \"one\n * token, sanely sized\" — the server resolves and authorizes it.\n */\nconst SourceSlugSchema = z\n .string()\n .min(1, { message: 'source must not be empty.' })\n .max(255, { message: 'source must be at most 255 characters.' })\n .regex(/^\\S+$/, { message: 'source must not contain whitespace.' });\n\n/**\n * The Fusion host this project publishes to. Origin-shaped: the same\n * normalization the credential store applies (`normalizeHost`) accepts it, so\n * \"valid here\" and \"usable as a session key\" cannot drift apart.\n */\nconst HostSchema = z\n .string()\n .min(1, { message: 'host must not be empty.' })\n .max(255, { message: 'host must be at most 255 characters.' })\n .refine(\n (value) => {\n try {\n const url = new URL(value);\n return url.protocol === 'https:' || url.protocol === 'http:';\n } catch {\n return false;\n }\n },\n {\n message:\n 'host must be a full http(s) origin, e.g. \"https://app.example.com\".',\n },\n );\n\nexport const EkanosProjectSchema = z\n .object({\n $schema: z.string().optional(),\n slug: SlugSchema.optional(),\n entry: EntrySchema.optional(),\n integrations: z.array(IntegrationEntrySchema).optional(),\n /**\n * Written by the first successful `ekanos publish --source <slug>` so\n * later publishes need no flag. Read only by `publish`.\n */\n source: SourceSlugSchema.optional(),\n /**\n * The Fusion host this project publishes to — an http(s) origin, e.g.\n * \"https://app.example.com\". Written by the first successful `ekanos\n * publish` (alongside `source`), read by every host-taking verb as the\n * fallback after `--host` and `EKANOS_HOST`.\n */\n host: HostSchema.optional(),\n /**\n * Where the target API's documentation lives — http(s) URLs and\n * project-relative paths, in the order an agent should try them. Written\n * by `init --docs`, read by agents (see AGENTS.md's \"API documentation\")\n * and advisory-checked by `validate`. Optional and allowed empty: a\n * project that predates the field stays valid.\n */\n docs: z.array(DocEntrySchema).optional(),\n // There is deliberately no `harness` field. One existed, accepted by the\n // schema and read by nothing — the same accept-and-ignore that made\n // `sourceGlobs` cost a partner a silently unstyled integration. `.strict()`\n // now rejects it by name, which is the honest answer until something\n // actually consumes it.\n sourceGlobs: z.array(SourceGlobSchema).optional(),\n })\n .strict()\n .superRefine((value, ctx) => {\n const hasSingle = value.slug !== undefined || value.entry !== undefined;\n const hasList = value.integrations !== undefined;\n\n if (hasSingle && hasList) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message:\n 'use either the single form ({ slug, entry }) or the list form ' +\n '({ integrations: [...] }), not both — two places to declare the ' +\n 'same thing is how they drift apart.',\n });\n return;\n }\n\n if (hasList) {\n if (value.integrations!.length === 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['integrations'],\n message: 'integrations must list at least one integration.',\n });\n }\n const seen = new Set<string>();\n value.integrations!.forEach((entry, index) => {\n if (seen.has(entry.slug)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['integrations', index, 'slug'],\n message:\n `duplicate slug \"${entry.slug}\" — every integration in a ` +\n 'project needs its own, since the slug addresses it everywhere.',\n });\n }\n seen.add(entry.slug);\n });\n return;\n }\n\n if (value.slug === undefined) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['slug'],\n message: 'slug is required (or use \"integrations\": [...]).',\n });\n }\n if (value.entry === undefined) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['entry'],\n message: 'entry is required (or use \"integrations\": [...]).',\n });\n }\n });\n\nexport type EkanosProject = z.infer<typeof EkanosProjectSchema>;\n\n/** Both `ekanos.json` shapes, flattened to the one the CLI works in. */\nexport function projectIntegrations(\n project: EkanosProject,\n): IntegrationEntry[] {\n if (project.integrations !== undefined) return [...project.integrations];\n return [{ slug: project.slug!, entry: project.entry! }];\n}\n\nexport const EKANOS_CONFIG_FILENAME = 'ekanos.json';\n\n/** One integration from `ekanos.json`, with its entry resolved on disk. */\nexport interface LoadedIntegration {\n slug: string;\n entry: string;\n /** Absolute path to the resolved entry module. */\n entryPath: string;\n}\n\nexport interface LoadedProject {\n project: EkanosProject;\n /** Absolute path to the directory containing ekanos.json (the project root). */\n projectDir: string;\n /** Absolute path to ekanos.json itself. */\n configPath: string;\n /** Every integration the project declares, in declaration order. */\n integrations: LoadedIntegration[];\n /**\n * The first integration. It names the generated harness shell and is what a\n * single-integration project means by \"the\" integration; nothing else\n * privileges it.\n */\n primary: LoadedIntegration;\n}\n\n/**\n * Read and validate ekanos.json from a directory. Missing file → not-found\n * (exit 6); malformed JSON or a schema violation → precondition failed\n * (exit 9), because a project the CLI cannot even read is a precondition for\n * every verb, not a definition-level validation finding.\n */\nexport function loadProject(dir: string): LoadedProject {\n const projectDir = path.resolve(dir);\n const configPath = path.join(projectDir, EKANOS_CONFIG_FILENAME);\n\n if (!fs.existsSync(configPath)) {\n throw notFoundError(\n `No ${EKANOS_CONFIG_FILENAME} found in ${projectDir}.`,\n `Run \"ekanos init\" here first, or change into the project directory ` +\n `that contains ${EKANOS_CONFIG_FILENAME}.`,\n );\n }\n\n let raw: unknown;\n try {\n raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));\n } catch (error) {\n throw preconditionError(\n `${EKANOS_CONFIG_FILENAME} is not valid JSON: ${\n error instanceof Error ? error.message : String(error)\n }`,\n `Fix the JSON syntax in ${configPath}.`,\n );\n }\n\n const result = EkanosProjectSchema.safeParse(raw);\n if (!result.success) {\n const first = result.error.issues[0];\n const where =\n first && first.path.length > 0 ? first.path.join('.') : '(root)';\n throw preconditionError(\n `${EKANOS_CONFIG_FILENAME} is invalid at \"${where}\": ${\n first?.message ?? 'schema violation'\n }`,\n `Correct ${configPath} so it matches the ekanos.json schema ` +\n `({ slug, entry } or { integrations: [...] }, plus optional ` +\n `source, host, docs and sourceGlobs).`,\n );\n }\n\n const project = result.data;\n\n const integrations = projectIntegrations(project).map((declared) => {\n const entryPath = path.resolve(projectDir, declared.entry);\n if (!fs.existsSync(entryPath)) {\n throw notFoundError(\n `entry \"${declared.entry}\" for \"${declared.slug}\" resolves to ` +\n `${entryPath}, which does not exist.`,\n `Point it at your integration module (the file that exports the ` +\n `defineIntegration(...) result) in ${configPath}.`,\n );\n }\n return { slug: declared.slug, entry: declared.entry, entryPath };\n });\n\n assertEsmProject(projectDir);\n\n return {\n project,\n projectDir,\n configPath,\n integrations,\n primary: integrations[0]!,\n };\n}\n\n/**\n * Fail unless the project is an ES module package.\n *\n * `init` sets `\"type\": \"module\"` and says so, but a warning can be scrolled\n * past, and a partner who misses it gets a confusing module-resolution error\n * from deep inside Node or Next rather than a sentence naming the problem.\n * This is the backstop: one precondition, in the loader every verb after\n * `init` goes through, so the failure is loud and says what to do.\n *\n * `init` deliberately does NOT come through here — it is the thing that fixes\n * this state.\n *\n * A project with no package.json at all is left alone: `dev` reports missing\n * dependencies with its own better message, and `validate`/`test` have their\n * own preconditions. We only assert on a manifest that exists and disagrees.\n */\nexport function assertEsmProject(projectDir: string): void {\n const manifestPath = path.join(projectDir, 'package.json');\n if (!fs.existsSync(manifestPath)) return;\n\n let manifest: unknown;\n try {\n manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));\n } catch {\n // Malformed JSON is npm's problem to report, not ours to guess at; the\n // partner will hit it on their next install with a better message.\n return;\n }\n\n const declared =\n typeof manifest === 'object' && manifest !== null\n ? (manifest as { type?: unknown }).type\n : undefined;\n\n if (declared === 'module') return;\n\n throw preconditionError(\n declared === undefined\n ? `${manifestPath} does not declare \"type\": \"module\", so Node treats ` +\n 'your integration as CommonJS and cannot load it.'\n : `${manifestPath} declares \"type\": ${JSON.stringify(declared)}, but the ` +\n 'integration sources are ES modules and Node cannot load them.',\n `Set \"type\": \"module\" in ${manifestPath}. If this project has CommonJS ` +\n 'sources of its own, put your integration in its own package instead — ' +\n 'the two module systems cannot share one package.json. Re-running ' +\n '\"ekanos init\" also fixes this.',\n );\n}\n\n/** Serialize an ekanos.json body with a trailing newline. */\nexport function serializeProject(project: EkanosProject): string {\n return `${JSON.stringify(project, null, 2)}\\n`;\n}\n"]}
|
package/dist/publish-api.d.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* → 400 { error, code: "INVALID_REQUEST" | "INVALID_ARCHIVE" }
|
|
21
21
|
* → 401 { error, code: "AUTH_REQUIRED" } token absent or expired —
|
|
22
22
|
* refresh once and retry
|
|
23
|
-
* → 403 { error, code: "FORBIDDEN" } no
|
|
23
|
+
* → 403 { error, code: "FORBIDDEN" } no developer seat on the source
|
|
24
24
|
* → 404 { error, code: "SOURCE_NOT_FOUND" }
|
|
25
25
|
* → 409 { error, code: "VERSION_EXISTS" } this (slug, version) is
|
|
26
26
|
* already submitted
|
package/dist/publish-api.js
CHANGED
|
@@ -22,7 +22,7 @@ import { authRequiredError, forbiddenError, invalidStateError, networkError, not
|
|
|
22
22
|
* → 400 { error, code: "INVALID_REQUEST" | "INVALID_ARCHIVE" }
|
|
23
23
|
* → 401 { error, code: "AUTH_REQUIRED" } token absent or expired —
|
|
24
24
|
* refresh once and retry
|
|
25
|
-
* → 403 { error, code: "FORBIDDEN" } no
|
|
25
|
+
* → 403 { error, code: "FORBIDDEN" } no developer seat on the source
|
|
26
26
|
* → 404 { error, code: "SOURCE_NOT_FOUND" }
|
|
27
27
|
* → 409 { error, code: "VERSION_EXISTS" } this (slug, version) is
|
|
28
28
|
* already submitted
|
|
@@ -77,7 +77,7 @@ export async function submitArchive(host, accessToken, payload) {
|
|
|
77
77
|
const serverMessage = str(body.error);
|
|
78
78
|
if (response.status === 403) {
|
|
79
79
|
throw forbiddenError(serverMessage !== null && serverMessage !== void 0 ? serverMessage : `${host} refused the submission: this account does not hold the ` +
|
|
80
|
-
`
|
|
80
|
+
`developer seat on source "${payload.source}".`, `Ask the source operator to grant your account the developer seat for ` +
|
|
81
81
|
`"${payload.source}", then retry.`);
|
|
82
82
|
}
|
|
83
83
|
if (response.status === 404) {
|
package/dist/publish-api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publish-api.js","sourceRoot":"","sources":["../src/publish-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { request } from './auth/fusion-api';\nimport {\n authRequiredError,\n forbiddenError,\n invalidStateError,\n networkError,\n notFoundError,\n validationError,\n} from './errors';\n\n/**\n * The CLI's client for the Fusion submission intake route.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the seam between this package and the Fusion web app.\n *\n * The server side lives in `apps/web/app/api/partner/submissions/route.ts`.\n * As with the auth routes (see `auth/fusion-api.ts`), the two halves are\n * coupled only by HTTP; tests stub `fetch`, never the route.\n *\n * POST /api/partner/submissions [Authorization: Bearer <access_token>]\n * ← multipart/form-data:\n * source — Fusion source slug\n * slug — integration slug (kebab-case)\n * version — semver\n * manifest — JSON string (the parsed ekanos.json + declaration summary)\n * archive — gzipped tarball, ≤ 4 MiB\n * → 201 { ok: true, data: { submissionId, slug, version, state,\n * archiveSha256 } }\n * → 400 { error, code: \"INVALID_REQUEST\" | \"INVALID_ARCHIVE\" }\n * → 401 { error, code: \"AUTH_REQUIRED\" } token absent or expired —\n * refresh once and retry\n * → 403 { error, code: \"FORBIDDEN\" } no dev seat on the source\n * → 404 { error, code: \"SOURCE_NOT_FOUND\" }\n * → 409 { error, code: \"VERSION_EXISTS\" } this (slug, version) is\n * already submitted\n * → 413 { error, code: \"ARCHIVE_TOO_LARGE\" }\n * → 3xx treated as 401 (an\n * auth-gated Fusion page\n * answers 307)\n *\n * GET /api/partner/submissions?source=<slug|id> [Authorization: Bearer]\n * → 200 { ok: true, data: { source, integrations: [{ id, slug, name,\n * createdAt, versions: [{ id, version, state, archiveSha256,\n * submittedAt }] }] } }\n * → 400 { error, code: \"INVALID_REQUEST\" }\n * → 401 { error, code: \"AUTH_REQUIRED\" } refresh once and retry\n * → 403 { error, code: \"MFA_REQUIRED\" }\n * → 404 { error, code: \"SOURCE_NOT_FOUND\" }\n * → 3xx treated as 401\n * ---------------------------------------------------------------------------\n */\n\n/**\n * The server's manifest cap — 64 KiB of UTF-8, enforced in\n * `apps/web/app/api/partner/submissions/_lib/submission-validation.ts` against\n * `Buffer.byteLength`. Mirrored here so `publish` can refuse an oversized\n * manifest with a clear message instead of the server's generic 400.\n */\nexport const MAX_MANIFEST_BYTES = 64 * 1024;\n\nexport interface SubmissionPayload {\n source: string;\n slug: string;\n version: string;\n /** JSON string — already serialized by the caller. */\n manifest: string;\n archive: Uint8Array;\n}\n\nexport interface SubmissionReceipt {\n submissionId: string;\n slug: string;\n version: string;\n state: string;\n archiveSha256: string;\n}\n\nexport type SubmitResult =\n | { status: 'ok'; receipt: SubmissionReceipt }\n /** The bearer token did not authenticate — refresh and retry, once. */\n | { status: 'unauthorized' };\n\nexport async function submitArchive(\n host: string,\n accessToken: string,\n payload: SubmissionPayload,\n): Promise<SubmitResult> {\n const form = new FormData();\n form.append('source', payload.source);\n form.append('slug', payload.slug);\n form.append('version', payload.version);\n form.append('manifest', payload.manifest);\n form.append(\n 'archive',\n // Copy into a fresh Uint8Array: Buffer is Uint8Array<ArrayBufferLike>,\n // which BlobPart refuses (a SharedArrayBuffer view is not a BlobPart).\n new Blob([new Uint8Array(payload.archive)], { type: 'application/gzip' }),\n 'archive.tgz',\n );\n\n const response = await request(`${host}/api/partner/submissions`, {\n method: 'POST',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n body: form,\n });\n\n // 401, or the 307 an auth-gated Fusion route answers with: the token did\n // not authenticate. The caller owns the refresh-and-retry decision.\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const serverMessage = str(body.error);\n\n if (response.status === 403) {\n throw forbiddenError(\n serverMessage ??\n `${host} refused the submission: this account does not hold the ` +\n `dev seat on source \"${payload.source}\".`,\n `Ask the source operator to grant your account the developer seat for ` +\n `\"${payload.source}\", then retry.`,\n );\n }\n\n if (response.status === 404) {\n throw notFoundError(\n serverMessage ?? `${host} has no source with slug \"${payload.source}\".`,\n `Check the --source value (or the \"source\" field in ekanos.json) ` +\n `against the slug your Fusion operator gave you.`,\n );\n }\n\n if (response.status === 409) {\n throw invalidStateError(\n serverMessage ??\n `Version ${payload.version} of \"${payload.slug}\" is already submitted.`,\n `Submitted versions are immutable. Bump the \"version\" field in ` +\n `package.json and publish again.`,\n );\n }\n\n if (response.status === 400 || response.status === 413) {\n throw validationError(\n serverMessage ?? `${host} rejected the submission as invalid.`,\n `Fix the reported problem and re-run \"ekanos publish\". If the message ` +\n `names the archive, re-check the project layout (\"ekanos validate\").`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}/api/partner/submissions responded ${response.status}.`,\n response.status >= 500\n ? `The Fusion deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is a Fusion deployment running a build that ` +\n `includes the partner submissions route.`,\n );\n }\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const receipt = {\n submissionId: str(data.submissionId),\n slug: str(data.slug),\n version: str(data.version),\n state: str(data.state),\n archiveSha256: str(data.archiveSha256),\n };\n\n if (Object.values(receipt).some((value) => value === null)) {\n throw networkError(\n `${host} accepted the submission but returned an incomplete receipt.`,\n `Confirm the deployment is running a Fusion build that includes the ` +\n `partner submissions route, then check the submission's state with ` +\n `your operator before re-publishing.`,\n );\n }\n\n return { status: 'ok', receipt: receipt as SubmissionReceipt };\n}\n\nexport interface SubmissionVersion {\n id: string;\n version: string;\n state: string;\n archiveSha256: string | null;\n submittedAt: string | null;\n}\n\nexport interface SubmissionListing {\n id: string;\n slug: string;\n name: string | null;\n createdAt: string | null;\n versions: SubmissionVersion[];\n}\n\nexport type ListResult =\n | { status: 'ok'; integrations: SubmissionListing[] }\n /** The bearer token did not authenticate — refresh and retry, once. */\n | { status: 'unauthorized' };\n\n/**\n * List the caller's visible submissions for a source. Same authorization model\n * as `submitArchive`: the seat SELECT policies filter server-side, so a caller\n * with no seat sees an empty list rather than an error.\n */\nexport async function listSubmissions(\n host: string,\n accessToken: string,\n source: string,\n): Promise<ListResult> {\n const response = await request(\n `${host}/api/partner/submissions?source=${encodeURIComponent(source)}`,\n {\n method: 'GET',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n },\n );\n\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const serverMessage = str(body.error);\n\n if (response.status === 403) {\n throw forbiddenError(\n serverMessage ?? `${host} refused to list submissions for \"${source}\".`,\n `Complete MFA in the browser if the message names it, then run ` +\n `\"ekanos login --host ${host}\" again.`,\n );\n }\n\n if (response.status === 404) {\n throw notFoundError(\n serverMessage ?? `${host} has no source with slug \"${source}\".`,\n `Check the \"source\" field in ekanos.json (or --source) against the ` +\n `slug your Fusion operator gave you.`,\n );\n }\n\n if (response.status === 400) {\n throw validationError(\n serverMessage ?? `${host} rejected the submissions query.`,\n `Fix the reported problem and re-run \"ekanos status\".`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}/api/partner/submissions responded ${response.status}.`,\n response.status >= 500\n ? `The Fusion deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is a Fusion deployment running a build that ` +\n `includes the partner submissions route.`,\n );\n }\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const rows = Array.isArray(data.integrations) ? data.integrations : [];\n\n const integrations = rows\n .filter(\n (row): row is Record<string, unknown> =>\n typeof row === 'object' && row !== null,\n )\n .map((row) => ({\n id: str(row.id) ?? '',\n slug: str(row.slug) ?? '',\n name: str(row.name),\n createdAt: str(row.createdAt),\n versions: (Array.isArray(row.versions) ? row.versions : [])\n .filter(\n (version): version is Record<string, unknown> =>\n typeof version === 'object' && version !== null,\n )\n .map((version) => ({\n id: str(version.id) ?? '',\n version: str(version.version) ?? '',\n state: str(version.state) ?? '',\n archiveSha256: str(version.archiveSha256),\n submittedAt: str(version.submittedAt),\n })),\n }));\n\n return { status: 'ok', integrations };\n}\n\n/** Guard for the \"still unauthorized after one refresh\" terminal case. */\nexport function stillUnauthorized(host: string): never {\n throw authRequiredError(\n `The session for ${host} could not authorize the submission.`,\n `Run \"ekanos login --host ${host}\" and retry.`,\n );\n}\n\nasync function readOptionalJson(\n response: Response,\n): Promise<Record<string, unknown>> {\n try {\n const parsed: unknown = await response.json();\n\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n\nfunction isRedirect(status: number): boolean {\n return status >= 300 && status < 400;\n}\n\nfunction str(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"publish-api.js","sourceRoot":"","sources":["../src/publish-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { request } from './auth/fusion-api';\nimport {\n authRequiredError,\n forbiddenError,\n invalidStateError,\n networkError,\n notFoundError,\n validationError,\n} from './errors';\n\n/**\n * The CLI's client for the Fusion submission intake route.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the seam between this package and the Fusion web app.\n *\n * The server side lives in `apps/web/app/api/partner/submissions/route.ts`.\n * As with the auth routes (see `auth/fusion-api.ts`), the two halves are\n * coupled only by HTTP; tests stub `fetch`, never the route.\n *\n * POST /api/partner/submissions [Authorization: Bearer <access_token>]\n * ← multipart/form-data:\n * source — Fusion source slug\n * slug — integration slug (kebab-case)\n * version — semver\n * manifest — JSON string (the parsed ekanos.json + declaration summary)\n * archive — gzipped tarball, ≤ 4 MiB\n * → 201 { ok: true, data: { submissionId, slug, version, state,\n * archiveSha256 } }\n * → 400 { error, code: \"INVALID_REQUEST\" | \"INVALID_ARCHIVE\" }\n * → 401 { error, code: \"AUTH_REQUIRED\" } token absent or expired —\n * refresh once and retry\n * → 403 { error, code: \"FORBIDDEN\" } no developer seat on the source\n * → 404 { error, code: \"SOURCE_NOT_FOUND\" }\n * → 409 { error, code: \"VERSION_EXISTS\" } this (slug, version) is\n * already submitted\n * → 413 { error, code: \"ARCHIVE_TOO_LARGE\" }\n * → 3xx treated as 401 (an\n * auth-gated Fusion page\n * answers 307)\n *\n * GET /api/partner/submissions?source=<slug|id> [Authorization: Bearer]\n * → 200 { ok: true, data: { source, integrations: [{ id, slug, name,\n * createdAt, versions: [{ id, version, state, archiveSha256,\n * submittedAt }] }] } }\n * → 400 { error, code: \"INVALID_REQUEST\" }\n * → 401 { error, code: \"AUTH_REQUIRED\" } refresh once and retry\n * → 403 { error, code: \"MFA_REQUIRED\" }\n * → 404 { error, code: \"SOURCE_NOT_FOUND\" }\n * → 3xx treated as 401\n * ---------------------------------------------------------------------------\n */\n\n/**\n * The server's manifest cap — 64 KiB of UTF-8, enforced in\n * `apps/web/app/api/partner/submissions/_lib/submission-validation.ts` against\n * `Buffer.byteLength`. Mirrored here so `publish` can refuse an oversized\n * manifest with a clear message instead of the server's generic 400.\n */\nexport const MAX_MANIFEST_BYTES = 64 * 1024;\n\nexport interface SubmissionPayload {\n source: string;\n slug: string;\n version: string;\n /** JSON string — already serialized by the caller. */\n manifest: string;\n archive: Uint8Array;\n}\n\nexport interface SubmissionReceipt {\n submissionId: string;\n slug: string;\n version: string;\n state: string;\n archiveSha256: string;\n}\n\nexport type SubmitResult =\n | { status: 'ok'; receipt: SubmissionReceipt }\n /** The bearer token did not authenticate — refresh and retry, once. */\n | { status: 'unauthorized' };\n\nexport async function submitArchive(\n host: string,\n accessToken: string,\n payload: SubmissionPayload,\n): Promise<SubmitResult> {\n const form = new FormData();\n form.append('source', payload.source);\n form.append('slug', payload.slug);\n form.append('version', payload.version);\n form.append('manifest', payload.manifest);\n form.append(\n 'archive',\n // Copy into a fresh Uint8Array: Buffer is Uint8Array<ArrayBufferLike>,\n // which BlobPart refuses (a SharedArrayBuffer view is not a BlobPart).\n new Blob([new Uint8Array(payload.archive)], { type: 'application/gzip' }),\n 'archive.tgz',\n );\n\n const response = await request(`${host}/api/partner/submissions`, {\n method: 'POST',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n body: form,\n });\n\n // 401, or the 307 an auth-gated Fusion route answers with: the token did\n // not authenticate. The caller owns the refresh-and-retry decision.\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const serverMessage = str(body.error);\n\n if (response.status === 403) {\n throw forbiddenError(\n serverMessage ??\n `${host} refused the submission: this account does not hold the ` +\n `developer seat on source \"${payload.source}\".`,\n `Ask the source operator to grant your account the developer seat for ` +\n `\"${payload.source}\", then retry.`,\n );\n }\n\n if (response.status === 404) {\n throw notFoundError(\n serverMessage ?? `${host} has no source with slug \"${payload.source}\".`,\n `Check the --source value (or the \"source\" field in ekanos.json) ` +\n `against the slug your Fusion operator gave you.`,\n );\n }\n\n if (response.status === 409) {\n throw invalidStateError(\n serverMessage ??\n `Version ${payload.version} of \"${payload.slug}\" is already submitted.`,\n `Submitted versions are immutable. Bump the \"version\" field in ` +\n `package.json and publish again.`,\n );\n }\n\n if (response.status === 400 || response.status === 413) {\n throw validationError(\n serverMessage ?? `${host} rejected the submission as invalid.`,\n `Fix the reported problem and re-run \"ekanos publish\". If the message ` +\n `names the archive, re-check the project layout (\"ekanos validate\").`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}/api/partner/submissions responded ${response.status}.`,\n response.status >= 500\n ? `The Fusion deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is a Fusion deployment running a build that ` +\n `includes the partner submissions route.`,\n );\n }\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const receipt = {\n submissionId: str(data.submissionId),\n slug: str(data.slug),\n version: str(data.version),\n state: str(data.state),\n archiveSha256: str(data.archiveSha256),\n };\n\n if (Object.values(receipt).some((value) => value === null)) {\n throw networkError(\n `${host} accepted the submission but returned an incomplete receipt.`,\n `Confirm the deployment is running a Fusion build that includes the ` +\n `partner submissions route, then check the submission's state with ` +\n `your operator before re-publishing.`,\n );\n }\n\n return { status: 'ok', receipt: receipt as SubmissionReceipt };\n}\n\nexport interface SubmissionVersion {\n id: string;\n version: string;\n state: string;\n archiveSha256: string | null;\n submittedAt: string | null;\n}\n\nexport interface SubmissionListing {\n id: string;\n slug: string;\n name: string | null;\n createdAt: string | null;\n versions: SubmissionVersion[];\n}\n\nexport type ListResult =\n | { status: 'ok'; integrations: SubmissionListing[] }\n /** The bearer token did not authenticate — refresh and retry, once. */\n | { status: 'unauthorized' };\n\n/**\n * List the caller's visible submissions for a source. Same authorization model\n * as `submitArchive`: the seat SELECT policies filter server-side, so a caller\n * with no seat sees an empty list rather than an error.\n */\nexport async function listSubmissions(\n host: string,\n accessToken: string,\n source: string,\n): Promise<ListResult> {\n const response = await request(\n `${host}/api/partner/submissions?source=${encodeURIComponent(source)}`,\n {\n method: 'GET',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n },\n );\n\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const serverMessage = str(body.error);\n\n if (response.status === 403) {\n throw forbiddenError(\n serverMessage ?? `${host} refused to list submissions for \"${source}\".`,\n `Complete MFA in the browser if the message names it, then run ` +\n `\"ekanos login --host ${host}\" again.`,\n );\n }\n\n if (response.status === 404) {\n throw notFoundError(\n serverMessage ?? `${host} has no source with slug \"${source}\".`,\n `Check the \"source\" field in ekanos.json (or --source) against the ` +\n `slug your Fusion operator gave you.`,\n );\n }\n\n if (response.status === 400) {\n throw validationError(\n serverMessage ?? `${host} rejected the submissions query.`,\n `Fix the reported problem and re-run \"ekanos status\".`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}/api/partner/submissions responded ${response.status}.`,\n response.status >= 500\n ? `The Fusion deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is a Fusion deployment running a build that ` +\n `includes the partner submissions route.`,\n );\n }\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const rows = Array.isArray(data.integrations) ? data.integrations : [];\n\n const integrations = rows\n .filter(\n (row): row is Record<string, unknown> =>\n typeof row === 'object' && row !== null,\n )\n .map((row) => ({\n id: str(row.id) ?? '',\n slug: str(row.slug) ?? '',\n name: str(row.name),\n createdAt: str(row.createdAt),\n versions: (Array.isArray(row.versions) ? row.versions : [])\n .filter(\n (version): version is Record<string, unknown> =>\n typeof version === 'object' && version !== null,\n )\n .map((version) => ({\n id: str(version.id) ?? '',\n version: str(version.version) ?? '',\n state: str(version.state) ?? '',\n archiveSha256: str(version.archiveSha256),\n submittedAt: str(version.submittedAt),\n })),\n }));\n\n return { status: 'ok', integrations };\n}\n\n/** Guard for the \"still unauthorized after one refresh\" terminal case. */\nexport function stillUnauthorized(host: string): never {\n throw authRequiredError(\n `The session for ${host} could not authorize the submission.`,\n `Run \"ekanos login --host ${host}\" and retry.`,\n );\n}\n\nasync function readOptionalJson(\n response: Response,\n): Promise<Record<string, unknown>> {\n try {\n const parsed: unknown = await response.json();\n\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n\nfunction isRedirect(status: number): boolean {\n return status >= 300 && status < 400;\n}\n\nfunction str(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n"]}
|
package/dist/safe-fs.d.ts
CHANGED
|
@@ -50,6 +50,6 @@ export declare function readFileNoFollow(root: string, filePath: string): string
|
|
|
50
50
|
* way and at the leaf. `O_NOFOLLOW` on the final open is what makes this safe
|
|
51
51
|
* against a link swapped in after our checks.
|
|
52
52
|
*/
|
|
53
|
-
export declare function writeFileNoFollow(root: string, filePath: string, contents: string): void;
|
|
53
|
+
export declare function writeFileNoFollow(root: string, filePath: string, contents: string | Uint8Array): void;
|
|
54
54
|
/** Append to an existing file under the same no-follow guarantee. */
|
|
55
55
|
export declare function appendFileNoFollow(root: string, filePath: string, contents: string): void;
|
package/dist/safe-fs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safe-fs.js","sourceRoot":"","sources":["../src/safe-fs.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { invalidStateError } from './errors';\n\n/**\n * Filesystem primitives for writing into a directory we do not fully trust.\n *\n * `ekanos dev` writes a generated tree into a partner's project, and every\n * path it touches is one an attacker (or a careless `ln -s`) may have created\n * first. Plain `existsSync` + `writeFileSync` follows symlinks, so a\n * `.ekanos/harness` pointing at `~/.ssh`, a generated leaf pointing at a\n * tracked source file, or a DANGLING link (which `existsSync` reports as\n * absent, and which `writeFileSync` then materializes) all let a scaffold\n * write outside the project.\n *\n * Two invariants close that, and both are enforced here rather than at the\n * call sites, because a single unguarded write anywhere reopens the hole:\n *\n * 1. **Containment** — every path resolves inside the project root.\n * 2. **No symlinks** — no component of a path we write through may be a\n * symlink, and the write itself uses `O_NOFOLLOW` so the kernel enforces\n * it at open time rather than trusting our earlier `lstat`. That closes\n * the check→write TOCTOU window: a link swapped in after the check makes\n * `open` fail with ELOOP instead of following.\n */\n\n/** True when `target` is inside `root` (or is `root` itself). */\nexport function isContained(root: string, target: string): boolean {\n const relative = path.relative(root, target);\n if (relative === '') return true;\n return !relative.startsWith('..') && !path.isAbsolute(relative);\n}\n\nfunction containmentError(root: string, target: string): never {\n throw invalidStateError(\n `Refusing to write ${target}: it resolves outside the project root ${root}.`,\n 'Remove the symlink or path component that redirects it, then re-run ' +\n '\"ekanos dev\".',\n );\n}\n\nfunction symlinkError(link: string): never {\n throw invalidStateError(\n `Refusing to write through the symlink ${link}.`,\n `\"ekanos dev\" only writes real files inside your project. Delete ${link} ` +\n '(or the tree containing it) and re-run.',\n );\n}\n\n/**\n * Walk every path component from `root` down to `target` and refuse if any\n * existing one is a symlink. `lstat` is deliberate — `stat` would resolve the\n * link and report the target's type, which is exactly what we must not do.\n */\nexport function assertNoSymlinkedComponent(root: string, target: string): void {\n if (!isContained(root, target)) containmentError(root, target);\n\n const relative = path.relative(root, target);\n if (relative === '') return;\n\n let current = root;\n for (const segment of relative.split(path.sep)) {\n current = path.join(current, segment);\n let stats: fs.Stats;\n try {\n stats = fs.lstatSync(current);\n } catch {\n // Does not exist yet — nothing to follow, and everything below it is\n // ours to create.\n return;\n }\n if (stats.isSymbolicLink()) symlinkError(current);\n }\n}\n\n/**\n * `mkdir -p`, one guarded component at a time. `fs.mkdirSync(recursive)` is\n * happy to walk through a symlinked ancestor; this is not.\n */\nexport function mkdirSafe(root: string, dir: string): void {\n if (!isContained(root, dir)) containmentError(root, dir);\n\n const relative = path.relative(root, dir);\n if (relative === '') return;\n\n let current = root;\n for (const segment of relative.split(path.sep)) {\n current = path.join(current, segment);\n let stats: fs.Stats | null = null;\n try {\n stats = fs.lstatSync(current);\n } catch {\n stats = null;\n }\n\n if (stats === null) {\n try {\n fs.mkdirSync(current);\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n // Lost a race to another process; re-check what landed there.\n if (code !== 'EEXIST') throw error;\n stats = fs.lstatSync(current);\n }\n if (stats === null) continue;\n }\n\n if (stats.isSymbolicLink()) symlinkError(current);\n if (!stats.isDirectory()) {\n throw invalidStateError(\n `${current} exists and is not a directory, so the harness shell ` +\n 'cannot be written there.',\n `Move or delete ${current}, then re-run \"ekanos dev\".`,\n );\n }\n }\n}\n\n/**\n * What `destPath` currently is, without following anything. `'symlink'` is\n * reported for a dangling link too — the case `existsSync` calls absent and\n * `writeFileSync` would silently materialize outside the project.\n */\nexport type PathKind = 'missing' | 'file' | 'directory' | 'symlink' | 'other';\n\nexport function classifyPath(destPath: string): PathKind {\n let stats: fs.Stats;\n try {\n stats = fs.lstatSync(destPath);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return 'missing';\n throw error;\n }\n if (stats.isSymbolicLink()) return 'symlink';\n if (stats.isDirectory()) return 'directory';\n if (stats.isFile()) return 'file';\n return 'other';\n}\n\n/**\n * Read a file, refusing to follow a symlink at the final component. Returns\n * null when it does not exist. `O_NOFOLLOW` makes the kernel enforce this, so\n * there is no window between deciding and reading.\n */\nexport function readFileNoFollow(\n root: string,\n filePath: string,\n): string | null {\n assertNoSymlinkedComponent(root, filePath);\n\n let fd: number;\n try {\n fd = fs.openSync(filePath, fs.constants.O_RDONLY | O_NOFOLLOW);\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ENOENT') return null;\n if (code === 'ELOOP') symlinkError(filePath);\n throw error;\n }\n\n try {\n return fs.readFileSync(fd, 'utf8');\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/**\n * Write a file, creating parent directories, refusing every symlink on the\n * way and at the leaf. `O_NOFOLLOW` on the final open is what makes this safe\n * against a link swapped in after our checks.\n */\nexport function writeFileNoFollow(\n root: string,\n filePath: string,\n contents: string,\n): void {\n assertNoSymlinkedComponent(root, filePath);\n mkdirSafe(root, path.dirname(filePath));\n\n let fd: number;\n try {\n fd = fs.openSync(\n filePath,\n fs.constants.O_WRONLY |\n fs.constants.O_CREAT |\n fs.constants.O_TRUNC |\n O_NOFOLLOW,\n 0o644,\n );\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ELOOP') symlinkError(filePath);\n throw error;\n }\n\n try {\n fs.writeFileSync(fd, contents);\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/** Append to an existing file under the same no-follow guarantee. */\nexport function appendFileNoFollow(\n root: string,\n filePath: string,\n contents: string,\n): void {\n assertNoSymlinkedComponent(root, filePath);\n\n let fd: number;\n try {\n fd = fs.openSync(\n filePath,\n fs.constants.O_WRONLY | fs.constants.O_APPEND | O_NOFOLLOW,\n );\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ELOOP') symlinkError(filePath);\n throw error;\n }\n\n try {\n fs.writeFileSync(fd, contents);\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/**\n * `O_NOFOLLOW` is POSIX and present on Linux and macOS. Node exposes it as 0\n * where the platform has no equivalent (Windows), in which case the explicit\n * `lstat` walk above is the whole guarantee rather than a belt to the kernel's\n * braces.\n */\nconst O_NOFOLLOW = fs.constants.O_NOFOLLOW ?? 0;\n"]}
|
|
1
|
+
{"version":3,"file":"safe-fs.js","sourceRoot":"","sources":["../src/safe-fs.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { invalidStateError } from './errors';\n\n/**\n * Filesystem primitives for writing into a directory we do not fully trust.\n *\n * `ekanos dev` writes a generated tree into a partner's project, and every\n * path it touches is one an attacker (or a careless `ln -s`) may have created\n * first. Plain `existsSync` + `writeFileSync` follows symlinks, so a\n * `.ekanos/harness` pointing at `~/.ssh`, a generated leaf pointing at a\n * tracked source file, or a DANGLING link (which `existsSync` reports as\n * absent, and which `writeFileSync` then materializes) all let a scaffold\n * write outside the project.\n *\n * Two invariants close that, and both are enforced here rather than at the\n * call sites, because a single unguarded write anywhere reopens the hole:\n *\n * 1. **Containment** — every path resolves inside the project root.\n * 2. **No symlinks** — no component of a path we write through may be a\n * symlink, and the write itself uses `O_NOFOLLOW` so the kernel enforces\n * it at open time rather than trusting our earlier `lstat`. That closes\n * the check→write TOCTOU window: a link swapped in after the check makes\n * `open` fail with ELOOP instead of following.\n */\n\n/** True when `target` is inside `root` (or is `root` itself). */\nexport function isContained(root: string, target: string): boolean {\n const relative = path.relative(root, target);\n if (relative === '') return true;\n return !relative.startsWith('..') && !path.isAbsolute(relative);\n}\n\nfunction containmentError(root: string, target: string): never {\n throw invalidStateError(\n `Refusing to write ${target}: it resolves outside the project root ${root}.`,\n 'Remove the symlink or path component that redirects it, then re-run ' +\n '\"ekanos dev\".',\n );\n}\n\nfunction symlinkError(link: string): never {\n throw invalidStateError(\n `Refusing to write through the symlink ${link}.`,\n `\"ekanos dev\" only writes real files inside your project. Delete ${link} ` +\n '(or the tree containing it) and re-run.',\n );\n}\n\n/**\n * Walk every path component from `root` down to `target` and refuse if any\n * existing one is a symlink. `lstat` is deliberate — `stat` would resolve the\n * link and report the target's type, which is exactly what we must not do.\n */\nexport function assertNoSymlinkedComponent(root: string, target: string): void {\n if (!isContained(root, target)) containmentError(root, target);\n\n const relative = path.relative(root, target);\n if (relative === '') return;\n\n let current = root;\n for (const segment of relative.split(path.sep)) {\n current = path.join(current, segment);\n let stats: fs.Stats;\n try {\n stats = fs.lstatSync(current);\n } catch {\n // Does not exist yet — nothing to follow, and everything below it is\n // ours to create.\n return;\n }\n if (stats.isSymbolicLink()) symlinkError(current);\n }\n}\n\n/**\n * `mkdir -p`, one guarded component at a time. `fs.mkdirSync(recursive)` is\n * happy to walk through a symlinked ancestor; this is not.\n */\nexport function mkdirSafe(root: string, dir: string): void {\n if (!isContained(root, dir)) containmentError(root, dir);\n\n const relative = path.relative(root, dir);\n if (relative === '') return;\n\n let current = root;\n for (const segment of relative.split(path.sep)) {\n current = path.join(current, segment);\n let stats: fs.Stats | null = null;\n try {\n stats = fs.lstatSync(current);\n } catch {\n stats = null;\n }\n\n if (stats === null) {\n try {\n fs.mkdirSync(current);\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n // Lost a race to another process; re-check what landed there.\n if (code !== 'EEXIST') throw error;\n stats = fs.lstatSync(current);\n }\n if (stats === null) continue;\n }\n\n if (stats.isSymbolicLink()) symlinkError(current);\n if (!stats.isDirectory()) {\n throw invalidStateError(\n `${current} exists and is not a directory, so the harness shell ` +\n 'cannot be written there.',\n `Move or delete ${current}, then re-run \"ekanos dev\".`,\n );\n }\n }\n}\n\n/**\n * What `destPath` currently is, without following anything. `'symlink'` is\n * reported for a dangling link too — the case `existsSync` calls absent and\n * `writeFileSync` would silently materialize outside the project.\n */\nexport type PathKind = 'missing' | 'file' | 'directory' | 'symlink' | 'other';\n\nexport function classifyPath(destPath: string): PathKind {\n let stats: fs.Stats;\n try {\n stats = fs.lstatSync(destPath);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return 'missing';\n throw error;\n }\n if (stats.isSymbolicLink()) return 'symlink';\n if (stats.isDirectory()) return 'directory';\n if (stats.isFile()) return 'file';\n return 'other';\n}\n\n/**\n * Read a file, refusing to follow a symlink at the final component. Returns\n * null when it does not exist. `O_NOFOLLOW` makes the kernel enforce this, so\n * there is no window between deciding and reading.\n */\nexport function readFileNoFollow(\n root: string,\n filePath: string,\n): string | null {\n assertNoSymlinkedComponent(root, filePath);\n\n let fd: number;\n try {\n fd = fs.openSync(filePath, fs.constants.O_RDONLY | O_NOFOLLOW);\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ENOENT') return null;\n if (code === 'ELOOP') symlinkError(filePath);\n throw error;\n }\n\n try {\n return fs.readFileSync(fd, 'utf8');\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/**\n * Write a file, creating parent directories, refusing every symlink on the\n * way and at the leaf. `O_NOFOLLOW` on the final open is what makes this safe\n * against a link swapped in after our checks.\n */\nexport function writeFileNoFollow(\n root: string,\n filePath: string,\n contents: string | Uint8Array,\n): void {\n assertNoSymlinkedComponent(root, filePath);\n mkdirSafe(root, path.dirname(filePath));\n\n let fd: number;\n try {\n fd = fs.openSync(\n filePath,\n fs.constants.O_WRONLY |\n fs.constants.O_CREAT |\n fs.constants.O_TRUNC |\n O_NOFOLLOW,\n 0o644,\n );\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ELOOP') symlinkError(filePath);\n throw error;\n }\n\n try {\n fs.writeFileSync(fd, contents);\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/** Append to an existing file under the same no-follow guarantee. */\nexport function appendFileNoFollow(\n root: string,\n filePath: string,\n contents: string,\n): void {\n assertNoSymlinkedComponent(root, filePath);\n\n let fd: number;\n try {\n fd = fs.openSync(\n filePath,\n fs.constants.O_WRONLY | fs.constants.O_APPEND | O_NOFOLLOW,\n );\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ELOOP') symlinkError(filePath);\n throw error;\n }\n\n try {\n fs.writeFileSync(fd, contents);\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/**\n * `O_NOFOLLOW` is POSIX and present on Linux and macOS. Node exposes it as 0\n * where the platform has no equivalent (Windows), in which case the explicit\n * `lstat` walk above is the whole guarantee rather than a belt to the kernel's\n * braces.\n */\nconst O_NOFOLLOW = fs.constants.O_NOFOLLOW ?? 0;\n"]}
|
package/dist/sources-api.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* GET /api/partner/sources [Authorization: Bearer <access_token>]
|
|
12
12
|
* → 200 { ok: true, data: { sources: [{ id, slug, name, roles }] } }
|
|
13
|
-
* roles is a non-empty array of "
|
|
13
|
+
* roles is a non-empty array of "developer" | "source_admin", sorted; sources
|
|
14
14
|
* is sorted by slug. Empty array means no seats anywhere.
|
|
15
15
|
* → 401 { error, code: "AUTH_REQUIRED" } token absent or expired —
|
|
16
16
|
* refresh once and retry
|
package/dist/sources-api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sources-api.js","sourceRoot":"","sources":["../src/sources-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { request } from './auth/fusion-api';\nimport { authRequiredError, networkError } from './errors';\n\n/**\n * The CLI's client for the Fusion source-seat visibility route.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the seam between this package and the Fusion web app.\n *\n * The server side lives in `apps/web/app/api/partner/sources/route.ts`. As\n * with the auth routes and `publish-api.ts`, the two halves are coupled only\n * by HTTP; tests stub `fetch`, never the route.\n *\n * GET /api/partner/sources [Authorization: Bearer <access_token>]\n * → 200 { ok: true, data: { sources: [{ id, slug, name, roles }] } }\n * roles is a non-empty array of \"
|
|
1
|
+
{"version":3,"file":"sources-api.js","sourceRoot":"","sources":["../src/sources-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { request } from './auth/fusion-api';\nimport { authRequiredError, networkError } from './errors';\n\n/**\n * The CLI's client for the Fusion source-seat visibility route.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the seam between this package and the Fusion web app.\n *\n * The server side lives in `apps/web/app/api/partner/sources/route.ts`. As\n * with the auth routes and `publish-api.ts`, the two halves are coupled only\n * by HTTP; tests stub `fetch`, never the route.\n *\n * GET /api/partner/sources [Authorization: Bearer <access_token>]\n * → 200 { ok: true, data: { sources: [{ id, slug, name, roles }] } }\n * roles is a non-empty array of \"developer\" | \"source_admin\", sorted; sources\n * is sorted by slug. Empty array means no seats anywhere.\n * → 401 { error, code: \"AUTH_REQUIRED\" } token absent or expired —\n * refresh once and retry\n * → 403 { error, code: \"MFA_REQUIRED\" }\n * → 3xx treated as 401 (an\n * auth-gated Fusion page\n * answers 307)\n * ---------------------------------------------------------------------------\n */\n\nexport interface SourceSeat {\n id: string;\n slug: string;\n name: string | null;\n roles: string[];\n}\n\nexport type FetchSourceSeatsResult =\n | { status: 'ok'; sources: SourceSeat[] }\n /** The bearer token did not authenticate — refresh and retry, once. */\n | { status: 'unauthorized' };\n\n/**\n * List the sources the caller holds a seat on (any role). Authorization is\n * entirely server-side (RLS on `source_memberships`): there is no client-side\n * filter to get wrong here.\n */\nexport async function fetchSourceSeats(\n host: string,\n accessToken: string,\n): Promise<FetchSourceSeatsResult> {\n const response = await request(`${host}/api/partner/sources`, {\n method: 'GET',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n });\n\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n\n if (response.status === 403) {\n throw authRequiredError(\n str(body.error) ??\n `${host} requires multi-factor authentication for this account.`,\n `Complete MFA in the browser, then run \"ekanos login\" again.`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}/api/partner/sources responded ${response.status}.`,\n response.status >= 500\n ? `The Fusion deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is a Fusion deployment running a build that ` +\n `includes the partner sources route.`,\n );\n }\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const rows = Array.isArray(data.sources) ? data.sources : [];\n\n const sources = rows\n .filter(\n (row): row is Record<string, unknown> =>\n typeof row === 'object' && row !== null,\n )\n .map((row) => ({\n id: str(row.id) ?? '',\n slug: str(row.slug) ?? '',\n name: str(row.name),\n roles: Array.isArray(row.roles)\n ? row.roles.filter((role): role is string => typeof role === 'string')\n : [],\n }));\n\n return { status: 'ok', sources };\n}\n\n/** Guard for the \"still unauthorized after one refresh\" terminal case. */\nexport function stillUnauthorizedSourceSeats(host: string): never {\n throw authRequiredError(\n `The session for ${host} could not authorize the source seat lookup.`,\n `Run \"ekanos login --host ${host}\" and retry.`,\n );\n}\n\nasync function readOptionalJson(\n response: Response,\n): Promise<Record<string, unknown>> {\n try {\n const parsed: unknown = await response.json();\n\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n\nfunction isRedirect(status: number): boolean {\n return status >= 300 && status < 400;\n}\n\nfunction str(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { collectCollisionFindings, collectDefinitionFindings, } from '@ekanos/integration-schema';
|
|
2
2
|
import { preconditionError } from './errors.js';
|
|
3
3
|
import { loadDefinition } from './load-definition.js';
|
|
4
|
-
import { collectProjectFindings } from './project-checks.js';
|
|
4
|
+
import { collectDocsFindings, collectProjectFindings } from './project-checks.js';
|
|
5
5
|
/**
|
|
6
6
|
* The full findings pass shared by `validate` (which reports them) and
|
|
7
7
|
* `publish` (which refuses to submit on any error-severity finding). One
|
|
@@ -55,6 +55,7 @@ export async function collectAllFindings(loaded) {
|
|
|
55
55
|
// Cross-checks only mean something with more than one definition in hand.
|
|
56
56
|
findings.push(...collectCollisionFindings(definitions));
|
|
57
57
|
findings.push(...collectProjectFindings(loaded.projectDir));
|
|
58
|
+
findings.push(...collectDocsFindings(loaded.project, loaded.projectDir));
|
|
58
59
|
return findings;
|
|
59
60
|
}
|
|
60
61
|
/**
|