bonescript-compiler 0.8.0 → 0.11.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.
@@ -142,20 +142,31 @@ export function emitGraphQLSchema(system: IR.IRSystem): string {
142
142
  }
143
143
 
144
144
  // ─── type Query ───
145
+ // GraphQL spec requires every schema to define a non-empty Query type.
146
+ // For systems with no API modules (event-only or channel-only), emit a
147
+ // placeholder _service field so the schema parses.
145
148
  lines.push(`type Query {`);
146
- for (const f of queryFields) {
147
- lines.push(f);
149
+ if (queryFields.length === 0) {
150
+ lines.push(` _service: String!`);
151
+ } else {
152
+ for (const f of queryFields) {
153
+ lines.push(f);
154
+ }
148
155
  }
149
156
  lines.push(`}`);
150
157
  lines.push(``);
151
158
 
152
159
  // ─── type Mutation ───
153
- lines.push(`type Mutation {`);
154
- for (const f of mutationFields) {
155
- lines.push(f);
160
+ // Mutation is technically optional in GraphQL, but emitting an empty body
161
+ // is a syntax error. Skip the type entirely if there's nothing to mutate.
162
+ if (mutationFields.length > 0) {
163
+ lines.push(`type Mutation {`);
164
+ for (const f of mutationFields) {
165
+ lines.push(f);
166
+ }
167
+ lines.push(`}`);
168
+ lines.push(``);
156
169
  }
157
- lines.push(`}`);
158
- lines.push(``);
159
170
 
160
171
  return lines.join("\n");
161
172
  }
@@ -97,6 +97,33 @@ export function emitNotifyService(system: IR.IRSystem): string {
97
97
  lines.push(` return createHmac("sha256", WEBHOOK_SECRET).update(body).digest("hex");`);
98
98
  lines.push(`}`);
99
99
  lines.push(``);
100
+ // Private-host detection. Catches loopback (127/8, ::1, 0.0.0.0), RFC1918
101
+ // (10/8, 172.16/12, 192.168/16), link-local (169.254/16, fe80::/10) and
102
+ // unique-local IPv6 (fc00::/7). DNS hostnames that resolve to private
103
+ // addresses are not blocked here — that requires a DNS lookup and a
104
+ // dual-stack check; we leave that to the deployer's network policy.
105
+ lines.push(`function isPrivateHost(host: string): boolean {`);
106
+ lines.push(` const h = host.toLowerCase();`);
107
+ lines.push(` if (h === "localhost" || h === "0.0.0.0" || h === "::" || h === "::1") return true;`);
108
+ lines.push(` // IPv4 dotted quad`);
109
+ lines.push(` const m4 = h.match(/^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/);`);
110
+ lines.push(` if (m4) {`);
111
+ lines.push(` const a = Number(m4[1]), b = Number(m4[2]);`);
112
+ lines.push(` if (a === 127) return true; // loopback`);
113
+ lines.push(` if (a === 10) return true; // RFC1918`);
114
+ lines.push(` if (a === 172 && b >= 16 && b <= 31) return true; // RFC1918`);
115
+ lines.push(` if (a === 192 && b === 168) return true; // RFC1918`);
116
+ lines.push(` if (a === 169 && b === 254) return true; // link-local / cloud metadata`);
117
+ lines.push(` if (a === 0) return true;`);
118
+ lines.push(` return false;`);
119
+ lines.push(` }`);
120
+ lines.push(` // IPv6 — strip optional brackets and zone suffix.`);
121
+ lines.push(` const v6 = h.replace(/^\\[|\\]$/g, "").split("%")[0];`);
122
+ lines.push(` if (/^fe[89ab][0-9a-f]?:/i.test(v6)) return true; // link-local fe80::/10`);
123
+ lines.push(` if (/^f[cd][0-9a-f]?:/i.test(v6)) return true; // unique-local fc00::/7`);
124
+ lines.push(` return false;`);
125
+ lines.push(`}`);
126
+ lines.push(``);
100
127
  lines.push(`/**`);
101
128
  lines.push(` * Send a JSON payload to NOTIFY_WEBHOOK_URL.`);
102
129
  lines.push(` *`);
@@ -113,13 +140,21 @@ export function emitNotifyService(system: IR.IRSystem): string {
113
140
  lines.push(` if (!WEBHOOK_URL) {`);
114
141
  lines.push(` throw new Error("NOTIFY_WEBHOOK_URL is not configured");`);
115
142
  lines.push(` }`);
116
- lines.push(` // Validate URL — only http(s) and reject obvious attempts at SSRF (loopback / RFC1918).`);
143
+ lines.push(` // Validate URL — only http(s), and reject loopback / RFC1918 /`);
144
+ lines.push(` // link-local hosts to make this server unusable as an SSRF probe.`);
145
+ lines.push(` // Set NOTIFY_WEBHOOK_ALLOW_PRIVATE=1 to opt out (e.g. for internal CI`);
146
+ lines.push(` // setups where the webhook receiver is on the same network).`);
117
147
  lines.push(` let url: URL;`);
118
148
  lines.push(` try { url = new URL(WEBHOOK_URL); }`);
119
149
  lines.push(` catch { throw new Error("Invalid NOTIFY_WEBHOOK_URL"); }`);
120
150
  lines.push(` if (url.protocol !== "https:" && url.protocol !== "http:") {`);
121
151
  lines.push(` throw new Error(\`Webhook URL protocol must be http(s), got \${url.protocol}\`);`);
122
152
  lines.push(` }`);
153
+ lines.push(` if (process.env.NOTIFY_WEBHOOK_ALLOW_PRIVATE !== "1") {`);
154
+ lines.push(` if (isPrivateHost(url.hostname)) {`);
155
+ lines.push(` throw new Error(\`Webhook URL host is loopback / private / link-local: \${url.hostname}. Set NOTIFY_WEBHOOK_ALLOW_PRIVATE=1 to allow.\`);`);
156
+ lines.push(` }`);
157
+ lines.push(` }`);
123
158
  lines.push(` const body = JSON.stringify(payload);`);
124
159
  lines.push(` const headers: Record<string, string> = { "Content-Type": "application/json" };`);
125
160
  lines.push(` const sig = signPayload(body);`);
@@ -50,8 +50,13 @@ function toPrismaNativeType(irType: string): string | null {
50
50
  case "bytes": return null;
51
51
  case "timestamp": return "@db.Timestamptz";
52
52
  case "float": return "@db.DoublePrecision";
53
- case "uint": return "@db.BigInt";
54
- case "int": return "@db.BigInt";
53
+ // BoneScript `uint` and `int` map to Prisma `Int`. Prisma rejects
54
+ // `Int @db.BigInt` (BigInt requires the Prisma `BigInt` type), so we use
55
+ // no native type and let Prisma map to the default Postgres `INTEGER`.
56
+ // If a caller needs 64-bit ints they should use the `int` IR type with a
57
+ // future `@db.bigint` annotation — TBD.
58
+ case "uint": return null;
59
+ case "int": return null;
55
60
  default: return null;
56
61
  }
57
62
  }
@@ -253,6 +258,9 @@ export class PrismaEmitter {
253
258
  } else if (field.name === "created_at" || (field.type === "timestamp" && field.name.includes("created"))) {
254
259
  attrs.push("@default(now())");
255
260
  } else if (field.name === "updated_at") {
261
+ // @updatedAt only fires on update — we also need a default for create
262
+ // or the NOT NULL column has no value at INSERT time.
263
+ attrs.push("@default(now())");
256
264
  attrs.push("@updatedAt");
257
265
  } else if (field.default_value) {
258
266
  const dv = this.mapDefaultValue(field.default_value, field.type);