bonescript-compiler 0.5.8 → 0.6.1

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 (56) hide show
  1. package/dist/ast.d.ts +2 -0
  2. package/dist/cli.js +52 -8
  3. package/dist/cli.js.map +1 -1
  4. package/dist/emit_admin.d.ts +5 -0
  5. package/dist/emit_admin.js +340 -35
  6. package/dist/emit_admin.js.map +1 -1
  7. package/dist/emit_audit.js +38 -4
  8. package/dist/emit_audit.js.map +1 -1
  9. package/dist/emit_capability.js +14 -0
  10. package/dist/emit_capability.js.map +1 -1
  11. package/dist/emit_full.js +10 -2
  12. package/dist/emit_full.js.map +1 -1
  13. package/dist/emit_maintenance.js +35 -3
  14. package/dist/emit_maintenance.js.map +1 -1
  15. package/dist/emit_nakama.js +36 -36
  16. package/dist/emit_notify.js +30 -2
  17. package/dist/emit_notify.js.map +1 -1
  18. package/dist/emit_runtime.d.ts +18 -1
  19. package/dist/emit_runtime.js +265 -85
  20. package/dist/emit_runtime.js.map +1 -1
  21. package/dist/emit_websocket.js +22 -2
  22. package/dist/emit_websocket.js.map +1 -1
  23. package/dist/emit_zod.js +12 -1
  24. package/dist/emit_zod.js.map +1 -1
  25. package/dist/formatter.d.ts +1 -0
  26. package/dist/formatter.js +10 -2
  27. package/dist/formatter.js.map +1 -1
  28. package/dist/ir.d.ts +2 -0
  29. package/dist/lexer.d.ts +1 -0
  30. package/dist/lexer.js +4 -0
  31. package/dist/lexer.js.map +1 -1
  32. package/dist/lowering.js +2 -0
  33. package/dist/lowering.js.map +1 -1
  34. package/dist/parse_decls.js +36 -1
  35. package/dist/parse_decls.js.map +1 -1
  36. package/dist/typechecker.js +9 -0
  37. package/dist/typechecker.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/ast.ts +2 -0
  40. package/src/cli.ts +58 -10
  41. package/src/emit_admin.ts +342 -35
  42. package/src/emit_audit.ts +40 -4
  43. package/src/emit_capability.ts +13 -0
  44. package/src/emit_full.ts +9 -2
  45. package/src/emit_maintenance.ts +35 -3
  46. package/src/emit_nakama.ts +576 -576
  47. package/src/emit_notify.ts +30 -2
  48. package/src/emit_runtime.ts +955 -763
  49. package/src/emit_websocket.ts +22 -2
  50. package/src/emit_zod.ts +11 -1
  51. package/src/formatter.ts +9 -2
  52. package/src/ir.ts +2 -0
  53. package/src/lexer.ts +2 -0
  54. package/src/lowering.ts +5 -3
  55. package/src/parse_decls.ts +31 -1
  56. package/src/typechecker.ts +10 -0
@@ -29,7 +29,23 @@ export function emitWebSocketServer(system: IR.IRSystem): string {
29
29
  lines.push(`import { eventBus } from "./events";`);
30
30
  lines.push(`import { logger } from "./logger";`);
31
31
  lines.push(``);
32
- lines.push(`const JWT_SECRET = process.env.JWT_SECRET || "bonescript-dev-secret-change-in-production";`);
32
+ // Use the same secret-loading rules as the HTTP middleware.
33
+ // Refuse to boot in production without JWT_SECRET; warn in dev.
34
+ lines.push(`const JWT_SECRET = (() => {`);
35
+ lines.push(` const secret = process.env.JWT_SECRET;`);
36
+ lines.push(` if (!secret) {`);
37
+ lines.push(` if (process.env.NODE_ENV === "production") {`);
38
+ lines.push(` console.error("[FATAL] JWT_SECRET environment variable is not set. Refusing to start in production.");`);
39
+ lines.push(` process.exit(1);`);
40
+ lines.push(` }`);
41
+ lines.push(` console.warn("[WARN] JWT_SECRET is not set. Using insecure default — do not use in production.");`);
42
+ lines.push(` return "bonescript-dev-secret-do-not-use-in-production";`);
43
+ lines.push(` }`);
44
+ lines.push(` if (secret.length < 32) {`);
45
+ lines.push(` console.warn("[WARN] JWT_SECRET is shorter than 32 characters. Use a longer secret in production.");`);
46
+ lines.push(` }`);
47
+ lines.push(` return secret;`);
48
+ lines.push(`})();`);
33
49
  lines.push(``);
34
50
  // Redis pub/sub for multi-instance support
35
51
  lines.push(`// Redis pub/sub for multi-instance WebSocket broadcasting`);
@@ -117,7 +133,11 @@ export function emitWebSocketServer(system: IR.IRSystem): string {
117
133
  lines.push(``);
118
134
  lines.push(` let userId: string;`);
119
135
  lines.push(` try {`);
120
- lines.push(` const decoded = jwt.verify(token, JWT_SECRET) as { sub: string };`);
136
+ lines.push(` const decoded = jwt.verify(token, JWT_SECRET, {`);
137
+ lines.push(` algorithms: ["HS256"],`);
138
+ lines.push(` maxAge: process.env.JWT_MAX_AGE || "1h",`);
139
+ lines.push(` }) as { sub?: unknown };`);
140
+ lines.push(` if (typeof decoded.sub !== "string" || decoded.sub.length === 0) throw new Error("invalid sub");`);
121
141
  lines.push(` userId = decoded.sub;`);
122
142
  lines.push(` } catch {`);
123
143
  lines.push(` socket.send(JSON.stringify({ type: "error", message: "Authentication failed" }));`);
package/src/emit_zod.ts CHANGED
@@ -62,9 +62,13 @@ export function emitZodSchemas(system: IR.IRSystem): string {
62
62
  lines.push(`import { z } from "zod";`);
63
63
  lines.push("");
64
64
 
65
- // Model schemas
65
+ // Model schemas — dedupe by name since the same entity can appear in both
66
+ // an api_service module and its backing data_store module.
67
+ const seenModels = new Set<string>();
66
68
  for (const mod of system.modules) {
67
69
  for (const model of mod.models) {
70
+ if (seenModels.has(model.name)) continue;
71
+ seenModels.add(model.name);
68
72
  const schemaName = toPascalCase(model.name) + "Schema";
69
73
  const typeName = toPascalCase(model.name);
70
74
 
@@ -79,6 +83,12 @@ export function emitZodSchemas(system: IR.IRSystem): string {
79
83
  }
80
84
  lines.push(`});`);
81
85
  lines.push(`export type ${typeName} = z.infer<typeof ${schemaName}>;`);
86
+ // Derived schemas for CRUD validation. `omit` drops server-managed fields
87
+ // and `partial` makes every key optional for PUT.
88
+ const hasState = model.fields.some(f => f.name === "state");
89
+ const createPartial = hasState ? ".partial({ state: true })" : "";
90
+ lines.push(`export const ${toPascalCase(model.name)}CreateSchema = ${schemaName}.omit({ id: true, created_at: true, updated_at: true })${createPartial};`);
91
+ lines.push(`export const ${toPascalCase(model.name)}UpdateSchema = ${schemaName}.omit({ id: true, created_at: true, updated_at: true }).partial();`);
82
92
  lines.push("");
83
93
  }
84
94
  }
package/src/formatter.ts CHANGED
@@ -76,7 +76,7 @@ export class Formatter {
76
76
 
77
77
  if (e.owns.length > 0) {
78
78
  if (e.owns.length <= 2) {
79
- const fields = e.owns.map(f => `${f.name}: ${this.formatType(f.type)}`).join(", ");
79
+ const fields = e.owns.map(f => this.formatField(f)).join(", ");
80
80
  this.line(`owns: [${fields}]`);
81
81
  } else {
82
82
  this.line(`owns: [`);
@@ -84,7 +84,7 @@ export class Formatter {
84
84
  for (let i = 0; i < e.owns.length; i++) {
85
85
  const f = e.owns[i];
86
86
  const comma = i < e.owns.length - 1 ? "," : "";
87
- this.line(`${f.name}: ${this.formatType(f.type)}${comma}`);
87
+ this.line(`${this.formatField(f)}${comma}`);
88
88
  }
89
89
  this.indent--;
90
90
  this.line(`]`);
@@ -264,6 +264,13 @@ export class Formatter {
264
264
  }
265
265
  }
266
266
 
267
+ private formatField(f: AST.FieldNode): string {
268
+ let s = `${f.name}: ${this.formatType(f.type)}`;
269
+ if (f.renamedFrom) s += ` @renamed_from(${f.renamedFrom})`;
270
+ if (f.sensitive) s += ` @sensitive`;
271
+ return s;
272
+ }
273
+
267
274
  private formatExpr(e: AST.ExprNode): string {
268
275
  switch (e.kind) {
269
276
  case "Literal":
package/src/ir.ts CHANGED
@@ -17,6 +17,8 @@ export interface IRField {
17
17
  unique: boolean;
18
18
  indexed: boolean;
19
19
  default_value: string | null;
20
+ renamed_from?: string | null;
21
+ sensitive?: boolean;
20
22
  }
21
23
 
22
24
  // ─── Models ──────────────────────────────────────────────────────────────────
package/src/lexer.ts CHANGED
@@ -25,6 +25,7 @@ export enum TokenKind {
25
25
  Arrow = "Arrow",
26
26
  Pipe = "Pipe",
27
27
  Semicolon = "Semicolon",
28
+ At = "At",
28
29
 
29
30
  // Operators
30
31
  Equals = "Equals",
@@ -519,6 +520,7 @@ export class Lexer {
519
520
  case "?": this.advance(); return { kind: TokenKind.Question, value: "?", loc };
520
521
  case "!": this.advance(); return { kind: TokenKind.Bang, value: "!", loc };
521
522
  case ";": this.advance(); return { kind: TokenKind.Semicolon, value: ";", loc };
523
+ case "@": this.advance(); return { kind: TokenKind.At, value: "@", loc };
522
524
  }
523
525
 
524
526
  // String literal
package/src/lowering.ts CHANGED
@@ -380,9 +380,9 @@ export class Lowering {
380
380
  config: {
381
381
  authenticated: entity.auth !== null && entity.auth !== "none",
382
382
  auth_method: entity.auth || "none",
383
- audit: policies.some(p => p.audit === true),
384
- rate_limit: policies.length > 0 && policies[0].rateLimit ? policies[0].rateLimit.count : 0,
385
- rate_limit_window_ms: policies.length > 0 && policies[0].rateLimit ? (parseDurationMs(String(policies[0].rateLimit.per)) || 60000) : 60000,
383
+ audit: policies.some(p => p.audit === true),
384
+ rate_limit: policies.length > 0 && policies[0].rateLimit ? policies[0].rateLimit.count : 0,
385
+ rate_limit_window_ms: policies.length > 0 && policies[0].rateLimit ? (parseDurationMs(String(policies[0].rateLimit.per)) || 60000) : 60000,
386
386
  },
387
387
  };
388
388
  }
@@ -554,6 +554,8 @@ export class Lowering {
554
554
  unique: false,
555
555
  indexed: false,
556
556
  default_value: f.defaultValue ? serializeExpr(f.defaultValue) : null,
557
+ renamed_from: f.renamedFrom ?? null,
558
+ sensitive: f.sensitive ?? false,
557
559
  };
558
560
  }
559
561
  }
@@ -33,7 +33,37 @@ export function parseField(s: TokenStream): AST.FieldNode {
33
33
  const type = parseTypeExpr(s);
34
34
  let defaultValue: AST.ExprNode | null = null;
35
35
  if (s.match(TokenKind.Equals)) { defaultValue = parseExpr(s); }
36
- return { kind: "Field", loc, name, type, defaultValue };
36
+ // Optional annotations: @renamed_from(old_name), @sensitive
37
+ let renamedFrom: string | null = null;
38
+ let sensitive = false;
39
+ while (s.check(TokenKind.At)) {
40
+ s.advance();
41
+ const annoName = parseIdentOrKeyword(s);
42
+ if (annoName === "renamed_from") {
43
+ s.expect(TokenKind.LParen, "renamed_from(old_name)");
44
+ renamedFrom = parseIdentOrKeyword(s);
45
+ s.expect(TokenKind.RParen, "renamed_from close");
46
+ } else if (annoName === "sensitive") {
47
+ // Bare flag annotation. Optional empty parens for forward compat.
48
+ sensitive = true;
49
+ if (s.match(TokenKind.LParen)) {
50
+ s.expect(TokenKind.RParen, "sensitive close");
51
+ }
52
+ } else {
53
+ // Unknown annotation — accept and ignore for forward compat; consume
54
+ // an optional (...) payload so it parses cleanly.
55
+ if (s.match(TokenKind.LParen)) {
56
+ let depth = 1;
57
+ while (depth > 0 && !s.check(TokenKind.EOF)) {
58
+ if (s.check(TokenKind.LParen)) depth++;
59
+ else if (s.check(TokenKind.RParen)) depth--;
60
+ if (depth > 0) s.advance();
61
+ }
62
+ s.expect(TokenKind.RParen, "annotation close");
63
+ }
64
+ }
65
+ }
66
+ return { kind: "Field", loc, name, type, defaultValue, renamedFrom, sensitive };
37
67
  }
38
68
 
39
69
  export function parseIdentList(s: TokenStream): string[] {
@@ -404,6 +404,16 @@ export class TypeChecker {
404
404
  const path = expr.path;
405
405
  if (path.length === 0) return null;
406
406
 
407
+ // Built-in: `caller` resolves to the authenticated actor's identity.
408
+ // `caller.id` is a uuid; bare `caller` is a record { id: uuid } for now.
409
+ if (path[0] === "caller") {
410
+ const callerType = record("Caller", new Map([
411
+ ["id", prim("uuid")],
412
+ ["actor_id", prim("uuid")],
413
+ ]));
414
+ return this.resolveFieldPath(callerType, path.slice(1), expr);
415
+ }
416
+
407
417
  // First segment: look up in context
408
418
  let currentType = ctx.lookup(path[0]);
409
419