@openephemeris/mcp-server 3.0.1 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +32 -22
  2. package/config/dev-allowlist.json +1319 -1165
  3. package/dist/backend/client.d.ts +12 -0
  4. package/dist/backend/client.js +99 -35
  5. package/dist/index.js +5 -0
  6. package/dist/schema-packs/llm.d.ts +1 -1
  7. package/dist/schema-packs/llm.js +1 -1
  8. package/dist/scripts/dev-allowlist.d.ts +1 -0
  9. package/dist/scripts/dev-allowlist.js +287 -0
  10. package/dist/scripts/pack-audit.d.ts +1 -0
  11. package/dist/scripts/pack-audit.js +45 -0
  12. package/dist/scripts/schema-packs.d.ts +1 -0
  13. package/dist/scripts/schema-packs.js +150 -0
  14. package/dist/scripts/smoke-dev-profile.d.ts +1 -0
  15. package/dist/scripts/smoke-dev-profile.js +25 -0
  16. package/dist/scripts/sync-readme.d.ts +1 -0
  17. package/dist/scripts/sync-readme.js +141 -0
  18. package/dist/src/auth/credentials.d.ts +65 -0
  19. package/dist/src/auth/credentials.js +200 -0
  20. package/dist/src/auth/device-auth.d.ts +56 -0
  21. package/dist/src/auth/device-auth.js +144 -0
  22. package/dist/src/backend/client.d.ts +61 -0
  23. package/dist/src/backend/client.js +335 -0
  24. package/dist/src/index.d.ts +2 -0
  25. package/dist/src/index.js +92 -0
  26. package/dist/src/schema-packs/llm.d.ts +105 -0
  27. package/dist/src/schema-packs/llm.js +429 -0
  28. package/dist/src/tools/auth.d.ts +1 -0
  29. package/dist/src/tools/auth.js +202 -0
  30. package/dist/src/tools/dev.d.ts +1 -0
  31. package/dist/src/tools/dev.js +187 -0
  32. package/dist/src/tools/index.d.ts +25 -0
  33. package/dist/src/tools/index.js +33 -0
  34. package/dist/src/tools/specialized/eclipse.d.ts +1 -0
  35. package/dist/src/tools/specialized/eclipse.js +56 -0
  36. package/dist/src/tools/specialized/electional.d.ts +1 -0
  37. package/dist/src/tools/specialized/electional.js +79 -0
  38. package/dist/src/tools/specialized/human_design.d.ts +1 -0
  39. package/dist/src/tools/specialized/human_design.js +53 -0
  40. package/dist/src/tools/specialized/moon.d.ts +1 -0
  41. package/dist/src/tools/specialized/moon.js +50 -0
  42. package/dist/src/tools/specialized/natal.d.ts +1 -0
  43. package/dist/src/tools/specialized/natal.js +71 -0
  44. package/dist/src/tools/specialized/relocation.d.ts +1 -0
  45. package/dist/src/tools/specialized/relocation.js +71 -0
  46. package/dist/src/tools/specialized/synastry.d.ts +1 -0
  47. package/dist/src/tools/specialized/synastry.js +61 -0
  48. package/dist/src/tools/specialized/transits.d.ts +1 -0
  49. package/dist/src/tools/specialized/transits.js +80 -0
  50. package/dist/test/allowlist-and-tools.test.d.ts +1 -0
  51. package/dist/test/allowlist-and-tools.test.js +96 -0
  52. package/dist/test/backend-client.test.d.ts +1 -0
  53. package/dist/test/backend-client.test.js +286 -0
  54. package/dist/test/credentials.test.d.ts +1 -0
  55. package/dist/test/credentials.test.js +143 -0
  56. package/dist/tools/dev.js +7 -3
  57. package/dist/tools/index.d.ts +7 -0
  58. package/package.json +3 -3
@@ -15,6 +15,12 @@ export interface BackendRequestOptions {
15
15
  /** Override per-request timeout in milliseconds. */
16
16
  timeoutMs?: number;
17
17
  }
18
+ export interface BinaryBackendResponse {
19
+ content_type: string;
20
+ content_length: number;
21
+ encoding: "base64";
22
+ data_base64: string;
23
+ }
18
24
  export declare class BackendClient {
19
25
  private client;
20
26
  private userId;
@@ -22,6 +28,12 @@ export declare class BackendClient {
22
28
  private serviceKey?;
23
29
  private apiKey?;
24
30
  constructor(config: BackendConfig);
31
+ private expectsBinaryResponse;
32
+ private toBuffer;
33
+ private decodePayload;
34
+ private extractMessage;
35
+ private normalizeContentType;
36
+ private isBinaryContentType;
25
37
  /** Maps an AxiosError to a concise MCP-friendly Error. */
26
38
  private mapError;
27
39
  get<T>(path: string, params?: Record<string, any>, timeoutMs?: number): Promise<T>;
@@ -1,6 +1,12 @@
1
1
  import axios, { AxiosError } from "axios";
2
2
  const DEFAULT_TIMEOUT_MS = 45_000;
3
3
  const RATE_LIMIT_RETRY_DELAYS_MS = [1_000, 2_000]; // Two retries on 429
4
+ const BINARY_ENDPOINT_PREFIXES = [
5
+ "/visualization/bi-wheel",
6
+ "/visualization/chart-wheel",
7
+ "/comparative/visualization/bi-wheel",
8
+ "/comparative/visualization/chart-wheel",
9
+ ];
4
10
  const DASHBOARD_ACCOUNT_URL = "https://openephemeris.com/dashboard?tab=account";
5
11
  const LOGIN_SIGNUP_URL = "https://openephemeris.com/login?signup=true&redirect=%2Fdashboard%3Ftab%3Daccount";
6
12
  const UPGRADE_URL = "https://openephemeris.com/pay";
@@ -62,12 +68,83 @@ export class BackendClient {
62
68
  return req;
63
69
  });
64
70
  }
71
+ expectsBinaryResponse(path, options) {
72
+ const normalizedPath = path.split("?")[0].trim().toLowerCase();
73
+ if (BINARY_ENDPOINT_PREFIXES.some((prefix) => normalizedPath.startsWith(prefix))) {
74
+ return true;
75
+ }
76
+ const format = options?.params?.format;
77
+ if (typeof format === "string") {
78
+ const normalizedFormat = format.trim().toLowerCase();
79
+ if (normalizedFormat === "png" || normalizedFormat === "svg") {
80
+ return true;
81
+ }
82
+ }
83
+ const acceptHeader = Object.entries(options?.headers ?? {}).find(([key]) => key.toLowerCase() === "accept")?.[1];
84
+ if (typeof acceptHeader === "string" && acceptHeader.toLowerCase().includes("image/")) {
85
+ return true;
86
+ }
87
+ return false;
88
+ }
89
+ toBuffer(data) {
90
+ if (data == null)
91
+ return null;
92
+ if (Buffer.isBuffer(data))
93
+ return data;
94
+ if (data instanceof ArrayBuffer)
95
+ return Buffer.from(data);
96
+ if (ArrayBuffer.isView(data)) {
97
+ return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
98
+ }
99
+ if (typeof data === "string")
100
+ return Buffer.from(data);
101
+ return null;
102
+ }
103
+ decodePayload(data) {
104
+ const asBuffer = this.toBuffer(data);
105
+ if (!asBuffer) {
106
+ return data;
107
+ }
108
+ const text = asBuffer.toString("utf8").trim();
109
+ if (!text)
110
+ return "";
111
+ try {
112
+ return JSON.parse(text);
113
+ }
114
+ catch {
115
+ return text;
116
+ }
117
+ }
118
+ extractMessage(data, fallback) {
119
+ if (typeof data === "string") {
120
+ const trimmed = data.trim();
121
+ return trimmed || fallback;
122
+ }
123
+ if (data && typeof data === "object") {
124
+ const obj = data;
125
+ for (const key of ["message", "detail", "title", "error"]) {
126
+ const value = obj[key];
127
+ if (typeof value === "string" && value.trim()) {
128
+ return value.trim();
129
+ }
130
+ }
131
+ }
132
+ return fallback;
133
+ }
134
+ normalizeContentType(value) {
135
+ if (typeof value !== "string")
136
+ return "";
137
+ return value.split(";")[0]?.trim().toLowerCase() || "";
138
+ }
139
+ isBinaryContentType(contentType) {
140
+ return contentType.startsWith("image/") || contentType === "application/octet-stream";
141
+ }
65
142
  /** Maps an AxiosError to a concise MCP-friendly Error. */
66
143
  mapError(error) {
67
144
  if (error.response) {
68
145
  const status = error.response.status;
69
- const data = error.response.data;
70
- const msg = data?.message || data?.detail || data?.title || error.message;
146
+ const data = this.decodePayload(error.response.data);
147
+ const msg = this.extractMessage(data, error.message);
71
148
  if (status === 401) {
72
149
  return new Error(`Authentication required: ${msg}. Sign in or create an account at ${LOGIN_SIGNUP_URL}, ` +
73
150
  `create credentials in ${DASHBOARD_ACCOUNT_URL}, and set ASTROMCP_API_KEY (or ASTROMCP_JWT / ASTROMCP_SERVICE_KEY) before retrying.`);
@@ -94,45 +171,17 @@ export class BackendClient {
94
171
  return new Error(`Network error: ${error.message}`);
95
172
  }
96
173
  async get(path, params, timeoutMs) {
97
- try {
98
- const response = await this.client.get(path, {
99
- params,
100
- timeout: timeoutMs ?? DEFAULT_TIMEOUT_MS,
101
- });
102
- return response.data;
103
- }
104
- catch (err) {
105
- if (err instanceof AxiosError)
106
- throw this.mapError(err);
107
- throw err;
108
- }
174
+ return this.request("GET", path, { params, timeoutMs });
109
175
  }
110
176
  async post(path, data, timeoutMs) {
111
- try {
112
- const response = await this.client.post(path, data, {
113
- timeout: timeoutMs ?? DEFAULT_TIMEOUT_MS,
114
- });
115
- return response.data;
116
- }
117
- catch (err) {
118
- if (err instanceof AxiosError)
119
- throw this.mapError(err);
120
- throw err;
121
- }
177
+ return this.request("POST", path, { data, timeoutMs });
122
178
  }
123
179
  async delete(path) {
124
- try {
125
- const response = await this.client.delete(path);
126
- return response.data;
127
- }
128
- catch (err) {
129
- if (err instanceof AxiosError)
130
- throw this.mapError(err);
131
- throw err;
132
- }
180
+ return this.request("DELETE", path);
133
181
  }
134
182
  async request(method, path, options) {
135
183
  const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
184
+ const expectsBinary = this.expectsBinaryResponse(path, options);
136
185
  let lastError = new Error("Unknown error");
137
186
  // Attempt + retries on 429
138
187
  for (let attempt = 0; attempt <= RATE_LIMIT_RETRY_DELAYS_MS.length; attempt++) {
@@ -144,8 +193,23 @@ export class BackendClient {
144
193
  data: options?.data,
145
194
  headers: options?.headers,
146
195
  timeout: timeoutMs,
196
+ responseType: expectsBinary ? "arraybuffer" : "json",
147
197
  });
148
- return response.data;
198
+ if (!expectsBinary) {
199
+ return response.data;
200
+ }
201
+ const contentType = this.normalizeContentType(response.headers?.["content-type"]);
202
+ if (!this.isBinaryContentType(contentType)) {
203
+ return this.decodePayload(response.data);
204
+ }
205
+ const bytes = this.toBuffer(response.data) ?? Buffer.alloc(0);
206
+ const payload = {
207
+ content_type: contentType || "application/octet-stream",
208
+ content_length: bytes.length,
209
+ encoding: "base64",
210
+ data_base64: bytes.toString("base64"),
211
+ };
212
+ return payload;
149
213
  }
150
214
  catch (err) {
151
215
  if (err instanceof AxiosError) {
package/dist/index.js CHANGED
@@ -36,6 +36,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
36
36
  name: tool.name,
37
37
  description: tool.description,
38
38
  inputSchema: tool.inputSchema,
39
+ annotations: tool.annotations ?? {
40
+ title: tool.name,
41
+ readOnlyHint: true,
42
+ destructiveHint: false,
43
+ },
39
44
  })),
40
45
  };
41
46
  });
@@ -5,7 +5,7 @@ export declare const LLM_V2_DICT: {
5
5
  readonly sign_id: readonly ["ari", "tau", "gem", "can", "leo", "vir", "lib", "sco", "sag", "cap", "aqu", "pis"];
6
6
  readonly aspect_id: readonly ["con", "opp", "tri", "sqr", "sex"];
7
7
  readonly aspect_angle: readonly [0, 180, 120, 90, 60];
8
- readonly kind_id: readonly ["planet", "angle", "node", "lilith", "asteroid", "uranian", "other"];
8
+ readonly kind_id: readonly ["planet", "angle", "node", "lilith", "asteroid", "other"];
9
9
  };
10
10
  export declare const LlmV2PayloadSchema: z.ZodObject<{
11
11
  output_mode: z.ZodOptional<z.ZodLiteral<"llm">>;
@@ -22,7 +22,7 @@ export const LLM_V2_DICT = {
22
22
  sign_id: ["ari", "tau", "gem", "can", "leo", "vir", "lib", "sco", "sag", "cap", "aqu", "pis"],
23
23
  aspect_id: ["con", "opp", "tri", "sqr", "sex"],
24
24
  aspect_angle: [0, 180, 120, 90, 60],
25
- kind_id: ["planet", "angle", "node", "lilith", "asteroid", "uranian", "other"],
25
+ kind_id: ["planet", "angle", "node", "lilith", "asteroid", "other"],
26
26
  };
27
27
  const PointsSchemaZ = z.tuple(LLM_V2_POINTS_SCHEMA.map((v) => z.literal(v)));
28
28
  const AspectsSchemaZ = z.tuple(LLM_V2_ASPECTS_SCHEMA.map((v) => z.literal(v)));
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,287 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import crypto from "node:crypto";
4
+ import { fileURLToPath } from "node:url";
5
+ import axios from "axios";
6
+ function normalizeTier(raw) {
7
+ if (typeof raw !== "string")
8
+ return null;
9
+ const value = raw.trim().toLowerCase();
10
+ if (value === "free" ||
11
+ value === "all" ||
12
+ value === "free/all" ||
13
+ value === "anonymous" ||
14
+ value === "anonymous/all" ||
15
+ value === "public" ||
16
+ value === "public/all")
17
+ return "free";
18
+ if (value === "dev" || value === "developer")
19
+ return "developer";
20
+ if (value === "pro")
21
+ return "pro";
22
+ if (value === "production")
23
+ return "production";
24
+ if (value === "admin" || value === "internal")
25
+ return "admin";
26
+ return null;
27
+ }
28
+ function isCommercialTier(tierNormalized) {
29
+ // Dev MCP should expose all commercially available tiers up through production.
30
+ return tierNormalized === "free" || tierNormalized === "developer" || tierNormalized === "pro" || tierNormalized === "production";
31
+ }
32
+ function sha256(text) {
33
+ return crypto.createHash("sha256").update(text).digest("hex");
34
+ }
35
+ function readJsonFile(filePath) {
36
+ const raw = fs.readFileSync(filePath, "utf-8");
37
+ return { raw, json: JSON.parse(raw) };
38
+ }
39
+ function writeJsonFile(filePath, obj) {
40
+ fs.writeFileSync(filePath, JSON.stringify(obj, null, 2) + "\n", "utf-8");
41
+ }
42
+ function normalizeTag(tag) {
43
+ return tag.trim().toLowerCase();
44
+ }
45
+ function isDeniedByPrefix(pathname, prefixes) {
46
+ return prefixes.some((p) => pathname.startsWith(p));
47
+ }
48
+ function hasDeniedTag(tags, denyTags) {
49
+ const deny = new Set(denyTags.map(normalizeTag));
50
+ return tags.some((t) => deny.has(normalizeTag(t)));
51
+ }
52
+ function getDefaults() {
53
+ const here = path.dirname(fileURLToPath(import.meta.url)); // .../mcp-server/scripts
54
+ const mcpServerRoot = path.resolve(here, "..");
55
+ const repoRoot = path.resolve(mcpServerRoot, "..");
56
+ return {
57
+ mcpServerRoot,
58
+ repoRoot,
59
+ allowlistPath: path.join(mcpServerRoot, "config", "dev-allowlist.json"),
60
+ openapiPath: path.join(repoRoot, "go-sidecar", "openapi.json"),
61
+ };
62
+ }
63
+ function parseArgs(argv) {
64
+ const args = {
65
+ openapiPath: undefined,
66
+ allowlistPath: undefined,
67
+ write: false,
68
+ check: false,
69
+ };
70
+ for (let i = 0; i < argv.length; i++) {
71
+ const a = argv[i];
72
+ if (a === "--openapi")
73
+ args.openapiPath = argv[++i];
74
+ else if (a === "--allowlist")
75
+ args.allowlistPath = argv[++i];
76
+ else if (a === "--write")
77
+ args.write = true;
78
+ else if (a === "--check")
79
+ args.check = true;
80
+ else if (a === "--help" || a === "-h") {
81
+ console.log([
82
+ "Dev allowlist helper (astromcp-dev-allowlist-v1)",
83
+ "",
84
+ "Usage:",
85
+ " tsx scripts/dev-allowlist.ts --check",
86
+ " tsx scripts/dev-allowlist.ts --write",
87
+ "",
88
+ "Options:",
89
+ " --openapi <path> Path to OpenAPI JSON (default: ../go-sidecar/openapi.json)",
90
+ " --allowlist <path> Path to allowlist JSON (default: config/dev-allowlist.json)",
91
+ " --check Validate allowlist against OpenAPI + deny rules (exit 1 on failure)",
92
+ " --write Update allowlist metadata + candidates + auto-allow commercial-tier operations",
93
+ ].join("\n"));
94
+ process.exit(0);
95
+ }
96
+ }
97
+ return args;
98
+ }
99
+ function asHttpMethod(m) {
100
+ const up = m.toUpperCase();
101
+ if (up === "GET" || up === "POST" || up === "PUT" || up === "PATCH" || up === "DELETE")
102
+ return up;
103
+ return null;
104
+ }
105
+ function buildCandidates(openapi, denyPrefixes, denyTags) {
106
+ const candidatesGet = [];
107
+ const candidatesNonGet = [];
108
+ const index = new Map();
109
+ for (const [pathname, pathItem] of Object.entries(openapi.paths || {})) {
110
+ // OpenAPI keys can include templated params; we keep as-is.
111
+ if (denyPrefixes.length && isDeniedByPrefix(pathname, denyPrefixes))
112
+ continue;
113
+ for (const [methodKey, operation] of Object.entries(pathItem || {})) {
114
+ const method = asHttpMethod(methodKey);
115
+ if (!method)
116
+ continue;
117
+ const tags = Array.isArray(operation?.tags) ? operation.tags : [];
118
+ if (denyTags.length && hasDeniedTag(tags, denyTags))
119
+ continue;
120
+ const tier = typeof operation?.["x-meridian-tier"] === "string" ? String(operation["x-meridian-tier"]) : undefined;
121
+ const tier_normalized = tier ? normalizeTier(tier) ?? undefined : undefined;
122
+ const candidate = {
123
+ method,
124
+ path: pathname,
125
+ tags,
126
+ tier,
127
+ tier_normalized,
128
+ operationId: typeof operation?.operationId === "string" ? operation.operationId : undefined,
129
+ summary: typeof operation?.summary === "string" ? operation.summary : undefined,
130
+ };
131
+ const key = `${method} ${pathname}`;
132
+ index.set(key, candidate);
133
+ if (method === "GET")
134
+ candidatesGet.push(candidate);
135
+ else
136
+ candidatesNonGet.push(candidate);
137
+ }
138
+ }
139
+ candidatesGet.sort((a, b) => (a.path + a.method).localeCompare(b.path + b.method));
140
+ candidatesNonGet.sort((a, b) => (a.path + a.method).localeCompare(b.path + b.method));
141
+ return { candidatesGet, candidatesNonGet, index };
142
+ }
143
+ function buildCommercialAllow(candidates, existingAllow) {
144
+ // Build a lookup of existing notes/metadata to preserve across regen
145
+ const existingMeta = new Map();
146
+ for (const e of existingAllow) {
147
+ if (e.note)
148
+ existingMeta.set(`${e.method} ${e.path}`, { note: e.note });
149
+ }
150
+ const allow = candidates
151
+ .filter((c) => isCommercialTier(c.tier_normalized))
152
+ .map((c) => {
153
+ const key = `${c.method} ${c.path}`;
154
+ const entry = { method: c.method, path: c.path };
155
+ const meta = existingMeta.get(key);
156
+ if (meta?.note)
157
+ entry.note = meta.note;
158
+ return entry;
159
+ });
160
+ // Deterministic ordering for diffs.
161
+ allow.sort((a, b) => (a.path + a.method).localeCompare(b.path + b.method));
162
+ return allow;
163
+ }
164
+ function validateAllowlist(allowlist, index, denyPrefixes, denyTags) {
165
+ const errors = [];
166
+ const warnings = [];
167
+ for (const entry of allowlist.allow) {
168
+ if (!entry || typeof entry !== "object") {
169
+ errors.push(`Invalid allow entry (not an object): ${JSON.stringify(entry)}`);
170
+ continue;
171
+ }
172
+ const method = entry.method;
173
+ const pathname = entry.path;
174
+ if (!asHttpMethod(method)) {
175
+ errors.push(`Invalid method in allow entry: ${JSON.stringify(entry)}`);
176
+ continue;
177
+ }
178
+ if (typeof pathname !== "string" || !pathname.startsWith("/")) {
179
+ errors.push(`Invalid path in allow entry: ${JSON.stringify(entry)}`);
180
+ continue;
181
+ }
182
+ if (denyPrefixes.length && isDeniedByPrefix(pathname, denyPrefixes)) {
183
+ errors.push(`Allowlisted entry violates deny prefix rule: ${method} ${pathname}`);
184
+ continue;
185
+ }
186
+ const key = `${method.toUpperCase()} ${pathname}`;
187
+ const candidate = index.get(key);
188
+ if (!candidate) {
189
+ errors.push(`Allowlisted entry not found in OpenAPI: ${key}`);
190
+ continue;
191
+ }
192
+ if (!isCommercialTier(candidate.tier_normalized)) {
193
+ errors.push(`Allowlisted entry is not a commercial-tier operation: ${key} (x-meridian-tier=${candidate.tier ?? "<missing>"})`);
194
+ continue;
195
+ }
196
+ if (denyTags.length && hasDeniedTag(candidate.tags, denyTags)) {
197
+ errors.push(`Allowlisted entry violates deny tag rule: ${key} (tags=${candidate.tags.join(",")})`);
198
+ continue;
199
+ }
200
+ // Method gating reminder: non-GET is allowed only by explicit allowlist (which it is),
201
+ // but we keep a warning so reviewers notice.
202
+ if (method.toUpperCase() !== "GET") {
203
+ warnings.push(`Non-GET allowlisted: ${key}`);
204
+ }
205
+ }
206
+ return { errors, warnings };
207
+ }
208
+ async function getOpenApiDocument(inputPathOrUrl) {
209
+ if (inputPathOrUrl.startsWith("http")) {
210
+ console.log(`Fetching OpenAPI from URL: ${inputPathOrUrl}`);
211
+ const response = await axios.get(inputPathOrUrl);
212
+ const json = response.data;
213
+ return { raw: JSON.stringify(json), json };
214
+ }
215
+ const raw = fs.readFileSync(inputPathOrUrl, "utf-8");
216
+ return { raw, json: JSON.parse(raw) };
217
+ }
218
+ async function main() {
219
+ const defaults = getDefaults();
220
+ const args = parseArgs(process.argv.slice(2));
221
+ const allowlistPath = path.resolve(args.allowlistPath || defaults.allowlistPath);
222
+ const openapiPathOrUrl = args.openapiPath || process.env.ASTROMCP_OPENAPI_URL || defaults.openapiPath;
223
+ const { raw: allowRaw, json: allowlist } = readJsonFile(allowlistPath);
224
+ if (!allowlist || allowlist.schema !== "astromcp-dev-allowlist-v1" || !Array.isArray(allowlist.allow)) {
225
+ throw new Error(`Invalid allowlist schema at ${allowlistPath}`);
226
+ }
227
+ const denyPrefixes = allowlist.deny?.path_prefixes ?? [];
228
+ const denyTags = allowlist.deny?.tags ?? [];
229
+ const { raw: openapiRaw, json: openapi } = await getOpenApiDocument(openapiPathOrUrl);
230
+ if (!openapi || typeof openapi !== "object" || !openapi.paths) {
231
+ throw new Error(`Invalid OpenAPI document at ${openapiPathOrUrl}`);
232
+ }
233
+ const openapiHash = sha256(openapiRaw);
234
+ const allowlistHash = sha256(allowRaw);
235
+ const { candidatesGet, candidatesNonGet, index } = buildCandidates(openapi, denyPrefixes, denyTags);
236
+ const computedAllow = buildCommercialAllow([...candidatesGet, ...candidatesNonGet], allowlist.allow);
237
+ const allowlistForValidation = args.write ? { ...allowlist, allow: computedAllow } : allowlist;
238
+ const { errors, warnings } = validateAllowlist(allowlistForValidation, index, denyPrefixes, denyTags);
239
+ const report = {
240
+ ok: errors.length === 0,
241
+ allowlistPath,
242
+ openapiPath: openapiPathOrUrl,
243
+ openapi_sha256: openapiHash,
244
+ allowlist_sha256: allowlistHash,
245
+ counts: {
246
+ allow: allowlistForValidation.allow.length,
247
+ candidates_get: candidatesGet.length,
248
+ candidates_non_get: candidatesNonGet.length,
249
+ errors: errors.length,
250
+ warnings: warnings.length,
251
+ },
252
+ errors,
253
+ warnings,
254
+ };
255
+ if (args.write) {
256
+ const now = new Date().toISOString();
257
+ const updated = {
258
+ ...allowlist,
259
+ generated_from: openapiPathOrUrl,
260
+ last_generated_at: now,
261
+ openapi_sha256: openapiHash,
262
+ allow: computedAllow,
263
+ candidates_get: candidatesGet.map((c) => ({ method: c.method, path: c.path, tags: c.tags, operationId: c.operationId })),
264
+ candidates_non_get: candidatesNonGet.map((c) => ({ method: c.method, path: c.path, tags: c.tags, operationId: c.operationId })),
265
+ validation: {
266
+ ok: errors.length === 0,
267
+ errors,
268
+ warnings,
269
+ },
270
+ };
271
+ writeJsonFile(allowlistPath, updated);
272
+ }
273
+ if (args.check) {
274
+ if (!report.ok) {
275
+ console.error(JSON.stringify(report, null, 2));
276
+ process.exit(1);
277
+ }
278
+ // Print a small success line for CI.
279
+ console.log(JSON.stringify(report, null, 2));
280
+ return;
281
+ }
282
+ console.log(JSON.stringify(report, null, 2));
283
+ }
284
+ main().catch((err) => {
285
+ console.error(err instanceof Error ? err.message : String(err));
286
+ process.exit(1);
287
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,45 @@
1
+ import { execSync } from "node:child_process";
2
+ function main() {
3
+ const raw = execSync("npm pack --dry-run --json", {
4
+ encoding: "utf-8",
5
+ stdio: ["ignore", "pipe", "inherit"],
6
+ });
7
+ const parsed = JSON.parse(raw);
8
+ if (!Array.isArray(parsed) || parsed.length === 0) {
9
+ throw new Error("Unable to parse npm pack --dry-run --json output.");
10
+ }
11
+ const pack = parsed[0];
12
+ const filePaths = new Set(pack.files.map((f) => f.path));
13
+ const required = ["dist/index.js", "config/dev-allowlist.json", "README.md", "package.json"];
14
+ const missing = required.filter((f) => !filePaths.has(f));
15
+ if (missing.length > 0) {
16
+ throw new Error(`Pack audit failed: required files missing: ${missing.join(", ")}`);
17
+ }
18
+ const bannedPatterns = [
19
+ /^src\//,
20
+ /^test\//,
21
+ /^scripts\//,
22
+ /^test-output/,
23
+ /^diff\.txt$/,
24
+ /^tsconfig(?:\.test)?\.json$/,
25
+ /^package-lock\.json$/,
26
+ /^node_modules\//,
27
+ ];
28
+ const banned = pack.files
29
+ .map((f) => f.path)
30
+ .filter((p) => bannedPatterns.some((re) => re.test(p)));
31
+ if (banned.length > 0) {
32
+ throw new Error(`Pack audit failed: unexpected files in tarball: ${banned.join(", ")}`);
33
+ }
34
+ const distFiles = pack.files.filter((f) => f.path.startsWith("dist/"));
35
+ if (distFiles.length === 0) {
36
+ throw new Error("Pack audit failed: no dist/* files present in tarball.");
37
+ }
38
+ console.log(JSON.stringify({
39
+ ok: true,
40
+ filename: pack.filename,
41
+ file_count: pack.files.length,
42
+ dist_file_count: distFiles.length,
43
+ }, null, 2));
44
+ }
45
+ main();
@@ -0,0 +1 @@
1
+ export {};