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.
- package/dist/ast.d.ts +2 -0
- package/dist/cli.js +52 -8
- package/dist/cli.js.map +1 -1
- package/dist/emit_admin.d.ts +5 -0
- package/dist/emit_admin.js +340 -35
- package/dist/emit_admin.js.map +1 -1
- package/dist/emit_audit.js +38 -4
- package/dist/emit_audit.js.map +1 -1
- package/dist/emit_capability.js +14 -0
- package/dist/emit_capability.js.map +1 -1
- package/dist/emit_full.js +10 -2
- package/dist/emit_full.js.map +1 -1
- package/dist/emit_maintenance.js +35 -3
- package/dist/emit_maintenance.js.map +1 -1
- package/dist/emit_nakama.js +36 -36
- package/dist/emit_notify.js +30 -2
- package/dist/emit_notify.js.map +1 -1
- package/dist/emit_runtime.d.ts +18 -1
- package/dist/emit_runtime.js +265 -85
- package/dist/emit_runtime.js.map +1 -1
- package/dist/emit_websocket.js +22 -2
- package/dist/emit_websocket.js.map +1 -1
- package/dist/emit_zod.js +12 -1
- package/dist/emit_zod.js.map +1 -1
- package/dist/formatter.d.ts +1 -0
- package/dist/formatter.js +10 -2
- package/dist/formatter.js.map +1 -1
- package/dist/ir.d.ts +2 -0
- package/dist/lexer.d.ts +1 -0
- package/dist/lexer.js +4 -0
- package/dist/lexer.js.map +1 -1
- package/dist/lowering.js +2 -0
- package/dist/lowering.js.map +1 -1
- package/dist/parse_decls.js +36 -1
- package/dist/parse_decls.js.map +1 -1
- package/dist/typechecker.js +9 -0
- package/dist/typechecker.js.map +1 -1
- package/package.json +1 -1
- package/src/ast.ts +2 -0
- package/src/cli.ts +58 -10
- package/src/emit_admin.ts +342 -35
- package/src/emit_audit.ts +40 -4
- package/src/emit_capability.ts +13 -0
- package/src/emit_full.ts +9 -2
- package/src/emit_maintenance.ts +35 -3
- package/src/emit_nakama.ts +576 -576
- package/src/emit_notify.ts +30 -2
- package/src/emit_runtime.ts +955 -763
- package/src/emit_websocket.ts +22 -2
- package/src/emit_zod.ts +11 -1
- package/src/formatter.ts +9 -2
- package/src/ir.ts +2 -0
- package/src/lexer.ts +2 -0
- package/src/lowering.ts +5 -3
- package/src/parse_decls.ts +31 -1
- package/src/typechecker.ts +10 -0
package/src/emit_websocket.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 =>
|
|
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(`${
|
|
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
|
}
|
package/src/parse_decls.ts
CHANGED
|
@@ -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
|
-
|
|
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[] {
|
package/src/typechecker.ts
CHANGED
|
@@ -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
|
|