@malloy-publisher/server 0.0.247 → 0.0.248

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/server.mjs CHANGED
@@ -246982,11 +246982,67 @@ var init_package_load_pool = __esm(() => {
246982
246982
  });
246983
246983
 
246984
246984
  // src/service/authorize.ts
246985
+ import { payloadOf, routeOf } from "@malloydata/malloy";
246986
+ function noteRoute(text) {
246987
+ return routeOf({ value: text.trimStart() });
246988
+ }
246989
+ function notePayload(text) {
246990
+ return payloadOf({ value: text.trimStart() }) ?? "";
246991
+ }
246992
+ function authorizeNoteContent(text) {
246993
+ return noteRoute(text) === AUTHORIZE_ROUTE ? notePayload(text) : undefined;
246994
+ }
246985
246995
  function assertNoCallerAuthorizeAnnotation(callerText) {
246986
246996
  if (!AUTHORIZE_ANNOTATION_ANYWHERE.test(callerText))
246987
246997
  return;
246988
246998
  throw new BadRequestError("An `authorize` annotation is not permitted in caller-submitted Malloy " + "text. Access gates are declared by the model author on the source; a " + "request cannot introduce, replace, or relax one. To validate a gate " + "you are authoring, save it to the package's model file and reload the " + "package — model load validates every `#(authorize)` annotation it " + "declares.");
246989
246999
  }
247000
+ function containsAuthorizeAnnotationTag(texts) {
247001
+ return texts.some((text) => authorizeNoteContent(text) !== undefined);
247002
+ }
247003
+ function collectAuthorizeNearMisses(texts) {
247004
+ const found = [];
247005
+ for (const text of texts) {
247006
+ const trimmed2 = text.trimStart();
247007
+ const route = noteRoute(trimmed2);
247008
+ if (route === AUTHORIZE_ROUTE)
247009
+ continue;
247010
+ const nearMiss = route === undefined ? MALFORMED_AUTHORIZE_ATTEMPT.test(trimmed2) : route === "" ? MOTLY_AUTHORIZE_PAYLOAD.test(notePayload(trimmed2)) : route.toLowerCase() === AUTHORIZE_ROUTE;
247011
+ if (!nearMiss)
247012
+ continue;
247013
+ found.push(trimmed2.split(/[\r\n]/, 1)[0]);
247014
+ }
247015
+ return found;
247016
+ }
247017
+ function assertNoAuthorizeNearMisses(found) {
247018
+ if (found.length === 0)
247019
+ return;
247020
+ const unique = [...new Set(found)];
247021
+ throw new ModelCompilationError({
247022
+ message: `These annotations are not \`authorize\` gates and nothing enforces ` + `them:
247023
+ ${unique.map((t) => ` - \`${t}\``).join(`
247024
+ `)}
247025
+ ` + `Malloy routes an annotation by its prefix, and only ` + `\`#(authorize)\` (or \`##(authorize)\`, or the block form ` + `\`#|(authorize)\`) reaches the authorize route — a space after the ` + `\`#\`, spaces inside the brackets, or anything trailing the closing ` + `bracket makes it a plain tag Malloy hands to something else. Write ` + `\`#(authorize) "<expression>"\` on the \`source:\` statement you mean ` + `to protect. This is refused rather than interpreted: guessing at the ` + `intent would let publisher start enforcing a filter on a package that ` + `has been serving every row.`
247026
+ });
247027
+ }
247028
+ function describeMisplacedAuthorizeAnnotation(f) {
247029
+ if (f.kind === "query")
247030
+ return `on query "${f.name}"`;
247031
+ if (f.kind === "file")
247032
+ return "at the file level (`##(authorize)`)";
247033
+ return `on field "${f.fieldName}" of source "${f.name}"`;
247034
+ }
247035
+ function assertNoMisplacedAuthorizeAnnotations(found) {
247036
+ if (found.length === 0)
247037
+ return;
247038
+ const positions = found.map((f) => ` - ${describeMisplacedAuthorizeAnnotation(f)}`).join(`
247039
+ `);
247040
+ throw new ModelCompilationError({
247041
+ message: `An \`#(authorize)\` annotation is never enforced at:
247042
+ ${positions}
247043
+ ` + `A gate only applies where model load looks for one — a \`source:\`'s ` + `own annotation, or one it inherits from an \`extend\`/query-source ` + `base. File-level \`##(authorize)\` is deprecated and no longer ` + `enforced anywhere, so it always lands here: declare \`#(authorize)\` ` + `on each \`source:\` it was meant to protect instead. Every other ` + `position above should move to the \`source:\` statement it is meant ` + `to protect.`
247044
+ });
247045
+ }
246990
247046
  function buildAuthorizeProbe(exprs, givenDecls = []) {
246991
247047
  const selects = exprs.map((expr, i) => `__auth_${i} is (${expr})`).join(`
246992
247048
  `);
@@ -247014,6 +247070,158 @@ function referencedGivenNames(expr) {
247014
247070
  }
247015
247071
  return names;
247016
247072
  }
247073
+ function classifyAuthorizeGate(condition, declaredTypes, declaredDefaults) {
247074
+ const fieldUsage = condition.refSummary?.fieldUsage;
247075
+ if (!Array.isArray(fieldUsage) || fieldUsage.length === 0) {
247076
+ return { shape: "given_only" };
247077
+ }
247078
+ const givenNames = [];
247079
+ const literalAtoms = [];
247080
+ let rejection;
247081
+ const reject = (cause, detail) => {
247082
+ rejection ??= { shape: "rejected", cause, detail };
247083
+ return false;
247084
+ };
247085
+ const givenOperand = (node) => {
247086
+ const n = asNode(node);
247087
+ return n && n.node === "given" && typeof n.refName === "string" ? n.refName : null;
247088
+ };
247089
+ const isLiteralOperand = (node) => {
247090
+ const n = asNode(node);
247091
+ return !!n && (n.node === "numberLiteral" || n.node === "stringLiteral" || n.node === "true" || n.node === "false");
247092
+ };
247093
+ const literalOperandText = (node) => {
247094
+ const n = asNode(node);
247095
+ if (!n)
247096
+ return null;
247097
+ if (n.node === "true" || n.node === "false")
247098
+ return n.node;
247099
+ if (n.node === "numberLiteral" && typeof n.literal === "string") {
247100
+ return n.literal;
247101
+ }
247102
+ if (n.node === "stringLiteral" && typeof n.literal === "string") {
247103
+ return `'${n.literal.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
247104
+ }
247105
+ return null;
247106
+ };
247107
+ const declaredTypeOf = (name) => {
247108
+ const declared = declaredTypes.get(name);
247109
+ if (declared === undefined) {
247110
+ reject("unreachable_given", `\`$${name}\` is not on this model's given surface, so the gate ` + `would bind its declaration default rather than the caller's ` + `value. Declare it here, importing it if it lives elsewhere ` + `(\`import { ${name} } from "…"\`)`);
247111
+ return null;
247112
+ }
247113
+ return declared;
247114
+ };
247115
+ const walk = (node, depth) => {
247116
+ if (depth > MAX_GATE_WALK_DEPTH) {
247117
+ return reject("unsupported_node", "the gate nests too deeply to read");
247118
+ }
247119
+ const n = asNode(node);
247120
+ if (!n || typeof n.node !== "string") {
247121
+ return reject("unsupported_node", "the gate has an unreadable shape");
247122
+ }
247123
+ const kind = n.node;
247124
+ if (BOOLEAN_NODES.has(kind)) {
247125
+ const kids = asNode(n.kids);
247126
+ if (!kids) {
247127
+ return reject("unsupported_node", `\`${kind}\` has no operands`);
247128
+ }
247129
+ return walk(kids.left, depth + 1) && walk(kids.right, depth + 1);
247130
+ }
247131
+ if (TRANSPARENT_NODES.has(kind)) {
247132
+ return walk(n.e, depth + 1);
247133
+ }
247134
+ if (kind === "inGiven") {
247135
+ if (n.not === true) {
247136
+ return reject("unsupported_node", "a negated membership test (`not in`) is not an access rule; " + "write the gate as the set of rows a caller MAY read");
247137
+ }
247138
+ const given = givenOperand(n.givenRef);
247139
+ if (given === null) {
247140
+ return reject("unsupported_node", "`in` must test membership of a declared given");
247141
+ }
247142
+ const declared = declaredTypeOf(given);
247143
+ if (declared === null)
247144
+ return false;
247145
+ if (!isArrayType(declared)) {
247146
+ return reject("scalar_given_rejects_in", `\`$${given}\` is declared \`${declared}\` (a scalar), so ` + `\`in $${given}\` is not a membership test. Compare it with ` + `\`=\` instead`);
247147
+ }
247148
+ givenNames.push(given);
247149
+ return walkFieldOperand(n.e);
247150
+ }
247151
+ if (SCALAR_COMPARISON_NODES.has(kind)) {
247152
+ const kids = asNode(n.kids);
247153
+ if (!kids) {
247154
+ return reject("unsupported_node", `\`${kind}\` has no operands`);
247155
+ }
247156
+ const left = givenOperand(kids.left);
247157
+ const right = givenOperand(kids.right);
247158
+ const given = left ?? right;
247159
+ if (given === null) {
247160
+ return reject("no_given_reference", `\`${kind}\` must compare a field against a given; a comparison ` + `against a constant is a fixed filter and belongs in the ` + `source's own \`where:\``);
247161
+ }
247162
+ const declared = declaredTypeOf(given);
247163
+ if (declared === null)
247164
+ return false;
247165
+ if (isArrayType(declared)) {
247166
+ return reject("array_given_needs_in", `\`$${given}\` is declared \`${declared}\` (an array), so ` + `comparing it with \`${kind}\` compiles and then fails in the ` + `warehouse. Write \`in $${given}\` — it is also the spelling ` + `that matches no rows when the array is empty`);
247167
+ }
247168
+ const otherSide = left === null ? kids.left : kids.right;
247169
+ if (isLiteralOperand(otherSide)) {
247170
+ givenNames.push(given);
247171
+ const literalText = literalOperandText(otherSide);
247172
+ if (literalText !== null) {
247173
+ literalAtoms.push(left !== null ? `$${given} ${kind} ${literalText}` : `${literalText} ${kind} $${given}`);
247174
+ }
247175
+ return true;
247176
+ }
247177
+ if (declaredDefaults.has(given)) {
247178
+ return reject("field_given_has_default", `\`${kind}\` compares row field data against \`$${given}\`, ` + `which is declared with a default (\`${declaredDefaults.get(given)}\`). ` + `A caller who supplies no value for \`$${given}\` gets that ` + `default, and the comparison then applies to every row — ` + `admitting rows it was meant to exclude. Declare \`$${given}\` ` + `with no default so a caller must supply one explicitly`);
247179
+ }
247180
+ givenNames.push(given);
247181
+ return walkFieldOperand(otherSide);
247182
+ }
247183
+ return reject("unsupported_node", `\`${kind}\` is not permitted in a gate; a row-level gate is a ` + `boolean combination of \`<field> <operator> $GIVEN\` comparisons`);
247184
+ };
247185
+ const walkFieldOperand = (node) => {
247186
+ const n = asNode(node);
247187
+ if (!n || n.node !== "field" || !Array.isArray(n.path)) {
247188
+ return reject("unsupported_node", `a gate compares a FIELD against a given; \`${asNode(node)?.node ?? "that operand"}\` is not a field reference`);
247189
+ }
247190
+ return true;
247191
+ };
247192
+ let ok;
247193
+ try {
247194
+ ok = walk(condition.e, 0);
247195
+ } catch {
247196
+ return {
247197
+ shape: "rejected",
247198
+ cause: "unsupported_node",
247199
+ detail: "the gate's compiled shape could not be read"
247200
+ };
247201
+ }
247202
+ if (!ok) {
247203
+ return rejection ?? {
247204
+ shape: "rejected",
247205
+ cause: "unsupported_node",
247206
+ detail: "the gate is not an allowed shape"
247207
+ };
247208
+ }
247209
+ if (givenNames.length === 0) {
247210
+ return {
247211
+ shape: "rejected",
247212
+ cause: "no_given_reference",
247213
+ detail: "a row-level gate must compare a field against a given; a gate " + "that references none is a fixed filter and belongs in the " + "source's own `where:`"
247214
+ };
247215
+ }
247216
+ return { shape: "row_level", givenNames, literalAtoms };
247217
+ }
247218
+ function isArrayType(declaredType) {
247219
+ const normalized = declaredType.trim().toLowerCase();
247220
+ return normalized === "array" || normalized.endsWith("[]");
247221
+ }
247222
+ function asNode(node) {
247223
+ return node !== null && typeof node === "object" ? node : null;
247224
+ }
247017
247225
  function inferGivenType(value) {
247018
247226
  if (typeof value === "string")
247019
247227
  return "string";
@@ -247056,9 +247264,8 @@ async function runProbe(executor, probeText, givens) {
247056
247264
  }
247057
247265
  async function evaluateAuthorize(executor, exprs, givens, declaredTypes, options) {
247058
247266
  const selfContainedFirst = options?.selfContainedFirst ?? false;
247059
- const ambientPrefix = options?.ambientPrefix ?? 0;
247060
- for (const [index, expr] of exprs.entries()) {
247061
- if (selfContainedFirst && index >= ambientPrefix) {
247267
+ for (const expr of exprs) {
247268
+ if (selfContainedFirst) {
247062
247269
  if (await evaluateSelfContainedFirst(executor, expr, givens, declaredTypes)) {
247063
247270
  return true;
247064
247271
  }
@@ -247106,27 +247313,123 @@ async function evaluateSelfContainedFirst(executor, expr, givens, declaredTypes)
247106
247313
  }
247107
247314
  }
247108
247315
  }
247109
- async function validateAuthorizeProbes(compiler, sources) {
247110
- for (const source of sources) {
247111
- const exprs = source.authorize;
247112
- if (!exprs || exprs.length === 0)
247113
- continue;
247316
+ function quoteMalloyIdentifier(name) {
247317
+ return "`" + name.replace(/\\/g, "\\\\").replace(/`/g, "\\`") + "`";
247318
+ }
247319
+ function buildRowLevelProbe(graftTarget, filterText) {
247320
+ return `run: ${quoteMalloyIdentifier(graftTarget)} extend { where: ${filterText} } -> { select: __authorize_probe is 1; limit: 1 }`;
247321
+ }
247322
+ function liftProbeFilterCondition(prepared, label, filterText) {
247323
+ const filterList = prepared._query?.structRef?.filterList;
247324
+ if (!Array.isArray(filterList) || filterList.length === 0) {
247325
+ throw new Error(`${label} carries no filter condition`);
247326
+ }
247327
+ const lifted = filterList[filterList.length - 1];
247328
+ if (lifted.code !== filterText) {
247329
+ throw new Error(`${label} carries the wrong condition — expected "${filterText}", got "${lifted.code ?? ""}"`);
247330
+ }
247331
+ if (!lifted.isSourceFilter) {
247332
+ throw new Error(`${label} carries a condition that is not a source filter`);
247333
+ }
247334
+ return lifted;
247335
+ }
247336
+ function gateFilterText(exprs) {
247337
+ return exprs.map((e) => `(${e})`).join(" or ");
247338
+ }
247339
+ async function liftRowLevelCondition(compiler, sourceName, exprs) {
247340
+ const filterText = gateFilterText(exprs);
247341
+ const prepared = await compiler.loadQuery(buildRowLevelProbe(sourceName, filterText)).getPreparedQuery();
247342
+ return liftProbeFilterCondition(prepared, `row-level probe for "${sourceName}"`, filterText);
247343
+ }
247344
+ async function runOneRowProbeOrThrow(compiler, sourceName, exprs) {
247345
+ try {
247346
+ await compiler.loadQuery(buildAuthorizeProbe(exprs)).getPreparedQuery();
247347
+ } catch (err) {
247348
+ const detail = err instanceof Error ? err.message : String(err);
247349
+ throw new ModelCompilationError({
247350
+ message: `Invalid #(authorize) annotation on source "${sourceName}" [${exprs.join(" | ")}]: ${detail}`
247351
+ });
247352
+ }
247353
+ }
247354
+ async function assertNoVacuousDefaultAtom(executor, sourceName, literalAtoms) {
247355
+ for (const atom of literalAtoms) {
247356
+ let vacuous;
247114
247357
  try {
247115
- await compiler.loadQuery(buildAuthorizeProbe(exprs)).getPreparedQuery();
247358
+ vacuous = await runProbe(executor, buildAuthorizeProbe([atom]), {});
247116
247359
  } catch (err) {
247117
247360
  const detail = err instanceof Error ? err.message : String(err);
247361
+ if (NO_DEFAULT_GIVEN_PATTERN.test(detail))
247362
+ continue;
247118
247363
  throw new ModelCompilationError({
247119
- message: `Invalid #(authorize) annotation on source "${source.name ?? "(unnamed)"}" [${exprs.join(" | ")}]: ${detail}`
247364
+ message: `Invalid #(authorize) annotation on source "${sourceName}": ` + `the atom \`${atom}\` could not be evaluated against its ` + `given's declared default (${detail}).`
247120
247365
  });
247121
247366
  }
247367
+ if (vacuous) {
247368
+ throw new ModelCompilationError({
247369
+ message: `Invalid #(authorize) annotation on source "${sourceName}": ` + `the atom \`${atom}\` evaluates to TRUE when a caller supplies ` + `no givens (its given's own declared default). An OR'd atom ` + `that is true by default makes the whole row filter admit ` + `every row for that caller. Give the given a default this ` + `atom evaluates false against, or declare it with no default ` + `so a caller must supply one explicitly.`
247370
+ });
247371
+ }
247372
+ }
247373
+ }
247374
+ async function validateAuthorizeProbes(compiler, options) {
247375
+ const declaredTypes = options.declaredTypes ?? new Map;
247376
+ const declaredDefaults = options.declaredDefaults ?? new Map;
247377
+ const provenNoteObjects = new Set;
247378
+ const ownNotesOf = options.authorizeOwnNotes ?? new Map;
247379
+ const pending = [];
247380
+ for (const [sourceName, groups] of options.authorizeMap ?? []) {
247381
+ for (const exprs of groups) {
247382
+ if (exprs.length === 0)
247383
+ continue;
247384
+ let condition;
247385
+ try {
247386
+ condition = await liftRowLevelCondition(compiler, sourceName, exprs);
247387
+ } catch (err) {
247388
+ pending.push({ sourceName, exprs, err });
247389
+ continue;
247390
+ }
247391
+ const classification = classifyAuthorizeGate(condition, declaredTypes, declaredDefaults);
247392
+ if (classification.shape === "given_only") {
247393
+ await runOneRowProbeOrThrow(compiler, sourceName, exprs);
247394
+ for (const note of ownNotesOf.get(sourceName) ?? []) {
247395
+ provenNoteObjects.add(note);
247396
+ }
247397
+ continue;
247398
+ }
247399
+ if (classification.shape === "rejected") {
247400
+ options.onRowLevelGateRejected?.(classification.cause);
247401
+ const ownNotes = ownNotesOf.get(sourceName) ?? [];
247402
+ if (ownNotes.length === 0) {
247403
+ options.onRowLevelGateUnexpressible?.(sourceName, classification.detail);
247404
+ continue;
247405
+ }
247406
+ throw new ModelCompilationError({
247407
+ message: `Invalid #(authorize) annotation on source "${sourceName}" ` + `[${exprs.join(" | ")}]: ${classification.detail}`
247408
+ });
247409
+ }
247410
+ await assertNoVacuousDefaultAtom(compiler, sourceName, classification.literalAtoms);
247411
+ for (const note of ownNotesOf.get(sourceName) ?? []) {
247412
+ provenNoteObjects.add(note);
247413
+ }
247414
+ }
247415
+ }
247416
+ for (const failure of pending) {
247417
+ const ownNotes = ownNotesOf.get(failure.sourceName) ?? [];
247418
+ const inherited = ownNotes.length === 0 || ownNotes.every((note) => provenNoteObjects.has(note));
247419
+ if (inherited) {
247420
+ const detail = failure.err instanceof Error ? failure.err.message : String(failure.err);
247421
+ options.onRowLevelGateRejected?.("entry_point_unexpressible");
247422
+ options.onRowLevelGateUnexpressible?.(failure.sourceName, detail);
247423
+ continue;
247424
+ }
247425
+ await runOneRowProbeOrThrow(compiler, failure.sourceName, failure.exprs);
247122
247426
  }
247123
247427
  }
247124
247428
  function parseAuthorizeAnnotation(annotation) {
247125
- const trimmed2 = annotation.trim();
247126
- const prefix = AUTHORIZE_ANNOTATION_PREFIX.exec(trimmed2);
247127
- if (!prefix)
247429
+ const content = authorizeNoteContent(annotation);
247430
+ if (content === undefined)
247128
247431
  return null;
247129
- return unwrapQuotedExpression(trimmed2.slice(prefix[0].length).trim());
247432
+ return unwrapQuotedExpression(content.trim());
247130
247433
  }
247131
247434
  function collectAuthorizeExprs(annotations) {
247132
247435
  const exprs = [];
@@ -247174,14 +247477,20 @@ function unwrapQuotedExpression(body) {
247174
247477
  }
247175
247478
  return expr;
247176
247479
  }
247177
- var AUTHORIZE_TAG, AUTHORIZE_ANNOTATION_ANYWHERE, AUTHORIZE_ANNOTATION_PREFIX, GIVEN_REF_PATTERN, STRING_LITERAL_PATTERN;
247480
+ var AUTHORIZE_ROUTE = "authorize", AUTHORIZE_TAG_LIKE, AUTHORIZE_ANNOTATION_ANYWHERE, MOTLY_AUTHORIZE_PAYLOAD, MALFORMED_AUTHORIZE_ATTEMPT, GIVEN_REF_PATTERN, STRING_LITERAL_PATTERN, everyMemberOf = () => (...members) => members, ROW_LEVEL_GATE_REJECTION_CAUSES, BOOLEAN_NODES, TRANSPARENT_NODES, SCALAR_COMPARISON_NODES, MAX_GATE_WALK_DEPTH = 64, NO_DEFAULT_GIVEN_PATTERN;
247178
247481
  var init_authorize = __esm(() => {
247179
247482
  init_errors();
247180
- AUTHORIZE_TAG = String.raw`##?\(\s*authorize\s*\)`;
247181
- AUTHORIZE_ANNOTATION_ANYWHERE = new RegExp(AUTHORIZE_TAG);
247182
- AUTHORIZE_ANNOTATION_PREFIX = new RegExp(`^${AUTHORIZE_TAG}`);
247483
+ AUTHORIZE_TAG_LIKE = String.raw`##?\|?[ \t]*[([{<]?[ \t]*authorize(?=[)\]}>]|[ \t]|$)`;
247484
+ AUTHORIZE_ANNOTATION_ANYWHERE = new RegExp(AUTHORIZE_TAG_LIKE, "iu");
247485
+ MOTLY_AUTHORIZE_PAYLOAD = /^[ \t]*[([{<][ \t]*authorize[ \t]*[)\]}>]/iu;
247486
+ MALFORMED_AUTHORIZE_ATTEMPT = /^##?\|?[ \t]*[([{<]?[ \t]*authorize/iu;
247183
247487
  GIVEN_REF_PATTERN = /\$([A-Za-z_][A-Za-z0-9_]*)/g;
247184
247488
  STRING_LITERAL_PATTERN = /'(?:\\.|[^'\\])*'/g;
247489
+ ROW_LEVEL_GATE_REJECTION_CAUSES = everyMemberOf()("array_given_needs_in", "scalar_given_rejects_in", "field_given_has_default", "unsupported_node", "no_given_reference", "unreachable_given", "entry_point_unexpressible");
247490
+ BOOLEAN_NODES = new Set(["and", "or"]);
247491
+ TRANSPARENT_NODES = new Set(["()"]);
247492
+ SCALAR_COMPARISON_NODES = new Set(["=", "!=", ">", ">=", "<", "<="]);
247493
+ NO_DEFAULT_GIVEN_PATTERN = /has no value and no default/;
247185
247494
  });
247186
247495
 
247187
247496
  // src/authorize_metrics.ts
@@ -247197,9 +247506,22 @@ function recordAuthorizeBypass(entryPoint) {
247197
247506
  });
247198
247507
  bypassCounter.add(1, { entry_point: entryPoint });
247199
247508
  }
247200
- var guardRejectionCounter = null, bypassCounter = null;
247509
+ function recordRowLevelGateDecision(decision) {
247510
+ rowLevelDecisionCounter ??= publisherMeter().createCounter("publisher_authorize_row_level_total", {
247511
+ description: "How a row-level `#(authorize)` gate resolved a request. Label: decision ('denied_by_gate'|'empty_after_filter'). 'denied_by_gate' is the fail-closed refusal when the gate could not be applied; 'empty_after_filter' is a successful response with zero rows after the filter matched none, which is NOT an error."
247512
+ });
247513
+ rowLevelDecisionCounter.add(1, { decision });
247514
+ }
247515
+ function recordRowLevelGateRejected(cause) {
247516
+ rowLevelRejectionCounter ??= publisherMeter().createCounter("publisher_authorize_row_level_rejected_total", {
247517
+ description: "Row-level `#(authorize)` gates refused at package load because their compiled condition is not an allowed shape, or an inherited gate that could not be expressed at one derived entry point. Label: cause (" + ROW_LEVEL_GATE_REJECTION_CAUSES.map((c) => `'${c}'`).join("|") + "). All but 'entry_point_unexpressible' fail the whole model load; that one fires at load without failing it — see the doc above. Alert on any nonzero value since the last publish, not on a rate."
247518
+ });
247519
+ rowLevelRejectionCounter.add(1, { cause });
247520
+ }
247521
+ var guardRejectionCounter = null, bypassCounter = null, rowLevelDecisionCounter = null, rowLevelRejectionCounter = null;
247201
247522
  var init_authorize_metrics = __esm(() => {
247202
247523
  init_telemetry();
247524
+ init_authorize();
247203
247525
  });
247204
247526
 
247205
247527
  // src/materialization_metrics.ts
@@ -247800,7 +248122,10 @@ function annotationTexts(annote) {
247800
248122
  return texts.length > 0 ? texts : undefined;
247801
248123
  }
247802
248124
  function ownLevelNoteTexts(annote) {
247803
- return [...annote?.blockNotes ?? [], ...annote?.notes ?? []].map((note) => note.text);
248125
+ return ownLevelNotes(annote).map((note) => note.text);
248126
+ }
248127
+ function ownLevelNotes(annote) {
248128
+ return [...annote?.blockNotes ?? [], ...annote?.notes ?? []];
247804
248129
  }
247805
248130
  var init_annotations = () => {};
247806
248131
 
@@ -247995,6 +248320,18 @@ function assertMaterializationEligible(persistSource) {
247995
248320
  });
247996
248321
  }
247997
248322
  }
248323
+ function assertColocatedPersistNotAuthorizeGated(persistSource, sourceName = persistSource.name, origin2 = "persist") {
248324
+ if (referencesAuthorize(persistSource)) {
248325
+ recordEligibilityRefused("authorize");
248326
+ const gated = origin2 === "preaggregate" ? `the source '${sourceName}' rolls up is protected by an ` + `#(authorize) gate (its own or a joined source's)` : `it is protected by an #(authorize) gate (its own or a joined ` + `source's)`;
248327
+ const remedy = origin2 === "preaggregate" ? `Remove the '#@ preaggregate' annotation from the gated source's ` + `measure(s), or move the gate to a source that is not ` + `pre-aggregated.` : `Drop '#@ persist' from this source, or move the gate to a source ` + `that is not materialized.`;
248328
+ const what = origin2 === "preaggregate" ? `Pre-aggregation rollup '${sourceName}' cannot be built` : `Source '${sourceName}' cannot be materialized (colocated ` + `'#@ persist')`;
248329
+ const alsoRollup = origin2 === "preaggregate" ? ` A rollup also groups ACROSS the gated column, so it could not ` + `be row-filtered afterwards even in principle.` : "";
248330
+ throw new MaterializationEligibilityError({
248331
+ message: `${what}: ${gated}. An authorize expression is evaluated per ` + `request; a materialized-once table served frozen carries no gate, ` + `so it would be served to everyone, bypassing authorization.` + `${alsoRollup} This is refused for safety. ${remedy}`
248332
+ });
248333
+ }
248334
+ }
247998
248335
  function unboundParameterNames(persistSource) {
247999
248336
  const def = persistSource._sourceDef;
248000
248337
  if (def === null || typeof def !== "object") {
@@ -249248,16 +249585,182 @@ var init_preaggregation_validation = __esm(() => {
249248
249585
  init_preaggregation_classifier();
249249
249586
  });
249250
249587
 
249588
+ // src/service/gate_registry_walk.ts
249589
+ import { isSourceDef } from "@malloydata/malloy";
249590
+ function resolveDeclaredSource(struct, modelDef) {
249591
+ if (!modelDef)
249592
+ return { kind: "none" };
249593
+ let sawBrokenEntry = false;
249594
+ for (const id of [struct.referenceID, struct.sourceID]) {
249595
+ const entry = id ? modelDef.sourceRegistry?.[id]?.entry : undefined;
249596
+ if (!entry)
249597
+ continue;
249598
+ const declared = entry.type === "source_registry_reference" ? modelDef.contents[entry.name] : entry;
249599
+ if (declared === struct)
249600
+ continue;
249601
+ if (!declared || !isSourceDef(declared)) {
249602
+ sawBrokenEntry = true;
249603
+ continue;
249604
+ }
249605
+ return { kind: "resolved", source: declared };
249606
+ }
249607
+ return sawBrokenEntry ? { kind: "unresolvable" } : { kind: "none" };
249608
+ }
249609
+ function ancestorGateExprs(struct, modelDef, seen = new Set) {
249610
+ let inherited = struct.annotations?.inherits;
249611
+ for (let depth = 0;inherited && depth < ANCESTOR_WALK_MAX_DEPTH; depth++) {
249612
+ const exprs2 = collectAuthorizeExprs(ownLevelNoteTexts(inherited));
249613
+ if (exprs2.length > 0)
249614
+ return exprs2;
249615
+ inherited = inherited.inherits;
249616
+ }
249617
+ if (inherited)
249618
+ return ["false"];
249619
+ seen.add(struct);
249620
+ if (seen.size > ANCESTOR_WALK_MAX_DEPTH)
249621
+ return ["false"];
249622
+ const declared = resolveDeclaredSource(struct, modelDef);
249623
+ if (declared.kind === "unresolvable")
249624
+ return ["false"];
249625
+ if (declared.kind === "none" || seen.has(declared.source))
249626
+ return [];
249627
+ const exprs = collectAuthorizeExprs(ownLevelNoteTexts(declared.source.annotations));
249628
+ return exprs.length > 0 ? exprs : ancestorGateExprs(declared.source, modelDef, seen);
249629
+ }
249630
+ function resolveQuerySourceBase(struct, modelDef) {
249631
+ const duck = struct;
249632
+ if (duck.type !== "query_source")
249633
+ return;
249634
+ const ref = duck.query?.structRef;
249635
+ const base = typeof ref === "string" ? modelDef?.contents[ref] : ref;
249636
+ return base && isSourceDef(base) ? base : undefined;
249637
+ }
249638
+ function resolveCompositeResolvedBase(struct) {
249639
+ const duck = struct;
249640
+ return duck.type === "query_source" ? duck.query?.compositeResolvedSourceDef : undefined;
249641
+ }
249642
+ function effectiveAncestorGateExprs(struct, modelDef, seen = new Set) {
249643
+ const direct = ancestorGateExprs(struct, modelDef, new Set(seen));
249644
+ if (direct.length > 0)
249645
+ return [direct];
249646
+ if (seen.has(struct))
249647
+ return [];
249648
+ seen.add(struct);
249649
+ const groups = [];
249650
+ const base = resolveQuerySourceBase(struct, modelDef);
249651
+ if (!base) {
249652
+ const duck = struct;
249653
+ if (duck.type === "query_source")
249654
+ groups.push(["false"]);
249655
+ } else if (!seen.has(base)) {
249656
+ const ownExprs = collectAuthorizeExprs(ownLevelNoteTexts(base.annotations));
249657
+ groups.push(...ownExprs.length > 0 ? [ownExprs] : effectiveAncestorGateExprs(base, modelDef, seen));
249658
+ }
249659
+ const composite = resolveCompositeResolvedBase(struct);
249660
+ if (composite && !seen.has(composite)) {
249661
+ const parentOwnNotes = base ? ownLevelNotes(base.annotations) : [];
249662
+ const compositeOwnNotes = ownLevelNotes(composite.annotations).filter((note) => !parentOwnNotes.includes(note));
249663
+ const compositeOwn = collectAuthorizeExprs(compositeOwnNotes.map((note) => note.text));
249664
+ groups.push(...compositeOwn.length > 0 ? [compositeOwn] : effectiveAncestorGateExprs(composite, modelDef, seen));
249665
+ }
249666
+ return groups;
249667
+ }
249668
+ function derivedStructsReachable(roots, modelDef) {
249669
+ const seen = new Set(roots);
249670
+ const found = [];
249671
+ const worklist = [...roots];
249672
+ for (let i = 0;i < worklist.length; i++) {
249673
+ const struct = worklist[i];
249674
+ for (const next of [
249675
+ resolveQuerySourceBase(struct, modelDef),
249676
+ resolveCompositeResolvedBase(struct)
249677
+ ]) {
249678
+ if (!next || seen.has(next))
249679
+ continue;
249680
+ seen.add(next);
249681
+ found.push(next);
249682
+ worklist.push(next);
249683
+ }
249684
+ }
249685
+ return found;
249686
+ }
249687
+ var ANCESTOR_WALK_MAX_DEPTH = 32;
249688
+ var init_gate_registry_walk = __esm(() => {
249689
+ init_annotations();
249690
+ init_authorize();
249691
+ });
249692
+
249251
249693
  // src/service/source_extraction.ts
249252
249694
  import {
249253
- isSourceDef
249695
+ isJoined as isJoined2,
249696
+ isSourceDef as isSourceDef2
249254
249697
  } from "@malloydata/malloy";
249698
+ function joinFieldNamesUnresolvableDeclaration(field, modelDef) {
249699
+ const ids = [field.referenceID, field.sourceID].filter((id) => !!id);
249700
+ if (ids.length === 0)
249701
+ return false;
249702
+ for (const id of ids) {
249703
+ const entry = modelDef.sourceRegistry?.[id]?.entry;
249704
+ if (!entry)
249705
+ continue;
249706
+ const declared = entry.type === "source_registry_reference" ? modelDef.contents[entry.name] : entry;
249707
+ if (declared && isSourceDef2(declared))
249708
+ return false;
249709
+ }
249710
+ return true;
249711
+ }
249255
249712
  function extractSourcesFromModelDef(modelDef, givens, onParseError) {
249256
249713
  const filterMap = new Map;
249257
249714
  const authorizeMap = new Map;
249258
- const ownAuthorizeSources = [];
249259
- const fileLevelAuthorize = collectAuthorizeExprs((modelAnnotations(modelDef).notes ?? []).map((note) => note.text));
249260
- const sources = Object.values(modelDef.contents).filter((obj) => isSourceDef(obj)).map((sourceObj) => {
249715
+ const misplacedAuthorize = [];
249716
+ const gatedSourceOwnAuthorizeNotes = new Set;
249717
+ const nearMissAuthorize = [];
249718
+ const sweptStructs = [];
249719
+ for (const obj of Object.values(modelDef.contents)) {
249720
+ if (!isSourceDef2(obj))
249721
+ continue;
249722
+ const struct = obj;
249723
+ sweptStructs.push(obj);
249724
+ for (const note of ownLevelNotes(struct.annotations)) {
249725
+ if (containsAuthorizeAnnotationTag([note.text])) {
249726
+ gatedSourceOwnAuthorizeNotes.add(note);
249727
+ }
249728
+ }
249729
+ nearMissAuthorize.push(...collectAuthorizeNearMisses([
249730
+ ...ownLevelNotes(struct.annotations),
249731
+ ...struct.fields.flatMap((field) => ownLevelNotes(field.annotations))
249732
+ ].map((note) => note.text)));
249733
+ }
249734
+ for (const value of Object.values(modelDef.sourceRegistry ?? {})) {
249735
+ const entry = value.entry;
249736
+ if (entry.type === "source_registry_reference")
249737
+ continue;
249738
+ if (!isSourceDef2(entry))
249739
+ continue;
249740
+ sweptStructs.push(entry);
249741
+ for (const note of ownLevelNotes(entry.annotations)) {
249742
+ if (containsAuthorizeAnnotationTag([note.text])) {
249743
+ gatedSourceOwnAuthorizeNotes.add(note);
249744
+ }
249745
+ }
249746
+ nearMissAuthorize.push(...collectAuthorizeNearMisses(ownLevelNotes(entry.annotations).map((note) => note.text)));
249747
+ }
249748
+ for (const struct of derivedStructsReachable(sweptStructs, modelDef)) {
249749
+ nearMissAuthorize.push(...collectAuthorizeNearMisses([
249750
+ ...ownLevelNotes(struct.annotations),
249751
+ ...struct.fields.flatMap((field) => ownLevelNotes(field.annotations))
249752
+ ].map((note) => note.text)));
249753
+ }
249754
+ {
249755
+ const folded = modelAnnotations(modelDef);
249756
+ nearMissAuthorize.push(...collectAuthorizeNearMisses([...folded.notes ?? [], ...folded.blockNotes ?? []].map((note) => note.text)));
249757
+ }
249758
+ assertNoAuthorizeNearMisses(nearMissAuthorize);
249759
+ const authorizeOwnNotes = new Map;
249760
+ if (containsAuthorizeAnnotationTag((modelAnnotations(modelDef).notes ?? []).map((note) => note.text))) {
249761
+ misplacedAuthorize.push({ kind: "file" });
249762
+ }
249763
+ const sources = Object.values(modelDef.contents).filter((obj) => isSourceDef2(obj)).map((sourceObj) => {
249261
249764
  const struct = sourceObj;
249262
249765
  const sourceName = struct.as || struct.name;
249263
249766
  const annotations = annotationTexts(struct.annotations);
@@ -249295,36 +249798,44 @@ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
249295
249798
  }
249296
249799
  const ownNotes = ownLevelNoteTexts(struct.annotations);
249297
249800
  const ownGates = collectAuthorizeExprs(ownNotes);
249298
- let inheritedGates = [];
249299
- if (ownGates.length === 0) {
249300
- for (let cur2 = struct.annotations?.inherits;cur2; cur2 = cur2.inherits) {
249301
- const exprs = collectAuthorizeExprs(ownLevelNoteTexts(cur2));
249302
- if (exprs.length > 0) {
249303
- inheritedGates = exprs;
249304
- break;
249305
- }
249306
- }
249307
- }
249308
- const effective = [
249309
- ...fileLevelAuthorize,
249310
- ...ownGates.length > 0 ? ownGates : inheritedGates
249311
- ];
249801
+ authorizeOwnNotes.set(sourceName, ownLevelNotes(struct.annotations).filter((note) => containsAuthorizeAnnotationTag([note.text])));
249802
+ const inheritedGroups = ownGates.length === 0 ? effectiveAncestorGateExprs(struct, modelDef) : [];
249803
+ const effectiveGroups = ownGates.length > 0 ? [ownGates] : inheritedGroups;
249312
249804
  let authorize;
249313
- if (effective.length > 0) {
249314
- authorizeMap.set(sourceName, effective);
249315
- authorize = effective;
249316
- }
249317
- const ownEffective = [...fileLevelAuthorize, ...ownGates];
249318
- if (ownEffective.length > 0) {
249319
- ownAuthorizeSources.push({
249320
- name: sourceName,
249321
- authorize: ownEffective
249322
- });
249805
+ if (effectiveGroups.length > 0) {
249806
+ authorizeMap.set(sourceName, effectiveGroups);
249807
+ authorize = effectiveGroups.flat();
249323
249808
  }
249324
249809
  const views = struct.fields.filter((field) => field.type === "turtle").filter((turtle) => turtle.pipeline.map((stage) => stage.type).every((type) => type === "reduce")).map((turtle) => ({
249325
249810
  name: turtle.as || turtle.name,
249326
249811
  annotations: annotationTexts(turtle.annotations)
249327
249812
  }));
249813
+ for (const field of struct.fields) {
249814
+ const fieldAuthorizeNotes = ownLevelNotes(field.annotations).filter((note) => containsAuthorizeAnnotationTag([note.text]));
249815
+ if (fieldAuthorizeNotes.length === 0)
249816
+ continue;
249817
+ if (fieldAuthorizeNotes.every((note) => gatedSourceOwnAuthorizeNotes.has(note))) {
249818
+ continue;
249819
+ }
249820
+ const fieldName2 = field.as || field.name;
249821
+ if (isJoined2(field) && isSourceDef2(field)) {
249822
+ const joinedStruct = field;
249823
+ if (joinFieldNamesUnresolvableDeclaration(joinedStruct, modelDef)) {
249824
+ continue;
249825
+ }
249826
+ misplacedAuthorize.push({
249827
+ kind: "field",
249828
+ name: sourceName,
249829
+ fieldName: fieldName2
249830
+ });
249831
+ continue;
249832
+ }
249833
+ misplacedAuthorize.push({
249834
+ kind: "field",
249835
+ name: sourceName,
249836
+ fieldName: fieldName2
249837
+ });
249838
+ }
249328
249839
  return {
249329
249840
  name: sourceName,
249330
249841
  annotations,
@@ -249334,20 +249845,33 @@ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
249334
249845
  authorize
249335
249846
  };
249336
249847
  });
249337
- return { sources, filterMap, authorizeMap, ownAuthorizeSources };
249848
+ return {
249849
+ sources,
249850
+ filterMap,
249851
+ authorizeMap,
249852
+ misplacedAuthorize,
249853
+ authorizeOwnNotes
249854
+ };
249338
249855
  }
249339
249856
  function extractQueriesFromModelDef(modelDef) {
249340
249857
  const isNamedQuery = (obj) => obj.type === "query";
249341
- return Object.values(modelDef.contents).filter(isNamedQuery).map((queryObj) => ({
249858
+ const namedQueries = Object.values(modelDef.contents).filter(isNamedQuery);
249859
+ const misplacedAuthorize = namedQueries.filter((queryObj) => containsAuthorizeAnnotationTag(ownLevelNoteTexts(queryObj.annotations))).map((queryObj) => ({
249860
+ kind: "query",
249861
+ name: queryObj.as || queryObj.name
249862
+ }));
249863
+ const queries = namedQueries.map((queryObj) => ({
249342
249864
  name: queryObj.as || queryObj.name,
249343
249865
  sourceName: typeof queryObj.structRef === "string" ? queryObj.structRef : undefined,
249344
249866
  annotations: annotationTexts(queryObj.annotations)
249345
249867
  }));
249868
+ return { queries, misplacedAuthorize };
249346
249869
  }
249347
249870
  var init_source_extraction = __esm(() => {
249348
249871
  init_annotations();
249349
249872
  init_authorize();
249350
249873
  init_filter();
249874
+ init_gate_registry_walk();
249351
249875
  });
249352
249876
 
249353
249877
  // src/service/model.ts
@@ -249355,7 +249879,7 @@ import {
249355
249879
  API,
249356
249880
  FixedConnectionMap,
249357
249881
  InMemoryURLReader as InMemoryURLReader2,
249358
- isSourceDef as isSourceDef2,
249882
+ isSourceDef as isSourceDef3,
249359
249883
  MalloyConfig as MalloyConfig2,
249360
249884
  MalloyError as MalloyError2,
249361
249885
  Annotations as Annotations4,
@@ -249371,7 +249895,7 @@ import { readFileSync } from "fs";
249371
249895
  import { createRequire as createRequire2 } from "module";
249372
249896
  import * as path8 from "path";
249373
249897
  import { fileURLToPath as fileURLToPath5 } from "url";
249374
- function quoteMalloyIdentifier(name) {
249898
+ function quoteMalloyIdentifier2(name) {
249375
249899
  return "`" + (name ?? "").replace(/\\/g, "\\\\").replace(/`/g, "\\`") + "`";
249376
249900
  }
249377
249901
  function makeHydrationRuntime(malloyConfig, buildManifest) {
@@ -249414,6 +249938,7 @@ function hydrateNotebookCells(runtime, notebookCells) {
249414
249938
  text: sc.text,
249415
249939
  runnable,
249416
249940
  modelMaterializer,
249941
+ modelDef: cellModelDef,
249417
249942
  newSources: sc.newSources,
249418
249943
  queryInfo: sc.queryInfo
249419
249944
  };
@@ -249428,7 +249953,7 @@ function hydrateMarkdownOnlyCells(notebookCells) {
249428
249953
  return { type: "code", text: sc.text };
249429
249954
  });
249430
249955
  }
249431
- var MALLOY_VERSION, ANCESTOR_WALK_MAX_DEPTH = 32, Model;
249956
+ var MALLOY_VERSION, Model;
249432
249957
  var init_model = __esm(() => {
249433
249958
  init_telemetry();
249434
249959
  init_materialization_metrics();
@@ -249451,6 +249976,7 @@ var init_model = __esm(() => {
249451
249976
  init_json_utils();
249452
249977
  init_query_metadata();
249453
249978
  init_preaggregation_validation();
249979
+ init_gate_registry_walk();
249454
249980
  init_source_extraction();
249455
249981
  init_authorize_metrics();
249456
249982
  MALLOY_VERSION = createRequire2(import.meta.url)("@malloydata/malloy/package.json").version;
@@ -249473,7 +249999,6 @@ var init_model = __esm(() => {
249473
249999
  compilationError;
249474
250000
  filterMap;
249475
250001
  givens;
249476
- fileLevelAuthorize = [];
249477
250002
  declaredQueryMetadataMemo;
249478
250003
  declaredSourceQueryMetadataMemo;
249479
250004
  authorizeReferencedGivenNames = new Set;
@@ -249482,6 +250007,11 @@ var init_model = __esm(() => {
249482
250007
  freshnessResolver;
249483
250008
  preaggregateEntityIdResolver;
249484
250009
  entryPointGatesBySource = new Map;
250010
+ gateRuntime;
250011
+ gateShapeCache = new Map;
250012
+ graftedMaterializerCache = new Map;
250013
+ static GRAFTED_MATERIALIZER_CACHE_MAX = 32;
250014
+ rowLevelFilteredRunnables = new WeakSet;
249485
250015
  meter = publisherMeter();
249486
250016
  queryExecutionHistogram = this.meter.createHistogram("malloy_model_query_duration", {
249487
250017
  description: "How long it takes to execute a Malloy model query",
@@ -249505,11 +250035,6 @@ var init_model = __esm(() => {
249505
250035
  this.compilationError = compilationError;
249506
250036
  this.filterMap = filterMap ?? new Map;
249507
250037
  this.givens = givens;
249508
- try {
249509
- this.fileLevelAuthorize = this.modelDef ? collectAuthorizeExprs((modelAnnotations(this.modelDef).notes ?? []).map((note) => note.text)) : [];
249510
- } catch {
249511
- this.fileLevelAuthorize = [];
249512
- }
249513
250038
  try {
249514
250039
  this.entryPointGatesBySource = this.computeEntryPointGatesBySource();
249515
250040
  } catch {
@@ -249539,8 +250064,64 @@ var init_model = __esm(() => {
249539
250064
  getFilters(sourceName) {
249540
250065
  return this.filterMap.get(sourceName) ?? [];
249541
250066
  }
250067
+ setGateRuntime(runtime) {
250068
+ this.gateRuntime = runtime;
250069
+ }
250070
+ defaultGraftScope() {
250071
+ if (!this.modelDef || !this.modelMaterializer)
250072
+ return;
250073
+ return {
250074
+ modelDef: this.modelDef,
250075
+ materializer: this.modelMaterializer,
250076
+ cacheScope: "model"
250077
+ };
250078
+ }
250079
+ graftScopeForCell(cellIndex) {
250080
+ const cells = this.runnableNotebookCells;
250081
+ if (!cells)
250082
+ return;
250083
+ for (let i = cellIndex - 1;i >= 0; i--) {
250084
+ const prior = cells[i];
250085
+ if (prior.type === "code" && prior.modelDef && prior.modelMaterializer) {
250086
+ return {
250087
+ modelDef: prior.modelDef,
250088
+ materializer: prior.modelMaterializer,
250089
+ cacheScope: `cell:${i}`
250090
+ };
250091
+ }
250092
+ }
250093
+ return;
250094
+ }
250095
+ selfGraftScopeForCell(cellIndex) {
250096
+ const cell = this.runnableNotebookCells?.[cellIndex];
250097
+ if (!cell?.modelDef || !cell.modelMaterializer)
250098
+ return;
250099
+ return {
250100
+ modelDef: cell.modelDef,
250101
+ materializer: cell.modelMaterializer,
250102
+ cacheScope: `cell-self:${cellIndex}`
250103
+ };
250104
+ }
250105
+ async resolveNotebookCellGraftScope(cellIndex, runnable) {
250106
+ const selfScope = this.selfGraftScopeForCell(cellIndex);
250107
+ const earlierScope = this.graftScopeForCell(cellIndex);
250108
+ if (!earlierScope)
250109
+ return { graftScope: selfScope, usesOwnScope: true };
250110
+ const { struct, modelDef } = await this.resolveRunTargetStruct(runnable);
250111
+ if (struct && modelDef && this.resolveGraftTarget(struct, modelDef, earlierScope.modelDef)) {
250112
+ return { graftScope: earlierScope, usesOwnScope: false };
250113
+ }
250114
+ return { graftScope: selfScope, usesOwnScope: true };
250115
+ }
250116
+ givenDeclaredTypesCache;
249542
250117
  givenDeclaredTypes() {
249543
- return new Map((this.givens ?? []).filter((g) => g.name != null && g.type != null).map((g) => [g.name, g.type]));
250118
+ this.givenDeclaredTypesCache ??= new Map((this.givens ?? []).filter((g) => g.name != null && g.type != null).map((g) => [g.name, g.type]));
250119
+ return this.givenDeclaredTypesCache;
250120
+ }
250121
+ givenDeclaredDefaultsCache;
250122
+ givenDeclaredDefaults() {
250123
+ this.givenDeclaredDefaultsCache ??= new Map((this.givens ?? []).filter((g) => g.name != null && g.default != null).map((g) => [g.name, g.default]));
250124
+ return this.givenDeclaredDefaultsCache;
249544
250125
  }
249545
250126
  getAuthorize(sourceName) {
249546
250127
  return this.sources?.find((source) => source.name === sourceName)?.authorize ?? [];
@@ -249565,7 +250146,6 @@ var init_model = __esm(() => {
249565
250146
  names.add(name);
249566
250147
  }
249567
250148
  };
249568
- addExprs(this.fileLevelAuthorize);
249569
250149
  for (const gates of this.entryPointGatesBySource.values()) {
249570
250150
  for (const { exprs } of gates)
249571
250151
  addExprs(exprs);
@@ -249578,7 +250158,7 @@ var init_model = __esm(() => {
249578
250158
  if (!modelDef)
249579
250159
  return byName;
249580
250160
  for (const entry of Object.values(modelDef.contents)) {
249581
- if (!isSourceDef2(entry))
250161
+ if (!isSourceDef3(entry))
249582
250162
  continue;
249583
250163
  const name = entry.as ?? entry.name;
249584
250164
  byName.set(name, this.collectEntryPointGates(entry, modelDef, new Set, true));
@@ -249586,27 +250166,68 @@ var init_model = __esm(() => {
249586
250166
  return byName;
249587
250167
  }
249588
250168
  hasAuthorize() {
249589
- return this.fileLevelAuthorize.length > 0 || (this.sources?.some((s) => (s.authorize?.length ?? 0) > 0) ?? false);
249590
- }
249591
- effectiveAuthorizeFor(sourceName) {
249592
- if (sourceName && this.sources?.some((s) => s.name === sourceName)) {
249593
- return this.getAuthorize(sourceName);
249594
- }
249595
- return this.fileLevelAuthorize;
250169
+ return this.sources?.some((s) => (s.authorize?.length ?? 0) > 0) ?? false;
250170
+ }
250171
+ anyAuthorizeNote;
250172
+ hasAnyAuthorizeNote() {
250173
+ if (this.anyAuthorizeNote !== undefined)
250174
+ return this.anyAuthorizeNote;
250175
+ this.anyAuthorizeNote = (() => {
250176
+ const modelDef = this.modelDef;
250177
+ if (!modelDef)
250178
+ return false;
250179
+ try {
250180
+ const structs = [];
250181
+ for (const obj of Object.values(modelDef.contents)) {
250182
+ if (isSourceDef3(obj))
250183
+ structs.push(obj);
250184
+ }
250185
+ for (const value of Object.values(modelDef.sourceRegistry ?? {})) {
250186
+ const entry = value.entry;
250187
+ if (entry.type === "source_registry_reference")
250188
+ continue;
250189
+ if (isSourceDef3(entry))
250190
+ structs.push(entry);
250191
+ }
250192
+ structs.push(...derivedStructsReachable(structs, modelDef));
250193
+ for (const struct of structs) {
250194
+ if (containsAuthorizeAnnotationTag(annotationTexts(struct.annotations) ?? [])) {
250195
+ return true;
250196
+ }
250197
+ for (const field of struct.fields) {
250198
+ if (containsAuthorizeAnnotationTag(annotationTexts(field.annotations) ?? [])) {
250199
+ return true;
250200
+ }
250201
+ }
250202
+ }
250203
+ return false;
250204
+ } catch {
250205
+ return true;
250206
+ }
250207
+ })();
250208
+ return this.anyAuthorizeNote;
249596
250209
  }
249597
- async assertAuthorized(sourceName, givens, bypassAuthorize = false) {
250210
+ async assertAuthorized(sourceName, givens, bypassAuthorize = false, graftScope = this.defaultGraftScope()) {
249598
250211
  if (bypassAuthorize) {
249599
250212
  this.noteAuthorizeBypass("source", sourceName);
249600
250213
  return;
249601
250214
  }
249602
250215
  const gates = sourceName ? this.entryPointGatesBySource.get(sourceName) : undefined;
249603
250216
  if (gates) {
249604
- for (const { label, exprs, selfContained, ambientPrefix } of gates) {
249605
- await this.assertAuthorizedExprs(label, exprs, givens, selfContained, ambientPrefix);
250217
+ for (const entry of gates) {
250218
+ const resolution = this.modelDef ? await this.resolveGateShape(entry, this.modelDef, graftScope) : { kind: "given_only" };
250219
+ if (resolution.kind === "row_level")
250220
+ continue;
250221
+ if (resolution.kind === "deny") {
250222
+ recordRowLevelGateDecision("denied_by_gate");
250223
+ if (resolution.cause)
250224
+ recordRowLevelGateRejected(resolution.cause);
250225
+ throw new AccessDeniedError(`Access denied for source "${entry.label}".`);
250226
+ }
250227
+ await this.assertAuthorizedExprs(entry.label, entry.exprs, givens, entry.selfContained);
249606
250228
  }
249607
250229
  return;
249608
250230
  }
249609
- await this.assertAuthorizedExprs(sourceName ?? "(query)", this.effectiveAuthorizeFor(sourceName), givens);
249610
250231
  }
249611
250232
  noteAuthorizeBypass(entryPoint, sourceName) {
249612
250233
  recordAuthorizeBypass(entryPoint);
@@ -249617,7 +250238,7 @@ var init_model = __esm(() => {
249617
250238
  packageName: this.packageName
249618
250239
  });
249619
250240
  }
249620
- async assertAuthorizedExprs(label, exprs, givens, selfContainedFirst = false, ambientPrefix = 0) {
250241
+ async assertAuthorizedExprs(label, exprs, givens, selfContainedFirst = false) {
249621
250242
  if (exprs.length === 0)
249622
250243
  return;
249623
250244
  const deny = () => {
@@ -249627,7 +250248,7 @@ var init_model = __esm(() => {
249627
250248
  deny();
249628
250249
  let passed = false;
249629
250250
  try {
249630
- passed = await evaluateAuthorize(this.modelMaterializer, exprs, givens, this.givenDeclaredTypes(), { selfContainedFirst, ambientPrefix });
250251
+ passed = await evaluateAuthorize(this.modelMaterializer, exprs, givens, this.givenDeclaredTypes(), { selfContainedFirst });
249631
250252
  } catch (err) {
249632
250253
  logger.debug("Authorize probe failed; denying", {
249633
250254
  sourceName: label,
@@ -249640,28 +250261,102 @@ var init_model = __esm(() => {
249640
250261
  deny();
249641
250262
  }
249642
250263
  async assertAuthorizedForAllSources(runnable, givens, bypassAuthorize = false) {
249643
- if (bypassAuthorize) {
249644
- this.noteAuthorizeBypass("runnable", await this.resolveAuthorizeSourceFromRunnable(runnable));
249645
- return;
249646
- }
249647
- const ownSourceName = await this.resolveAuthorizeSourceFromRunnable(runnable);
249648
- await this.assertAuthorized(ownSourceName, givens);
249649
- await this.assertAuthorizedFromCompiledRunnable(runnable, givens);
250264
+ await this.authorizeAndBindRunnable(runnable, givens, {
250265
+ bypassAuthorize
250266
+ });
249650
250267
  }
249651
- async assertAuthorizedFromCompiledRunnable(runnable, givens) {
250268
+ async collectAuthorizeEntryPointGates(runnable, givens, graftScope, skipOwnSourceGate = false) {
250269
+ const ownSourceName = await this.resolveAuthorizeSourceFromRunnable(runnable);
250270
+ if (!skipOwnSourceGate) {
250271
+ await this.assertAuthorized(ownSourceName, givens, false, graftScope);
250272
+ }
249652
250273
  const { struct, modelDef, compositeResolvedSourceDef } = await this.resolveRunTargetStruct(runnable);
249653
250274
  const seen = new Set;
249654
250275
  const entryPointGates = this.collectEntryPointGates(struct, modelDef, seen, true);
249655
250276
  if (compositeResolvedSourceDef && modelDef) {
249656
- entryPointGates.push(...this.collectEntryPointGates(compositeResolvedSourceDef, modelDef, seen, true));
250277
+ entryPointGates.push(...this.collectEntryPointGates(compositeResolvedSourceDef, modelDef, seen, true, undefined, struct ? ownLevelNotes(struct.annotations) : []));
249657
250278
  }
249658
- for (const {
249659
- label,
249660
- exprs,
249661
- selfContained,
249662
- ambientPrefix
249663
- } of entryPointGates) {
249664
- await this.assertAuthorizedExprs(label, exprs, givens, selfContained, ambientPrefix);
250279
+ return { entryPointGates, modelDef };
250280
+ }
250281
+ async assertAuthorizedFromCompiledRunnable(runnable, givens) {
250282
+ await this.authorizeAndBindRunnable(runnable, givens, {
250283
+ skipOwnSourceGate: true
250284
+ });
250285
+ }
250286
+ queryHadRowLevelFilterAttached(runnable) {
250287
+ return this.rowLevelFilteredRunnables.has(runnable);
250288
+ }
250289
+ async probeEntryPointGates(runnable, givens, graftScope, skipOwnSourceGate = false) {
250290
+ const { entryPointGates, modelDef } = await this.collectAuthorizeEntryPointGates(runnable, givens, graftScope, skipOwnSourceGate);
250291
+ const rowLevel = [];
250292
+ for (const entry of entryPointGates) {
250293
+ const resolution = modelDef ? await this.resolveGateShape(entry, modelDef, graftScope) : { kind: "given_only" };
250294
+ if (resolution.kind === "row_level") {
250295
+ rowLevel.push({
250296
+ label: entry.label,
250297
+ graftTarget: resolution.graftTarget,
250298
+ filterText: resolution.filterText,
250299
+ condition: resolution.condition
250300
+ });
250301
+ continue;
250302
+ }
250303
+ if (resolution.kind === "deny") {
250304
+ recordRowLevelGateDecision("denied_by_gate");
250305
+ if (resolution.cause)
250306
+ recordRowLevelGateRejected(resolution.cause);
250307
+ throw new AccessDeniedError(`Access denied for source "${entry.label}".`);
250308
+ }
250309
+ await this.assertAuthorizedExprs(entry.label, entry.exprs, givens, entry.selfContained);
250310
+ }
250311
+ return rowLevel;
250312
+ }
250313
+ async queryEntryPointHasRowLevelGate(runnable) {
250314
+ try {
250315
+ const { struct, modelDef, compositeResolvedSourceDef } = await this.resolveRunTargetStruct(runnable);
250316
+ if (!modelDef)
250317
+ return true;
250318
+ const seen = new Set;
250319
+ const gates = this.collectEntryPointGates(struct, modelDef, seen, true);
250320
+ if (compositeResolvedSourceDef) {
250321
+ gates.push(...this.collectEntryPointGates(compositeResolvedSourceDef, modelDef, seen, true, undefined, struct ? ownLevelNotes(struct.annotations) : []));
250322
+ }
250323
+ const graftScope = this.defaultGraftScope();
250324
+ for (const entry of gates) {
250325
+ const resolution = await this.resolveGateShape(entry, modelDef, graftScope);
250326
+ if (resolution.kind !== "given_only")
250327
+ return true;
250328
+ }
250329
+ return false;
250330
+ } catch {
250331
+ return true;
250332
+ }
250333
+ }
250334
+ async authorizeAndBindRunnable(runnable, givens, options) {
250335
+ if (options?.bypassAuthorize) {
250336
+ this.noteAuthorizeBypass("runnable", await this.resolveAuthorizeSourceFromRunnable(runnable));
250337
+ return runnable;
250338
+ }
250339
+ const graftScope = options?.graftScope ?? this.defaultGraftScope();
250340
+ const rowLevel = await this.probeEntryPointGates(runnable, givens, graftScope, options?.skipOwnSourceGate ?? false);
250341
+ if (rowLevel.length === 0)
250342
+ return runnable;
250343
+ if (!options?.recompile) {
250344
+ recordRowLevelGateDecision("denied_by_gate");
250345
+ throw new AccessDeniedError(`Access denied for source "${rowLevel[0].label}".`);
250346
+ }
250347
+ try {
250348
+ const graftedMaterializer = this.getOrBuildGraftedMaterializer(rowLevel, graftScope);
250349
+ const recompiled = options.recompile(graftedMaterializer);
250350
+ await this.assertGateLanded(recompiled, rowLevel);
250351
+ this.rowLevelFilteredRunnables.add(recompiled);
250352
+ return recompiled;
250353
+ } catch (err) {
250354
+ recordRowLevelGateDecision("denied_by_gate");
250355
+ logger.debug("Row-level authorize attach failed; denying", {
250356
+ modelPath: this.modelPath,
250357
+ error: err instanceof Error ? err.message : String(err)
250358
+ });
250359
+ throw new AccessDeniedError(`Access denied for source "${rowLevel[0].label}".`);
249665
250360
  }
249666
250361
  }
249667
250362
  async resolveRunTargetStruct(runnable) {
@@ -249689,107 +250384,233 @@ var init_model = __esm(() => {
249689
250384
  };
249690
250385
  }
249691
250386
  }
249692
- gateExprsForOwnAnnotations(struct, modelDef) {
249693
- const ownNotes = ownLevelNoteTexts(struct.annotations);
250387
+ gateExprsForOwnAnnotations(struct, modelDef, excludeNotes = []) {
250388
+ const ownNotes = ownLevelNotes(struct.annotations).filter((note) => !excludeNotes.includes(note));
249694
250389
  try {
249695
- const own = collectAuthorizeExprs(ownNotes);
250390
+ const own = collectAuthorizeExprs(ownNotes.map((note) => note.text));
249696
250391
  if (own.length > 0) {
249697
- return {
249698
- exprs: [...this.fileLevelAuthorize, ...own],
249699
- fromAncestor: false,
249700
- ambientPrefix: this.fileLevelAuthorize.length
249701
- };
250392
+ return { exprs: own, fromAncestor: false };
249702
250393
  }
249703
- const ancestor = this.ancestorGateExprs(struct, modelDef);
249704
- return {
249705
- exprs: [...this.fileLevelAuthorize, ...ancestor],
249706
- fromAncestor: ancestor.length > 0,
249707
- ambientPrefix: this.fileLevelAuthorize.length
249708
- };
250394
+ const ancestor = ancestorGateExprs(struct, modelDef);
250395
+ return { exprs: ancestor, fromAncestor: ancestor.length > 0 };
249709
250396
  } catch {
249710
- return { exprs: ["false"], fromAncestor: false, ambientPrefix: 0 };
250397
+ return { exprs: ["false"], fromAncestor: false };
249711
250398
  }
249712
250399
  }
249713
- ancestorGateExprs(struct, modelDef, seen = new Set) {
249714
- let inherited = struct.annotations?.inherits;
249715
- for (let depth = 0;inherited && depth < ANCESTOR_WALK_MAX_DEPTH; depth++) {
249716
- const exprs2 = collectAuthorizeExprs(ownLevelNoteTexts(inherited));
249717
- if (exprs2.length > 0)
249718
- return exprs2;
249719
- inherited = inherited.inherits;
249720
- }
249721
- if (inherited)
249722
- return ["false"];
249723
- seen.add(struct);
249724
- if (seen.size > ANCESTOR_WALK_MAX_DEPTH)
249725
- return ["false"];
249726
- const declared = this.resolveDeclaredSource(struct, modelDef);
249727
- if (declared.kind === "unresolvable")
249728
- return ["false"];
249729
- if (declared.kind === "none" || seen.has(declared.source))
249730
- return [];
249731
- const exprs = collectAuthorizeExprs(ownLevelNoteTexts(declared.source.annotations));
249732
- return exprs.length > 0 ? exprs : this.ancestorGateExprs(declared.source, modelDef, seen);
249733
- }
249734
- resolveDeclaredSource(struct, modelDef) {
249735
- if (!modelDef)
249736
- return { kind: "none" };
249737
- let sawBrokenEntry = false;
249738
- for (const id of [struct.referenceID, struct.sourceID]) {
249739
- const entry = id ? modelDef.sourceRegistry?.[id]?.entry : undefined;
249740
- if (!entry)
249741
- continue;
249742
- const declared = entry.type === "source_registry_reference" ? modelDef.contents[entry.name] : entry;
249743
- if (declared === struct)
249744
- continue;
249745
- if (!declared || !isSourceDef2(declared)) {
249746
- sawBrokenEntry = true;
249747
- continue;
249748
- }
249749
- return { kind: "resolved", source: declared };
249750
- }
249751
- return sawBrokenEntry ? { kind: "unresolvable" } : { kind: "none" };
249752
- }
249753
- collectEntryPointGates(struct, modelDef, seen = new Set, treatAsOwnGate = false) {
250400
+ collectEntryPointGates(struct, modelDef, seen = new Set, treatAsOwnGate = false, entryPointStruct = struct, excludeNotes = []) {
249754
250401
  if (!struct || !modelDef || seen.has(struct))
249755
250402
  return [];
249756
250403
  seen.add(struct);
249757
250404
  const results = [];
249758
250405
  const label = struct.as ?? struct.name;
249759
- const {
249760
- exprs: ownExprs,
249761
- fromAncestor,
249762
- ambientPrefix
249763
- } = this.gateExprsForOwnAnnotations(struct, modelDef);
250406
+ const { exprs: ownExprs, fromAncestor } = this.gateExprsForOwnAnnotations(struct, modelDef, excludeNotes);
249764
250407
  if (ownExprs.length > 0) {
249765
250408
  results.push({
249766
250409
  label,
249767
250410
  exprs: ownExprs,
249768
250411
  selfContained: fromAncestor || !treatAsOwnGate,
249769
- ambientPrefix
250412
+ struct: entryPointStruct
249770
250413
  });
249771
250414
  }
249772
250415
  const duck = struct;
249773
250416
  if (duck.type === "query_source") {
249774
- const ref = duck.query?.structRef;
249775
- const base = typeof ref === "string" ? modelDef.contents[ref] : ref;
249776
- if (base && isSourceDef2(base)) {
249777
- results.push(...this.collectEntryPointGates(base, modelDef, seen));
250417
+ const base = resolveQuerySourceBase(struct, modelDef);
250418
+ if (base) {
250419
+ results.push(...this.collectEntryPointGates(base, modelDef, seen, false, entryPointStruct));
249778
250420
  } else {
249779
250421
  results.push({
249780
250422
  label,
249781
250423
  exprs: ["false"],
249782
- selfContained: true,
249783
- ambientPrefix: 0
250424
+ selfContained: true
249784
250425
  });
249785
250426
  }
249786
250427
  const resolved = duck.query?.compositeResolvedSourceDef;
249787
250428
  if (resolved) {
249788
- results.push(...this.collectEntryPointGates(resolved, modelDef, seen));
250429
+ results.push(...this.collectEntryPointGates(resolved, modelDef, seen, false, entryPointStruct, base ? ownLevelNotes(base.annotations) : []));
249789
250430
  }
249790
250431
  }
249791
250432
  return results;
249792
250433
  }
250434
+ async resolveGateShape(entry, originModelDef, graftScope) {
250435
+ if (!entry.struct)
250436
+ return { kind: "given_only" };
250437
+ if (!graftScope) {
250438
+ logger.debug("Row-level gate has no graft scope to attach to (no scope was available at all, not the gate's own condition); denying", { modelPath: this.modelPath, label: entry.label });
250439
+ return { kind: "deny" };
250440
+ }
250441
+ const graftTarget = this.resolveGraftTarget(entry.struct, originModelDef, graftScope.modelDef);
250442
+ if (!graftTarget) {
250443
+ return this.classifyWithoutGraft(entry, graftScope.materializer);
250444
+ }
250445
+ const filterText = gateFilterText(entry.exprs);
250446
+ const cacheKey = `${graftScope.cacheScope}\x00${graftTarget}\x00${filterText}`;
250447
+ let cached2 = this.gateShapeCache.get(cacheKey);
250448
+ if (!cached2) {
250449
+ let condition;
250450
+ try {
250451
+ condition = await this.liftGateCondition(graftTarget, filterText, graftScope.materializer);
250452
+ } catch (err) {
250453
+ logger.debug("Row-level gate condition failed to lift; checking whether it is given-only before denying", {
250454
+ modelPath: this.modelPath,
250455
+ graftTarget,
250456
+ error: err instanceof Error ? err.message : String(err)
250457
+ });
250458
+ return this.classifyWithoutGraft(entry, graftScope.materializer);
250459
+ }
250460
+ const classification = classifyAuthorizeGate(condition, this.givenDeclaredTypes(), this.givenDeclaredDefaults());
250461
+ cached2 = { classification, condition };
250462
+ this.gateShapeCache.set(cacheKey, cached2);
250463
+ }
250464
+ if (cached2.classification.shape === "given_only") {
250465
+ return { kind: "given_only" };
250466
+ }
250467
+ if (cached2.classification.shape === "rejected") {
250468
+ return { kind: "deny", cause: cached2.classification.cause };
250469
+ }
250470
+ return {
250471
+ kind: "row_level",
250472
+ graftTarget,
250473
+ filterText,
250474
+ condition: cached2.condition
250475
+ };
250476
+ }
250477
+ async classifyWithoutGraft(entry, materializer) {
250478
+ try {
250479
+ await materializer.loadQuery(buildAuthorizeProbe(entry.exprs)).getPreparedQuery();
250480
+ return { kind: "given_only" };
250481
+ } catch (err) {
250482
+ logger.debug("Row-level gate has no attachable graft and does not compile as given-only; denying", {
250483
+ modelPath: this.modelPath,
250484
+ label: entry.label,
250485
+ error: err instanceof Error ? err.message : String(err)
250486
+ });
250487
+ return { kind: "deny" };
250488
+ }
250489
+ }
250490
+ resolveGraftTarget(struct, originModelDef, graftModelDef) {
250491
+ const direct = this.findContentsKey(struct, graftModelDef);
250492
+ if (direct)
250493
+ return direct;
250494
+ let current = struct;
250495
+ const seen = new Set([struct]);
250496
+ for (let depth = 0;depth < ANCESTOR_WALK_MAX_DEPTH; depth++) {
250497
+ const declared = resolveDeclaredSource(current, originModelDef);
250498
+ let next;
250499
+ if (declared.kind === "resolved" && !seen.has(declared.source)) {
250500
+ next = declared.source;
250501
+ } else if (declared.kind === "none") {
250502
+ next = this.findSourceByOwnAnnotationIdentity(current, graftModelDef, seen);
250503
+ }
250504
+ if (!next)
250505
+ return;
250506
+ const key = this.findContentsKey(next, graftModelDef);
250507
+ if (key)
250508
+ return key;
250509
+ seen.add(next);
250510
+ current = next;
250511
+ }
250512
+ return;
250513
+ }
250514
+ findContentsKey(struct, modelDef) {
250515
+ for (const [key, value] of Object.entries(modelDef.contents)) {
250516
+ if (value === struct)
250517
+ return key;
250518
+ }
250519
+ if (struct.sourceID) {
250520
+ for (const [key, value] of Object.entries(modelDef.contents)) {
250521
+ if (isSourceDef3(value) && value.sourceID === struct.sourceID) {
250522
+ return key;
250523
+ }
250524
+ }
250525
+ }
250526
+ return;
250527
+ }
250528
+ findSourceByOwnAnnotationIdentity(struct, modelDef, exclude) {
250529
+ const ownNotes = [
250530
+ ...struct.annotations?.blockNotes ?? [],
250531
+ ...struct.annotations?.notes ?? []
250532
+ ];
250533
+ if (ownNotes.length === 0)
250534
+ return;
250535
+ for (const value of Object.values(modelDef.contents)) {
250536
+ if (!isSourceDef3(value) || value === struct || exclude.has(value)) {
250537
+ continue;
250538
+ }
250539
+ const candidateNotes = [
250540
+ ...value.annotations?.blockNotes ?? [],
250541
+ ...value.annotations?.notes ?? []
250542
+ ];
250543
+ if (candidateNotes.some((note) => ownNotes.includes(note))) {
250544
+ return value;
250545
+ }
250546
+ }
250547
+ return;
250548
+ }
250549
+ async liftGateCondition(graftTarget, filterText, materializer) {
250550
+ const probe = materializer.loadQuery(buildRowLevelProbe(graftTarget, filterText));
250551
+ const prepared = await probe.getPreparedQuery();
250552
+ return liftProbeFilterCondition(prepared, `lifted probe for "${graftTarget}"`, filterText);
250553
+ }
250554
+ getOrBuildGraftedMaterializer(grafts, graftScope) {
250555
+ const key = `${graftScope.cacheScope}\x00` + grafts.map((g) => `${g.graftTarget}\x00${g.filterText}`).sort().join("\x01");
250556
+ const materializer = this.graftedMaterializerCache.get(key);
250557
+ if (materializer) {
250558
+ this.graftedMaterializerCache.delete(key);
250559
+ this.graftedMaterializerCache.set(key, materializer);
250560
+ return materializer;
250561
+ }
250562
+ const built = this.buildGraftedMaterializer(grafts, graftScope.modelDef);
250563
+ this.graftedMaterializerCache.set(key, built);
250564
+ while (this.graftedMaterializerCache.size > Model.GRAFTED_MATERIALIZER_CACHE_MAX) {
250565
+ const oldest = this.graftedMaterializerCache.keys().next();
250566
+ if (oldest.done)
250567
+ break;
250568
+ this.graftedMaterializerCache.delete(oldest.value);
250569
+ }
250570
+ return built;
250571
+ }
250572
+ buildGraftedMaterializer(grafts, modelDef) {
250573
+ if (!this.gateRuntime) {
250574
+ throw new Error("no retained runtime to graft a row-level gate through");
250575
+ }
250576
+ const copy = structuredClone(modelDef);
250577
+ for (const { graftTarget, condition } of grafts) {
250578
+ const target = copy.contents[graftTarget];
250579
+ if (!target || !isSourceDef3(target)) {
250580
+ throw new Error(`graft target "${graftTarget}" is not a source in this model`);
250581
+ }
250582
+ target.filterList = [...target.filterList ?? [], condition];
250583
+ }
250584
+ return this.gateRuntime._loadModelFromModelDef(copy);
250585
+ }
250586
+ async assertGateLanded(recompiled, grafts) {
250587
+ const prepared = await recompiled.getPreparedQuery();
250588
+ const modelDef = prepared._modelDef ?? this.modelDef;
250589
+ const structRef = prepared._query?.structRef;
250590
+ const resolvedRef = typeof structRef === "string" ? modelDef?.contents[structRef] : structRef;
250591
+ const struct = resolvedRef && typeof resolvedRef === "object" ? resolvedRef : undefined;
250592
+ for (const { condition } of grafts) {
250593
+ if (!condition.code || !this.filterListContainsCode(struct, modelDef, condition.code, 0)) {
250594
+ throw new Error("a row-level gate condition did not land on the recompiled query");
250595
+ }
250596
+ }
250597
+ }
250598
+ static MAX_GATE_PROOF_DEPTH = 8;
250599
+ filterListContainsCode(struct, modelDef, code, depth) {
250600
+ if (!struct || depth > Model.MAX_GATE_PROOF_DEPTH)
250601
+ return false;
250602
+ if (struct.filterList?.some((f) => f.code === code))
250603
+ return true;
250604
+ const duck = struct;
250605
+ if (duck.type === "query_source" && modelDef) {
250606
+ const ref = duck.query?.structRef;
250607
+ const base = typeof ref === "string" ? modelDef.contents[ref] : ref;
250608
+ if (base && isSourceDef3(base)) {
250609
+ return this.filterListContainsCode(base, modelDef, code, depth + 1);
250610
+ }
250611
+ }
250612
+ return false;
250613
+ }
249793
250614
  async assertAuthorizedForText(text, givens) {
249794
250615
  await this.assertAuthorized(extractRunTargetSourceName(text), givens);
249795
250616
  }
@@ -249842,8 +250663,22 @@ var init_model = __esm(() => {
249842
250663
  const sourceResult = Model.getSources(modelDef, givens);
249843
250664
  sources = sourceResult.sources;
249844
250665
  filterMap = sourceResult.filterMap;
249845
- queries = Model.getQueries(modelDef);
249846
- await validateAuthorizeProbes(modelMaterializer, sourceResult.ownAuthorizeSources);
250666
+ const queryResult = Model.getQueries(modelDef);
250667
+ queries = queryResult.queries;
250668
+ assertNoMisplacedAuthorizeAnnotations([
250669
+ ...sourceResult.misplacedAuthorize,
250670
+ ...queryResult.misplacedAuthorize
250671
+ ]);
250672
+ const declaredGivenTypes = new Map((givens ?? []).filter((g) => g.name != null && g.type != null).map((g) => [g.name, g.type]));
250673
+ const declaredGivenDefaults = new Map((givens ?? []).filter((g) => g.name != null && g.default != null).map((g) => [g.name, g.default]));
250674
+ await validateAuthorizeProbes(modelMaterializer, {
250675
+ authorizeMap: sourceResult.authorizeMap,
250676
+ declaredTypes: declaredGivenTypes,
250677
+ declaredDefaults: declaredGivenDefaults,
250678
+ authorizeOwnNotes: sourceResult.authorizeOwnNotes,
250679
+ onRowLevelGateRejected: recordRowLevelGateRejected,
250680
+ onRowLevelGateUnexpressible: (sourceName, detail) => logger.warn("Row-level #(authorize) gate not expressible at this entry point; every query against it will be denied", { packageName, modelPath, sourceName, detail })
250681
+ });
249847
250682
  const imports = modelDef.imports || [];
249848
250683
  const importedSourceNames = new Set;
249849
250684
  for (const importLocation of imports) {
@@ -249873,7 +250708,9 @@ var init_model = __esm(() => {
249873
250708
  }
249874
250709
  }
249875
250710
  }
249876
- return new Model(packageName, modelPath, dataStyles, modelType, modelMaterializer, modelDef, sources, queries, sourceInfos.length > 0 ? sourceInfos : undefined, runnableNotebookCells, undefined, filterMap, givens);
250711
+ const model = new Model(packageName, modelPath, dataStyles, modelType, modelMaterializer, modelDef, sources, queries, sourceInfos.length > 0 ? sourceInfos : undefined, runnableNotebookCells, undefined, filterMap, givens);
250712
+ model.setGateRuntime(runtime);
250713
+ return model;
249877
250714
  } catch (error) {
249878
250715
  let computedError = error;
249879
250716
  if (error instanceof Error && error.stack) {
@@ -249898,6 +250735,9 @@ var init_model = __esm(() => {
249898
250735
  const sourceInfos = data.sourceInfos;
249899
250736
  const givens = data.givens;
249900
250737
  const filterMap = data.filterMap ? new Map(data.filterMap) : undefined;
250738
+ for (const warning of data.authorizeWarnings ?? []) {
250739
+ logger.warn(warning, { packageName, modelPath: data.modelPath });
250740
+ }
249901
250741
  if (!modelDef) {
249902
250742
  return new Model(packageName, data.modelPath, dataStyles, data.modelType, undefined, undefined, sources, queries, sourceInfos, data.modelType === "notebook" ? hydrateMarkdownOnlyCells(data.notebookCells) : undefined, undefined, filterMap, givens, modelInfo);
249903
250743
  }
@@ -249905,6 +250745,7 @@ var init_model = __esm(() => {
249905
250745
  const modelMaterializer = runtime._loadModelFromModelDef(modelDef);
249906
250746
  const runnableNotebookCells = data.modelType === "notebook" ? hydrateNotebookCells(runtime, data.notebookCells) : undefined;
249907
250747
  const model = new Model(packageName, data.modelPath, dataStyles, data.modelType, modelMaterializer, modelDef, sources, queries, sourceInfos, runnableNotebookCells, undefined, filterMap, givens, modelInfo);
250748
+ model.setGateRuntime(runtime);
249908
250749
  return model;
249909
250750
  }
249910
250751
  static fromCompilationError(packageName, modelPath, modelType, error) {
@@ -250117,7 +250958,7 @@ var init_model = __esm(() => {
250117
250958
  }
250118
250959
  targets.push({
250119
250960
  label: query.name,
250120
- queryString: `run: ${quoteMalloyIdentifier(query.name)}`
250961
+ queryString: `run: ${quoteMalloyIdentifier2(query.name)}`
250121
250962
  });
250122
250963
  }
250123
250964
  for (const source of this.sources ?? []) {
@@ -250127,7 +250968,7 @@ var init_model = __esm(() => {
250127
250968
  }
250128
250969
  targets.push({
250129
250970
  label: `${source.name} -> ${view.name}`,
250130
- queryString: `run: ${quoteMalloyIdentifier(source.name)} -> ${quoteMalloyIdentifier(view.name)}`
250971
+ queryString: `run: ${quoteMalloyIdentifier2(source.name)} -> ${quoteMalloyIdentifier2(view.name)}`
250131
250972
  });
250132
250973
  }
250133
250974
  }
@@ -250307,6 +251148,7 @@ var init_model = __esm(() => {
250307
251148
  }
250308
251149
  let runnable;
250309
251150
  let liveRunnable;
251151
+ let queryString;
250310
251152
  let serveVirtualMap;
250311
251153
  let serveShapeBindings = [];
250312
251154
  let servedFrom;
@@ -250337,13 +251179,12 @@ var init_model = __esm(() => {
250337
251179
  throw err;
250338
251180
  }
250339
251181
  }
250340
- let queryString;
250341
251182
  if (!sourceName && !queryName && query) {
250342
251183
  queryString = `
250343
251184
  ` + query;
250344
251185
  } else if (queryName && !query) {
250345
251186
  queryString = `
250346
- run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMalloyIdentifier(queryName)}`;
251187
+ run: ${sourceName ? `${quoteMalloyIdentifier2(sourceName)} -> ` : ""}${quoteMalloyIdentifier2(queryName)}`;
250347
251188
  } else {
250348
251189
  const endTime = performance.now();
250349
251190
  const executionTime2 = endTime - startTime;
@@ -250368,7 +251209,10 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
250368
251209
  }
250369
251210
  runnable = this.modelMaterializer.loadRestrictedQuery(queryString);
250370
251211
  liveRunnable = runnable;
250371
- if (getPersistStorageMode() === "on" && this.serveBindings.length > 0 && this.serveDestinationConfig) {
251212
+ const storageRoutingPossible = getPersistStorageMode() === "on" && this.serveBindings.length > 0 && !!this.serveDestinationConfig;
251213
+ const preaggServeMaterializer = this.preaggregateServeMaterializer;
251214
+ const routingBlockedByRowLevelGate = (storageRoutingPossible || !!preaggServeMaterializer) && !bypassAuthorize && this.hasAnyAuthorizeNote() && await this.queryEntryPointHasRowLevelGate(runnable);
251215
+ if (storageRoutingPossible && !routingBlockedByRowLevelGate) {
250372
251216
  try {
250373
251217
  const shaped = await this.loadServeShapeQuery(queryString);
250374
251218
  runnable = shaped.runnable;
@@ -250388,9 +251232,9 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
250388
251232
  });
250389
251233
  }
250390
251234
  }
250391
- if (this.preaggregateServeMaterializer && !serveVirtualMap) {
251235
+ if (preaggServeMaterializer && !routingBlockedByRowLevelGate && !serveVirtualMap) {
250392
251236
  try {
250393
- const candidate = this.preaggregateServeMaterializer.loadRestrictedQuery(queryString);
251237
+ const candidate = preaggServeMaterializer.loadRestrictedQuery(queryString);
250394
251238
  await candidate.getSQL({
250395
251239
  givens: querySurfaceGivens,
250396
251240
  buildManifest
@@ -250430,7 +251274,10 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
250430
251274
  if (boundary === "deferred") {
250431
251275
  this.assertQueryBoundaryCompiled(compiledSource, query);
250432
251276
  }
250433
- await this.assertAuthorizedForAllSources(runnable, givens ?? {}, bypassAuthorize);
251277
+ runnable = await this.authorizeAndBindRunnable(runnable, givens ?? {}, {
251278
+ recompile: (mm) => mm.loadRestrictedQuery(queryString),
251279
+ bypassAuthorize
251280
+ });
250434
251281
  const maxRows = getMaxQueryRows();
250435
251282
  const maxBytes = getMaxResponseBytes();
250436
251283
  const effectiveBuildManifest = serveVirtualMap ? undefined : preaggRouted ? buildManifest : liveBuildManifest;
@@ -250530,6 +251377,9 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
250530
251377
  servedFrom = "live_fallback";
250531
251378
  executionTime = performance.now() - startTime;
250532
251379
  }
251380
+ if (this.queryHadRowLevelFilterAttached(runnable) && queryResults.totalRows === 0) {
251381
+ recordRowLevelGateDecision("empty_after_filter");
251382
+ }
250533
251383
  assertWithinModelRowLimit(queryResults.totalRows, maxRows, "model_query");
250534
251384
  const wrappedResult = API.util.wrapResult(queryResults);
250535
251385
  const serializedResult = stringifyQueryResponse(responseShape === "compact" ? queryResults.data.value : wrappedResult, queryResults.totalRows, maxBytes, "model_query", responseShape === "compact" ? bigIntReplacer : undefined);
@@ -250626,7 +251476,7 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
250626
251476
  return;
250627
251477
  try {
250628
251478
  const entry = this.modelDef.contents?.[sourceName];
250629
- if (!entry || !isSourceDef2(entry))
251479
+ if (!entry || !isSourceDef3(entry))
250630
251480
  return;
250631
251481
  const def = entry;
250632
251482
  if (!def.annotations)
@@ -250694,26 +251544,35 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
250694
251544
  text: cell.text
250695
251545
  };
250696
251546
  }
250697
- if (cell.runnable) {
250698
- await this.assertAuthorizedForAllSources(cell.runnable, givens ?? {});
250699
- }
250700
251547
  let queryName = undefined;
250701
251548
  let queryResult = undefined;
250702
251549
  if (cell.runnable) {
250703
251550
  try {
250704
251551
  let runnableToExecute = cell.runnable;
250705
- if (!bypassFilters && cell.modelMaterializer) {
250706
- const effectiveSource = extractRunTargetSourceName(cell.text);
250707
- if (effectiveSource) {
250708
- const filters = this.getFilters(effectiveSource);
250709
- if (filters.length > 0) {
250710
- const filterClause = buildFilterClause(filters, filterParams ?? {});
250711
- if (filterClause) {
250712
- const refinedQuery = injectFilterRefinement(cell.text, filterClause);
250713
- runnableToExecute = cell.modelMaterializer.loadQuery(refinedQuery);
250714
- }
250715
- }
250716
- }
251552
+ let textToExecute = cell.text;
251553
+ const { graftScope, usesOwnScope } = await this.resolveNotebookCellGraftScope(cellIndex, cell.runnable);
251554
+ if (!graftScope) {
251555
+ logger.debug("Notebook cell has no graft scope to attach a row-level gate against (no earlier code cell, and this cell has no compiled model of its own)", { modelPath: this.modelPath, cellIndex });
251556
+ }
251557
+ const effectiveSource = !bypassFilters && cell.modelMaterializer ? extractRunTargetSourceName(cell.text) : undefined;
251558
+ const cellFilters = effectiveSource ? this.getFilters(effectiveSource) : [];
251559
+ if (cell.modelMaterializer && cellFilters.length > 0) {
251560
+ await this.probeEntryPointGates(cell.runnable, givens ?? {}, graftScope);
251561
+ }
251562
+ const filterClause = cellFilters.length > 0 ? buildFilterClause(cellFilters, filterParams ?? {}) : undefined;
251563
+ if (filterClause && cell.modelMaterializer) {
251564
+ textToExecute = injectFilterRefinement(cell.text, filterClause);
251565
+ runnableToExecute = cell.modelMaterializer.loadQuery(textToExecute);
251566
+ }
251567
+ if (cell.modelMaterializer) {
251568
+ const textForRecompile = textToExecute;
251569
+ const queryDefForOwnScopeRepoint = usesOwnScope ? (await runnableToExecute.getPreparedQuery())._query : undefined;
251570
+ runnableToExecute = await this.authorizeAndBindRunnable(runnableToExecute, givens ?? {}, {
251571
+ recompile: usesOwnScope ? (mm) => mm._loadQueryFromQueryDef(queryDefForOwnScopeRepoint) : (mm) => mm.loadQuery(textForRecompile),
251572
+ graftScope
251573
+ });
251574
+ } else {
251575
+ await this.assertAuthorizedForAllSources(runnableToExecute, givens ?? {});
250717
251576
  }
250718
251577
  const cellMaxRows = getMaxQueryRows();
250719
251578
  const cellMaxBytes = getMaxResponseBytes();
@@ -250737,6 +251596,9 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
250737
251596
  });
250738
251597
  const query = (await runnableToExecute.getPreparedQuery())._query;
250739
251598
  queryName = query.as || query.name;
251599
+ if (result?._queryResult && result.totalRows === 0 && this.queryHadRowLevelFilterAttached(runnableToExecute)) {
251600
+ recordRowLevelGateDecision("empty_after_filter");
251601
+ }
250740
251602
  if (result?._queryResult) {
250741
251603
  assertWithinModelRowLimit(result.totalRows, cellMaxRows, "notebook_cell");
250742
251604
  }
@@ -250745,6 +251607,9 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
250745
251607
  assertWithinModelByteLimit(queryResult, cellMaxBytes, "notebook_cell");
250746
251608
  }
250747
251609
  } catch (error) {
251610
+ if (error instanceof AccessDeniedError) {
251611
+ throw error;
251612
+ }
250748
251613
  if (error instanceof FilterValidationError) {
250749
251614
  throw new BadRequestError(error.message);
250750
251615
  }
@@ -250824,14 +251689,23 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
250824
251689
  return malloyConfig;
250825
251690
  }
250826
251691
  static getQueries(modelDef) {
250827
- return extractQueriesFromModelDef(modelDef);
251692
+ const { queries, misplacedAuthorize } = extractQueriesFromModelDef(modelDef);
251693
+ return { queries, misplacedAuthorize };
250828
251694
  }
250829
251695
  static getSources(modelDef, givens) {
250830
- const { sources, filterMap, ownAuthorizeSources } = extractSourcesFromModelDef(modelDef, givens, (sourceName, err) => logger.warn(`Failed to parse filter annotations on source "${sourceName}"`, { error: err }));
251696
+ const {
251697
+ sources,
251698
+ filterMap,
251699
+ authorizeMap,
251700
+ misplacedAuthorize,
251701
+ authorizeOwnNotes
251702
+ } = extractSourcesFromModelDef(modelDef, givens, (sourceName, err) => logger.warn(`Failed to parse filter annotations on source "${sourceName}"`, { error: err }));
250831
251703
  return {
250832
251704
  sources,
250833
251705
  filterMap,
250834
- ownAuthorizeSources
251706
+ authorizeMap,
251707
+ misplacedAuthorize,
251708
+ authorizeOwnNotes
250835
251709
  };
250836
251710
  }
250837
251711
  static async getModelMaterializer(runtime, importBaseURL, modelURL, modelPath) {
@@ -250932,6 +251806,7 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
250932
251806
  text: stmt.text,
250933
251807
  runnable,
250934
251808
  modelMaterializer: localMM,
251809
+ modelDef: currentModelDef,
250935
251810
  newSources,
250936
251811
  queryInfo
250937
251812
  };
@@ -295572,6 +296447,8 @@ class MaterializationService {
295572
296447
  const destination = resolveStorageDestination(persistSource);
295573
296448
  if (destination) {
295574
296449
  assertMaterializationEligible(persistSource);
296450
+ } else {
296451
+ assertColocatedPersistNotAuthorizeGated(persistSource, persistSource.name, compiled.preaggregatePlans?.[persistSource.sourceID] ? "preaggregate" : "persist");
295575
296452
  }
295576
296453
  const sourceEntityId = computeSourceEntityId(persistSource, compiled.connectionDigests);
295577
296454
  if (seen.has(sourceEntityId))
@@ -295834,6 +296711,8 @@ class MaterializationService {
295834
296711
  const orchestratedInstruction = bySourceID.get(persistSource.sourceID);
295835
296712
  if (orchestratedInstruction?.destination && getPersistStorageMode() !== "off") {
295836
296713
  assertMaterializationEligible(persistSource);
296714
+ } else {
296715
+ assertColocatedPersistNotAuthorizeGated(persistSource, persistSource.name, compiled.preaggregatePlans?.[persistSource.sourceID] ? "preaggregate" : "persist");
295837
296716
  }
295838
296717
  const sourceEntityId = computeSourceEntityId(persistSource, connectionDigests);
295839
296718
  const instruction = orchestratedInstruction ?? bySourceEntityId.get(sourceEntityId);