laravel-workers 0.0.0-stage → 0.4.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.
- package/LICENSE +21 -0
- package/README.md +295 -2
- package/bin/laravel-workers.mjs +141 -0
- package/build.mjs +2599 -0
- package/compiler/bin/controller.mjs +92 -0
- package/compiler/bin/transpile.mjs +65 -0
- package/compiler/runtime/rt-from-context.mjs +2399 -0
- package/compiler/src/classes.mjs +1232 -0
- package/compiler/src/controller.mjs +1029 -0
- package/compiler/src/dist-swap.mjs +76 -0
- package/compiler/src/mail-reach.mjs +104 -0
- package/compiler/src/policy.mjs +345 -0
- package/compiler/src/transpiler.mjs +3094 -0
- package/compiler/src/view-name.mjs +21 -0
- package/kernel/auth.mjs +1502 -0
- package/kernel/cache.mjs +405 -0
- package/kernel/collections.mjs +1615 -0
- package/kernel/cookies.mjs +474 -0
- package/kernel/csrf.mjs +698 -0
- package/kernel/db.mjs +703 -0
- package/kernel/digest.mjs +360 -0
- package/kernel/encrypter.mjs +106 -0
- package/kernel/form-request.mjs +106 -0
- package/kernel/hash.mjs +113 -0
- package/kernel/html-entities.mjs +268 -0
- package/kernel/http.mjs +1803 -0
- package/kernel/image.mjs +341 -0
- package/kernel/mail.mjs +269 -0
- package/kernel/mailable.mjs +172 -0
- package/kernel/model.mjs +1226 -0
- package/kernel/multipart.mjs +161 -0
- package/kernel/paginator.mjs +336 -0
- package/kernel/php-json.mjs +220 -0
- package/kernel/php-serialize.mjs +245 -0
- package/kernel/queue.mjs +96 -0
- package/kernel/ratelimit.mjs +855 -0
- package/kernel/redis.mjs +264 -0
- package/kernel/resources.mjs +857 -0
- package/kernel/response.mjs +17 -0
- package/kernel/router.mjs +276 -0
- package/kernel/scheduler.mjs +236 -0
- package/kernel/service-adapters.mjs +49 -0
- package/kernel/session.mjs +934 -0
- package/kernel/storage.mjs +265 -0
- package/kernel/str.mjs +445 -0
- package/kernel/tests/auth.test.mjs +1074 -0
- package/kernel/tests/budget.test.mjs +527 -0
- package/kernel/tests/cache.test.mjs +290 -0
- package/kernel/tests/collections.test.mjs +534 -0
- package/kernel/tests/cookies.test.mjs +302 -0
- package/kernel/tests/csrf.test.mjs +597 -0
- package/kernel/tests/db.test.mjs +1069 -0
- package/kernel/tests/encrypter.test.mjs +90 -0
- package/kernel/tests/fixtures/laravel-1337-cookies.json +120 -0
- package/kernel/tests/hash.test.mjs +149 -0
- package/kernel/tests/http.test.mjs +1182 -0
- package/kernel/tests/image.test.mjs +235 -0
- package/kernel/tests/login-flow.test.mjs +509 -0
- package/kernel/tests/model.test.mjs +324 -0
- package/kernel/tests/oracle-app.mjs +32 -0
- package/kernel/tests/paginator.test.mjs +132 -0
- package/kernel/tests/php-json.test.mjs +263 -0
- package/kernel/tests/php-serialize.test.mjs +210 -0
- package/kernel/tests/ports.test.mjs +47 -0
- package/kernel/tests/ratelimit.test.mjs +975 -0
- package/kernel/tests/readiness.test.mjs +155 -0
- package/kernel/tests/redis.test.mjs +175 -0
- package/kernel/tests/registry-json.test.mjs +28 -0
- package/kernel/tests/resources.test.mjs +275 -0
- package/kernel/tests/router.test.mjs +281 -0
- package/kernel/tests/routes-json.test.mjs +32 -0
- package/kernel/tests/services.test.mjs +474 -0
- package/kernel/tests/session.test.mjs +709 -0
- package/kernel/tests/sqlite-integration.test.mjs +390 -0
- package/kernel/tests/storage.test.mjs +133 -0
- package/kernel/tests/str.test.mjs +83 -0
- package/kernel/tests/upload.test.mjs +193 -0
- package/kernel/tests/validation-messages.test.mjs +117 -0
- package/kernel/tests/validation.test.mjs +340 -0
- package/kernel/tests/validator-hooks.test.mjs +343 -0
- package/kernel/tests/validator.test.mjs +805 -0
- package/kernel/tests/vendor-lite.test.mjs +39 -0
- package/kernel/validation-messages.data.mjs +168 -0
- package/kernel/validation-messages.mjs +249 -0
- package/kernel/validation.mjs +123 -0
- package/kernel/validator.mjs +1538 -0
- package/package.json +68 -5
- package/shadow/run.mjs +784 -0
- package/shadow/scenarios/fresh-app.mjs +60 -0
- package/shadow/scenarios/lw-constructs-app.mjs +669 -0
- package/src/ports/dbcheck.mjs +22 -0
- package/src/worker.mjs +200 -0
- package/tools/_root.php +135 -0
- package/tools/bench.mjs +129 -0
- package/tools/class-dump.php +87 -0
- package/tools/compile-view.php +263 -0
- package/tools/config-dump.php +192 -0
- package/tools/constants-dump.php +46 -0
- package/tools/construct-audit.mjs +326 -0
- package/tools/constructs.json +1510 -0
- package/tools/docs-check.mjs +149 -0
- package/tools/event-dump.php +320 -0
- package/tools/formrequest-dump.php +229 -0
- package/tools/gate.mjs +36 -0
- package/tools/http-oracle.php +200 -0
- package/tools/init.mjs +73 -0
- package/tools/integrations.json +69 -0
- package/tools/mailable-views.php +116 -0
- package/tools/make-cookie-fixture.mjs +87 -0
- package/tools/make-html-entities.mjs +106 -0
- package/tools/model-dump.php +198 -0
- package/tools/php-auth-oracle.php +701 -0
- package/tools/php-cache-oracle.php +166 -0
- package/tools/php-crypto-oracle.php +113 -0
- package/tools/php-csrf-oracle.php +271 -0
- package/tools/php-http-pipeline-oracle.php +358 -0
- package/tools/php-json-oracle.php +102 -0
- package/tools/php-login-flow-oracle.php +342 -0
- package/tools/php-provision.mjs +172 -0
- package/tools/php-ratelimit-oracle.php +508 -0
- package/tools/php-runtime.mjs +342 -0
- package/tools/php-serialize-oracle.php +114 -0
- package/tools/php-session-oracle.php +266 -0
- package/tools/php-shadow-oracle.php +588 -0
- package/tools/policy-dump.php +240 -0
- package/tools/ports.mjs +51 -0
- package/tools/prerender.php +68 -0
- package/tools/readiness.mjs +255 -0
- package/tools/registry-json.mjs +22 -0
- package/tools/routes-dump.php +27 -0
- package/tools/routes-json.mjs +48 -0
- package/tools/schedule-dump.php +82 -0
- package/tools/schema-dump.php +158 -0
- package/tools/validation-messages.php +54 -0
- package/tools/vendor-lite.mjs +299 -0
- package/tools/verify-s0.php +209 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// compiler/src/dist-swap.mjs — the atomic swap of the build output.
|
|
2
|
+
//
|
|
3
|
+
// The build writes every artifact into `<dist>.building` and calls commitDist()
|
|
4
|
+
// only after its LAST check passed. A build that fails therefore leaves the
|
|
5
|
+
// previous `dist/` untouched: an operator (or a watcher, or CI) can never pick
|
|
6
|
+
// up a half-written artifact, and the bundler never sees a `dist` whose
|
|
7
|
+
// generated modules were not emitted yet.
|
|
8
|
+
//
|
|
9
|
+
// Windows detail: a running `wrangler dev` (or an editor) can hold a handle on
|
|
10
|
+
// `dist/` or a subdirectory, so the rename can fail even though every file is
|
|
11
|
+
// closed. The fallback wipes the contents and keeps the locked directory
|
|
12
|
+
// shells — the build rewrites everything anyway.
|
|
13
|
+
import { existsSync, readdirSync, renameSync, rmSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
|
|
16
|
+
/** Remove a directory tree, tolerating a locked entry. */
|
|
17
|
+
function wipe(dir, onLocked) {
|
|
18
|
+
try {
|
|
19
|
+
rmSync(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 });
|
|
20
|
+
return;
|
|
21
|
+
} catch {
|
|
22
|
+
/* a handle is held — wipe what can be wiped */
|
|
23
|
+
}
|
|
24
|
+
if (!existsSync(dir)) return;
|
|
25
|
+
for (const entry of readdirSync(dir)) {
|
|
26
|
+
try {
|
|
27
|
+
rmSync(join(dir, entry), { recursive: true, force: true, maxRetries: 10, retryDelay: 200 });
|
|
28
|
+
} catch {
|
|
29
|
+
if (onLocked) onLocked(join(dir, entry));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Move the finished staging directory into place.
|
|
36
|
+
*
|
|
37
|
+
* The previous `dist/` is renamed aside first and deleted only AFTER the new
|
|
38
|
+
* one landed: a crash between the two renames leaves the old build recoverable
|
|
39
|
+
* under `<dist>.old-*` (the next build sweeps those), never a mix of both.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} staging the directory the build wrote
|
|
42
|
+
* @param {string} final the directory the app (and wrangler) reads
|
|
43
|
+
* @param {(path: string) => void} [onLocked] told about each entry it cannot delete
|
|
44
|
+
*/
|
|
45
|
+
export function commitDist(staging, final, onLocked) {
|
|
46
|
+
if (!existsSync(staging)) {
|
|
47
|
+
throw new Error('commitDist: the staging directory ' + staging + ' does not exist');
|
|
48
|
+
}
|
|
49
|
+
const previous = final + '.old-' + Date.now();
|
|
50
|
+
let moved = false;
|
|
51
|
+
if (existsSync(final)) {
|
|
52
|
+
try {
|
|
53
|
+
renameSync(final, previous);
|
|
54
|
+
moved = true;
|
|
55
|
+
} catch {
|
|
56
|
+
wipe(final, onLocked);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
renameSync(staging, final);
|
|
60
|
+
if (moved) {
|
|
61
|
+
try {
|
|
62
|
+
rmSync(previous, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
63
|
+
} catch {
|
|
64
|
+
/* locked — the `dist.old-*` sweep removes it on the next build */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Throw the staging directory away (a failed build). */
|
|
70
|
+
export function discardStaging(staging) {
|
|
71
|
+
try {
|
|
72
|
+
rmSync(staging, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
73
|
+
} catch {
|
|
74
|
+
/* still locked — the next build wipes it */
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// compiler/src/mail-reach.mjs — the class graph a route reaches.
|
|
2
|
+
//
|
|
3
|
+
// A route does not only render its OWN views: a mailable renders ITS view,
|
|
4
|
+
// and at request time the mailable renders it through the ROUTE's runtime
|
|
5
|
+
// (kernel/mailable.mjs: the mailable's __lwMail() calls the runtime's
|
|
6
|
+
// view()). So the view of every mailable a route can REACH must be in that
|
|
7
|
+
// route's compiled view map - otherwise the request answers 500 ("view not
|
|
8
|
+
// compiled") where Laravel answers 200 and sends the mail.
|
|
9
|
+
//
|
|
10
|
+
// "Reach" is the class graph, and the build knows it exactly:
|
|
11
|
+
// - the route's own `new X()` sites,
|
|
12
|
+
// - a class's constructor dependencies (the runtime builds them: the exact
|
|
13
|
+
// edge - a service a listener injects is ALWAYS built),
|
|
14
|
+
// - a class's `new X()` sites (the compiler resolved them to their FQCN),
|
|
15
|
+
// - the listeners of an event class (the runtime dispatches to them).
|
|
16
|
+
//
|
|
17
|
+
// The graph is walked with a visited set: a listener can dispatch an event
|
|
18
|
+
// whose listener dispatches back.
|
|
19
|
+
//
|
|
20
|
+
// The walk is deliberately over-approximating on the METHOD-CONDITIONAL edge
|
|
21
|
+
// (a class's `new` sites): over-attaching a view is harmless (the route
|
|
22
|
+
// imports a view it may not render), while under-attaching one is a
|
|
23
|
+
// request-time 500 where Laravel answers 200.
|
|
24
|
+
|
|
25
|
+
// The classes a route instantiates, resolved the way the COMPILER resolved
|
|
26
|
+
// them: the build's alias map (a short name -> the FQCN of the compiled
|
|
27
|
+
// class; an ambiguous short name is absent) and the set of compiled FQCNs (the
|
|
28
|
+
// FQCN-written form). The same rule the transpiler applies
|
|
29
|
+
// (compiler/src/transpiler.mjs: the `new` case), so the graph and the
|
|
30
|
+
// compiled code agree on WHICH class the route instantiates.
|
|
31
|
+
//
|
|
32
|
+
// `newClasses` is the extract's list: { alias, fqcn } per `new` site, the
|
|
33
|
+
// alias being the name as written (a leading backslash stripped).
|
|
34
|
+
// `classesMeta` is the build's compiled-class metadata: { fqcns, aliases }.
|
|
35
|
+
export function resolveNewSites(newClasses, classesMeta) {
|
|
36
|
+
const fqcns = new Set(classesMeta && classesMeta.fqcns ? classesMeta.fqcns : []);
|
|
37
|
+
const aliases = (classesMeta && classesMeta.aliases) || {};
|
|
38
|
+
const out = [];
|
|
39
|
+
for (const entry of newClasses || []) {
|
|
40
|
+
const alias = String(entry.alias || '').replace(/^\\/, '');
|
|
41
|
+
if (!alias) continue;
|
|
42
|
+
// The short name of a compiled class (the alias map), else the class
|
|
43
|
+
// itself (the FQCN-written form). A name the compiler could not resolve
|
|
44
|
+
// (an unknown class) resolves to nothing here: the route was stopped.
|
|
45
|
+
const fqcn = aliases[alias] || (fqcns.has(alias) ? alias : null);
|
|
46
|
+
if (fqcn && !out.includes(fqcn)) out.push(fqcn);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The classes a route reaches, from the route's own `new` sites.
|
|
52
|
+
// `ctx` carries the class graph:
|
|
53
|
+
// classMeta - the compiled classes: { FQCN: { instantiated: [FQCN] } }
|
|
54
|
+
// dumpClasses - the dumped classes: { FQCN: { kind, ctor: [...] } }
|
|
55
|
+
// events - the dumped events: { FQCN: { listeners: [FQCN] } }
|
|
56
|
+
export function reachableClasses(newClasses, classesMeta, ctx) {
|
|
57
|
+
const seeds = resolveNewSites(newClasses, classesMeta);
|
|
58
|
+
const reached = new Set();
|
|
59
|
+
const queue = [...seeds];
|
|
60
|
+
while (queue.length) {
|
|
61
|
+
const fqcn = queue.shift();
|
|
62
|
+
if (!fqcn || reached.has(fqcn)) continue;
|
|
63
|
+
reached.add(fqcn);
|
|
64
|
+
for (const next of classEdges(fqcn, ctx)) {
|
|
65
|
+
if (next && !reached.has(next)) queue.push(next);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { reached, seeds };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The classes one class reaches: its constructor dependencies, the classes it
|
|
72
|
+
// instantiates, and (for an event class) its listeners.
|
|
73
|
+
function classEdges(fqcn, ctx) {
|
|
74
|
+
const edges = [];
|
|
75
|
+
// The constructor dependencies (the runtime builds them: rt.newClass for
|
|
76
|
+
// every class-typed dependency - the exact edge). The dump records the
|
|
77
|
+
// dependency's class (and the concrete binding of an interface).
|
|
78
|
+
const dumped = (ctx.dumpClasses && ctx.dumpClasses[fqcn]) || null;
|
|
79
|
+
for (const p of (dumped && dumped.ctor) || []) {
|
|
80
|
+
if (!p || p.kind !== 'class') continue;
|
|
81
|
+
edges.push(p.impl || p.type);
|
|
82
|
+
}
|
|
83
|
+
// The classes the class instantiates (`new X()` in its methods).
|
|
84
|
+
const meta = (ctx.classMeta && ctx.classMeta[fqcn]) || null;
|
|
85
|
+
for (const inst of (meta && meta.instantiated) || []) edges.push(inst);
|
|
86
|
+
// The listeners of an event class (the runtime dispatches the event to them).
|
|
87
|
+
const event = (ctx.events && ctx.events[fqcn]) || null;
|
|
88
|
+
for (const listener of (event && event.listeners) || []) edges.push(listener);
|
|
89
|
+
return edges;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The mailables among the reached classes: the app's mailable classes (the
|
|
93
|
+
// dump records their kind). Each mailable's views must be in the route's view
|
|
94
|
+
// map.
|
|
95
|
+
export function reachedMailables(reached, dumpClasses) {
|
|
96
|
+
const mailables = [];
|
|
97
|
+
for (const fqcn of reached) {
|
|
98
|
+
const info = (dumpClasses && dumpClasses[fqcn]) || null;
|
|
99
|
+
if (info && info.kind === 'mailable' && !mailables.includes(fqcn)) mailables.push(fqcn);
|
|
100
|
+
}
|
|
101
|
+
return mailables;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export default { resolveNewSites, reachableClasses, reachedMailables };
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
// compiler/src/policy.mjs — the policy compiler (build time).
|
|
2
|
+
//
|
|
3
|
+
// A Laravel policy is a class: `App\Policies\PostPolicy` with the ability
|
|
4
|
+
// methods (`viewAny`, `create`, `update`, ...) and an optional `before()` hook.
|
|
5
|
+
// The compiled rung compiles the policy methods into plain async functions and
|
|
6
|
+
// ships them in `dist/src/policies.generated.mjs`; the Gate check at request
|
|
7
|
+
// time calls them with Laravel's own rules (the guest rules, the before() hook,
|
|
8
|
+
// the missing-ability denial).
|
|
9
|
+
//
|
|
10
|
+
// The metadata comes from tools/policy-dump.php (the real Gate of the booted
|
|
11
|
+
// app): which policy answers for a model, which abilities it has, whether a
|
|
12
|
+
// guest may call them, and whether the app registers global Gate callbacks.
|
|
13
|
+
//
|
|
14
|
+
// Anything the compiled rung cannot answer exactly fails loud here — the
|
|
15
|
+
// policy is not compiled, and the routes that check it stay blocked.
|
|
16
|
+
import parser from 'php-parser';
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
import { transpileControllerMethod } from './transpiler.mjs';
|
|
19
|
+
|
|
20
|
+
const phpParser = new parser({ parser: { extractDoc: false }, ast: { withPositions: true } });
|
|
21
|
+
|
|
22
|
+
const vname = (name) => 'v_' + String(name).replace(/[^A-Za-z0-9_]/g, '_');
|
|
23
|
+
const shortName = (fqcn) => String(fqcn).split('\\').pop();
|
|
24
|
+
|
|
25
|
+
const nodeName = (n) => {
|
|
26
|
+
if (n == null) return '';
|
|
27
|
+
if (typeof n === 'string') return n;
|
|
28
|
+
if (typeof n.name === 'string') return n.name;
|
|
29
|
+
if (n.name && typeof n.name.name === 'string') return n.name.name;
|
|
30
|
+
return '';
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const normPath = (p) => String(p || '').replace(/\\/g, '/').toLowerCase();
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// the AST of the policy file
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
function parsePolicyFile(file) {
|
|
40
|
+
let source;
|
|
41
|
+
try {
|
|
42
|
+
source = readFileSync(file, 'utf8');
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return { error: 'the policy file could not be read: ' + (e.message || e) };
|
|
45
|
+
}
|
|
46
|
+
let ast;
|
|
47
|
+
try {
|
|
48
|
+
ast = phpParser.parseCode(source);
|
|
49
|
+
} catch (e) {
|
|
50
|
+
return { error: 'parse error: ' + (e.message || String(e)) };
|
|
51
|
+
}
|
|
52
|
+
if (ast && ast.errors && ast.errors.length) {
|
|
53
|
+
const first = ast.errors[0];
|
|
54
|
+
return { error: 'parse error: ' + (first.msg || first.message || JSON.stringify(first)) };
|
|
55
|
+
}
|
|
56
|
+
let classNode = null;
|
|
57
|
+
const walk = (node) => {
|
|
58
|
+
if (!node || typeof node !== 'object') return;
|
|
59
|
+
if (Array.isArray(node)) {
|
|
60
|
+
for (const n of node) walk(n);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (!classNode && node.kind === 'class') {
|
|
64
|
+
classNode = node;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
for (const k of Object.keys(node)) {
|
|
68
|
+
if (k === 'loc') continue;
|
|
69
|
+
walk(node[k]);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
walk(ast.children || []);
|
|
73
|
+
if (!classNode) return { error: 'the policy file has no class' };
|
|
74
|
+
return { classNode };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function findMethod(classNode, name) {
|
|
78
|
+
for (const n of classNode.body || []) {
|
|
79
|
+
if (n.kind === 'method' && nodeName(n.name) === name) return n;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// one policy method
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
// The parameters of a policy method: the user, then (for an ability) the
|
|
89
|
+
// subject; for `before()`: the user, the ability, then the subject.
|
|
90
|
+
function planParams(entry, methodMeta, isBefore, opts) {
|
|
91
|
+
const params = methodMeta.params || [];
|
|
92
|
+
const seeds = [];
|
|
93
|
+
params.forEach((p, i) => {
|
|
94
|
+
const isUser = i === 0;
|
|
95
|
+
const isSubject = isBefore ? i >= 2 : i === 1;
|
|
96
|
+
if (isSubject) {
|
|
97
|
+
const fqcn =
|
|
98
|
+
p.type && !p.builtin && opts.models.has(p.type) ? p.type : entry.model;
|
|
99
|
+
seeds.push({ var: vname(p.name), kind: 'model', alias: shortName(fqcn), fqcn });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (!isUser) return;
|
|
103
|
+
// the user: its declared type when it is a model the build knows, else the
|
|
104
|
+
// app's auth provider model (an untyped `$user` is still that user).
|
|
105
|
+
let fqcn = null;
|
|
106
|
+
if (p.type && !p.builtin && opts.models.has(p.type)) fqcn = p.type;
|
|
107
|
+
else if (!p.type && opts.userModel && opts.models.has(opts.userModel)) fqcn = opts.userModel;
|
|
108
|
+
if (fqcn) seeds.push({ var: vname(p.name), kind: 'model', alias: shortName(fqcn), fqcn });
|
|
109
|
+
});
|
|
110
|
+
return seeds;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function compileMethod(entry, methodMeta, isBefore, opts, classNode) {
|
|
114
|
+
const name = methodMeta.name;
|
|
115
|
+
const label = (entry.policy || 'the policy') + '::' + name + '()';
|
|
116
|
+
if (methodMeta.static) {
|
|
117
|
+
return { error: 'the policy method ' + label + ' is static — Laravel calls the policy as an instance method' };
|
|
118
|
+
}
|
|
119
|
+
if (methodMeta.abstract) {
|
|
120
|
+
return { error: 'the policy method ' + label + ' is abstract' };
|
|
121
|
+
}
|
|
122
|
+
if (methodMeta.file && entry.file && normPath(methodMeta.file) !== normPath(entry.file)) {
|
|
123
|
+
return {
|
|
124
|
+
error:
|
|
125
|
+
'the policy method ' + label + ' is declared in ' + methodMeta.file + ' (a trait or a base class) — the compiled rung compiles the policy file only',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (methodMeta.returnType && !methodMeta.returnBuiltin && methodMeta.returnType !== 'mixed') {
|
|
129
|
+
return {
|
|
130
|
+
error:
|
|
131
|
+
'the policy method ' + label + ' returns ' + methodMeta.returnType + ' — the compiled rung compiles the boolean policies only (an authorization Response carries a message the edge does not render)',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const params = methodMeta.params || [];
|
|
135
|
+
if (isBefore && params.length > 3) {
|
|
136
|
+
return { error: 'the policy before() may take the user, the ability and one subject — ' + params.length + ' parameters are declared' };
|
|
137
|
+
}
|
|
138
|
+
if (!isBefore && params.length > 2) {
|
|
139
|
+
return {
|
|
140
|
+
error:
|
|
141
|
+
'the policy method ' + label + ' may take the user and one subject — Laravel hands the policy the Gate arguments',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
for (const p of params) {
|
|
145
|
+
if (p.variadic) {
|
|
146
|
+
return { error: 'the policy method ' + label + ' has a variadic parameter — the compiled rung does not compile variadics' };
|
|
147
|
+
}
|
|
148
|
+
if (p.byRef) {
|
|
149
|
+
return { error: 'the policy method ' + label + ' has a by-reference parameter' };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const methodNode = findMethod(classNode, name);
|
|
153
|
+
if (!methodNode) {
|
|
154
|
+
return { error: 'the policy method ' + label + ' is not in the policy file' };
|
|
155
|
+
}
|
|
156
|
+
const seeds = planParams(entry, methodMeta, isBefore, opts);
|
|
157
|
+
const paramVars = new Set(params.map((p) => vname(p.name)));
|
|
158
|
+
const emitted = transpileControllerMethod((methodNode.body && methodNode.body.children) || [], {
|
|
159
|
+
models: opts.modelAliases,
|
|
160
|
+
modelNotCompiled: opts.modelNotCompiled,
|
|
161
|
+
modelClassFqcns: opts.modelClassFqcns,
|
|
162
|
+
views: new Set(),
|
|
163
|
+
indent: '',
|
|
164
|
+
paramVars,
|
|
165
|
+
requestVars: new Set(),
|
|
166
|
+
// The class constants the app declares (`Post::PUBLISHED`): dumped at
|
|
167
|
+
// build time, so a policy body that reads one compiles instead of
|
|
168
|
+
// stopping on an unavailable constant.
|
|
169
|
+
constants: opts.constants || {},
|
|
170
|
+
formRequests: new Map(),
|
|
171
|
+
modelVarSeeds: seeds,
|
|
172
|
+
});
|
|
173
|
+
if (emitted.errors.length) {
|
|
174
|
+
return { error: 'the policy method ' + label + ' did not compile: ' + emitted.errors[0].message };
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
fn: emitted.code,
|
|
178
|
+
params: params.map((p) => vname(p.name)),
|
|
179
|
+
body: emitted.code,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// the whole registry
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
export function compilePolicies(dump, opts = {}) {
|
|
188
|
+
const globals = (dump && dump.globals) || { before: 0, after: 0, abilities: [] };
|
|
189
|
+
const models = (dump && dump.models) || [];
|
|
190
|
+
const modelSet = opts.models instanceof Set ? opts.models : new Set(opts.models || []);
|
|
191
|
+
const modelClassFqcns = opts.modelClassFqcns || {};
|
|
192
|
+
// The class names the policy body may use (the aliases the app writes +
|
|
193
|
+
// the FQCNs): a model chain inside a policy (`Post::where(...)`) resolves
|
|
194
|
+
// against them.
|
|
195
|
+
const modelAliases =
|
|
196
|
+
opts.modelAliases instanceof Set ? opts.modelAliases : new Set(Object.keys(modelClassFqcns));
|
|
197
|
+
const options = {
|
|
198
|
+
models: modelSet,
|
|
199
|
+
modelSet,
|
|
200
|
+
modelAliases,
|
|
201
|
+
modelNotCompiled: opts.modelNotCompiled || new Map(),
|
|
202
|
+
modelClassFqcns,
|
|
203
|
+
userModel: opts.userModel || null,
|
|
204
|
+
// The dumped class constants, by alias and by FQCN.
|
|
205
|
+
constants: opts.constants || {},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const meta = {};
|
|
209
|
+
const entries = [];
|
|
210
|
+
const errors = [];
|
|
211
|
+
// The GLOBAL Gate::define() abilities, under a reserved key: a subject-less
|
|
212
|
+
// `Gate::allows('x')` resolves here (the policy map is per model).
|
|
213
|
+
meta.__global__ = {
|
|
214
|
+
definitions: Object.fromEntries(
|
|
215
|
+
Object.entries((globals && globals.definitions) || {}).filter(
|
|
216
|
+
([, d]) => d && d.kind === 'string' && d.callback
|
|
217
|
+
)
|
|
218
|
+
),
|
|
219
|
+
definedAbilities: Array.isArray(globals.abilities) ? globals.abilities : [],
|
|
220
|
+
globalBefore: Number(globals.before) || 0,
|
|
221
|
+
globalAfter: Number(globals.after) || 0,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
for (const entry of models) {
|
|
225
|
+
const modelMeta = {
|
|
226
|
+
policy: entry.policy || null,
|
|
227
|
+
error: entry.error || null,
|
|
228
|
+
hasMagicCall: !!entry.hasMagicCall,
|
|
229
|
+
globalBefore: Number(globals.before) || 0,
|
|
230
|
+
globalAfter: Number(globals.after) || 0,
|
|
231
|
+
definedAbilities: Array.isArray(globals.abilities) ? globals.abilities : [],
|
|
232
|
+
before: null,
|
|
233
|
+
methods: {},
|
|
234
|
+
compileError: null,
|
|
235
|
+
};
|
|
236
|
+
meta[entry.model] = modelMeta;
|
|
237
|
+
|
|
238
|
+
if (entry.error && entry.error !== 'no policy') {
|
|
239
|
+
// The policy could not be resolved (an exception, a missing class): the
|
|
240
|
+
// Gate check cannot be answered -> the route stays blocked.
|
|
241
|
+
errors.push({ model: entry.model, message: 'the policy for ' + entry.model + ' could not be read: ' + entry.error });
|
|
242
|
+
entries.push({ model: entry.model, policy: null, before: null, methods: {} });
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (!entry.policy) {
|
|
246
|
+
// No policy: Laravel denies every ability (the build fails loud on a
|
|
247
|
+
// global Gate::define() that would answer instead).
|
|
248
|
+
entries.push({ model: entry.model, policy: null, before: null, methods: {} });
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const parsed = parsePolicyFile(entry.file);
|
|
253
|
+
if (parsed.error) {
|
|
254
|
+
modelMeta.compileError = parsed.error;
|
|
255
|
+
errors.push({ model: entry.model, message: parsed.error });
|
|
256
|
+
entries.push({ model: entry.model, policy: entry.policy, before: null, methods: {} });
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const compiled = { before: null, methods: {} };
|
|
261
|
+
let failed = null;
|
|
262
|
+
|
|
263
|
+
if (entry.before) {
|
|
264
|
+
const r = compileMethod(entry, entry.before, true, options, parsed.classNode);
|
|
265
|
+
if (r.error) failed = r.error;
|
|
266
|
+
else {
|
|
267
|
+
compiled.before = { guest: !!entry.before.guest, fn: r.fn, params: r.params };
|
|
268
|
+
modelMeta.before = { guest: !!entry.before.guest, params: (entry.before.params || []).length };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!failed) {
|
|
273
|
+
for (const [name, methodMeta] of Object.entries(entry.methods || {})) {
|
|
274
|
+
const r = compileMethod(entry, methodMeta, false, options, parsed.classNode);
|
|
275
|
+
if (r.error) {
|
|
276
|
+
failed = r.error;
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
compiled.methods[name] = { guest: !!methodMeta.guest, fn: r.fn, params: r.params };
|
|
280
|
+
modelMeta.methods[name] = {
|
|
281
|
+
guest: !!methodMeta.guest,
|
|
282
|
+
params: (methodMeta.params || []).map((p) => ({ name: p.name, type: p.type })),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (failed) {
|
|
288
|
+
modelMeta.compileError = failed;
|
|
289
|
+
errors.push({ model: entry.model, message: failed });
|
|
290
|
+
entries.push({ model: entry.model, policy: entry.policy, before: null, methods: {} });
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
entries.push({ model: entry.model, policy: entry.policy, before: compiled.before, methods: compiled.methods });
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return { code: emitRegistry(entries, meta.__global__ ? meta.__global__.definitions : {}), errors, meta };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// The generated module: the compiled policies, keyed by the model FQCN.
|
|
301
|
+
function emitRegistry(entries, globals) {
|
|
302
|
+
const lines = [];
|
|
303
|
+
lines.push('// generated by laravel-workers — the compiled policies (do not edit)');
|
|
304
|
+
lines.push('// The Gate checks the edge answers: the abilities, the guest rules, before().');
|
|
305
|
+
lines.push('');
|
|
306
|
+
lines.push('export const policies = {');
|
|
307
|
+
for (const e of entries) {
|
|
308
|
+
lines.push(' ' + JSON.stringify(e.model) + ': {');
|
|
309
|
+
lines.push(' policy: ' + JSON.stringify(e.policy) + ',');
|
|
310
|
+
if (e.before) {
|
|
311
|
+
lines.push(' before: {');
|
|
312
|
+
lines.push(' guest: ' + (e.before.guest ? 'true' : 'false') + ',');
|
|
313
|
+
lines.push(' fn: async (' + ['rt'].concat(e.before.params).join(', ') + ') => {');
|
|
314
|
+
for (const l of String(e.before.fn || '').split('\n')) lines.push(l ? ' ' + l : l);
|
|
315
|
+
lines.push(' },');
|
|
316
|
+
lines.push(' },');
|
|
317
|
+
} else {
|
|
318
|
+
lines.push(' before: null,');
|
|
319
|
+
}
|
|
320
|
+
lines.push(' methods: {');
|
|
321
|
+
for (const [name, m] of Object.entries(e.methods)) {
|
|
322
|
+
lines.push(' ' + JSON.stringify(name) + ': {');
|
|
323
|
+
lines.push(' guest: ' + (m.guest ? 'true' : 'false') + ',');
|
|
324
|
+
lines.push(' fn: async (' + ['rt'].concat(m.params).join(', ') + ') => {');
|
|
325
|
+
for (const l of String(m.fn || '').split('\n')) lines.push(l ? ' ' + l : l);
|
|
326
|
+
lines.push(' },');
|
|
327
|
+
lines.push(' },');
|
|
328
|
+
}
|
|
329
|
+
lines.push(' },');
|
|
330
|
+
lines.push(' },');
|
|
331
|
+
}
|
|
332
|
+
lines.push('};');
|
|
333
|
+
lines.push('');
|
|
334
|
+
// The GLOBAL Gate::define() abilities (`Gate::allows('x')` with no subject):
|
|
335
|
+
// the kernel's `rt.gateGlobal` reads this map, and only the compilable ones
|
|
336
|
+
// (a 'Class@method' string callback) are here - a closure never reaches it.
|
|
337
|
+
const globalsMap = globals || {};
|
|
338
|
+
lines.push('export const gateDefinitions = ' + JSON.stringify(globalsMap, null, 2) + ';');
|
|
339
|
+
lines.push('');
|
|
340
|
+
lines.push('export default policies;');
|
|
341
|
+
lines.push('');
|
|
342
|
+
return lines.join('\n');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export default { compilePolicies };
|