@sanity/workflow-engine 0.28.0 → 0.29.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/dist/index.cjs CHANGED
@@ -449,7 +449,7 @@ function fieldEditedData(args) {
449
449
  };
450
450
  }
451
451
 
452
- const DATA_MODEL_VERSION = 5, DATA_MODEL_MIN_READER = 4, READER_MODEL_ROLLOUT_URL = "https://www.sanity.io/docs/editorial-workflows/prerelease";
452
+ const DATA_MODEL_VERSION = 7, DATA_MODEL_MIN_READER = 4, READER_MODEL_ROLLOUT_URL = "https://www.sanity.io/docs/editorial-workflows/prerelease";
453
453
 
454
454
  class ReaderModelAcknowledgementError extends invariants.WorkflowError {
455
455
  code="WORKFLOW_READER_MODEL_ACKNOWLEDGEMENT_MISMATCH";
@@ -499,7 +499,7 @@ const DATA_MODEL_CHANGES = Object.freeze([ Object.freeze({
499
499
  documentTypes: Object.freeze([ "definition" ]),
500
500
  compatibility: "additive",
501
501
  applicability: "detectable",
502
- summary: "Ordinary actions may carry a closed bag of advisory workflow semantics."
502
+ summary: "Ordinary actions may carry advisory workflow semantics."
503
503
  }), Object.freeze({
504
504
  id: "inclusive-scalar-bounds",
505
505
  introducedInModel: 2,
@@ -548,6 +548,30 @@ const DATA_MODEL_CHANGES = Object.freeze([ Object.freeze({
548
548
  compatibility: "additive",
549
549
  applicability: "detectable",
550
550
  summary: "Due-date field kinds (dueDate, dueDatetime) mark a level deadline, elevated aliases of date/datetime carrying the same stored value."
551
+ }), Object.freeze({
552
+ id: "node-semantics",
553
+ introducedInModel: 6,
554
+ minReaderModel: 0,
555
+ documentTypes: Object.freeze([ "definition" ]),
556
+ compatibility: "additive",
557
+ applicability: "detectable",
558
+ summary: "Workflow, stage, activity, and action nodes may carry signal or custom advisory semantics."
559
+ }), Object.freeze({
560
+ id: "field-patch-ops",
561
+ introducedInModel: 6,
562
+ minReaderModel: 0,
563
+ documentTypes: Object.freeze([ "definition", "instance" ]),
564
+ compatibility: "additive",
565
+ applicability: "detectable",
566
+ summary: "Field ops may increment, decrement, or initialize a missing field value."
567
+ }), Object.freeze({
568
+ id: "attributes-condition-var",
569
+ introducedInModel: 7,
570
+ minReaderModel: 0,
571
+ documentTypes: Object.freeze([ "definition" ]),
572
+ compatibility: "additive",
573
+ applicability: "unconditional",
574
+ summary: "Caller-bound $attributes condition variable binds the acting token's org-level User Attributes (advisory; absent when unavailable)."
551
575
  }) ]);
552
576
 
553
577
  function recordOf(value) {
@@ -570,13 +594,34 @@ function parsedDefinitionSnapshot(root) {
570
594
  }));
571
595
  }
572
596
 
573
- function persistedFieldEntries(document) {
597
+ function persistedDefinitionTree(document) {
574
598
  const root = recordOf(document);
575
- if (root === void 0) return [];
576
- const snapshot = parsedDefinitionSnapshot(root), roots = snapshot === void 0 ? [ root ] : [ root, snapshot ], stages = roots.flatMap(candidate => recordsAt(candidate, "stages")), activities = stages.flatMap(stage => recordsAt(stage, "activities")), actions = activities.flatMap(activity => recordsAt(activity, "actions")), effects = actions.flatMap(action => recordsAt(action, "effects"));
599
+ if (root === void 0) return;
600
+ const snapshot = parsedDefinitionSnapshot(root), roots = snapshot === void 0 ? [ root ] : [ root, snapshot ], {stages: stages, activities: activities, actions: actions} = definitionDescendants(roots);
601
+ return {
602
+ roots: roots,
603
+ stages: stages,
604
+ activities: activities,
605
+ actions: actions
606
+ };
607
+ }
608
+
609
+ function persistedFieldEntries(document) {
610
+ const tree = persistedDefinitionTree(document);
611
+ if (tree === void 0) return [];
612
+ const {roots: roots, stages: stages, activities: activities, actions: actions} = tree, effects = actions.flatMap(action => recordsAt(action, "effects"));
577
613
  return nestedFieldEntries([ ...roots.flatMap(candidate => recordsAt(candidate, "fields")), ...stages.flatMap(stage => recordsAt(stage, "fields")), ...activities.flatMap(activity => recordsAt(activity, "fields")), ...actions.flatMap(action => recordsAt(action, "params")), ...effects.flatMap(effect => recordsAt(effect, "outputs")) ]);
578
614
  }
579
615
 
616
+ function definitionDescendants(roots) {
617
+ const stages = roots.flatMap(root => recordsAt(root, "stages")), activities = stages.flatMap(stage => recordsAt(stage, "activities")), actions = activities.flatMap(activity => recordsAt(activity, "actions"));
618
+ return {
619
+ stages: stages,
620
+ activities: activities,
621
+ actions: actions
622
+ };
623
+ }
624
+
580
625
  function hasChoiceList(document) {
581
626
  return persistedFieldEntries(document).some(entry => {
582
627
  const options = recordOf(entry.options);
@@ -588,9 +633,34 @@ function hasFieldKind(document, kind) {
588
633
  return persistedFieldEntries(document).some(entry => entry.type === kind || entry._type === kind);
589
634
  }
590
635
 
591
- function hasActionSemantics(document) {
636
+ function definitionNodes(document) {
592
637
  const root = recordOf(document);
593
- return root === void 0 ? !1 : recordsAt(root, "stages").flatMap(stage => recordsAt(stage, "activities")).flatMap(activity => recordsAt(activity, "actions")).some(action => Array.isArray(action.semantics));
638
+ if (root !== void 0) return {
639
+ root: root,
640
+ ...definitionDescendants([ root ])
641
+ };
642
+ }
643
+
644
+ function hasActionSemantics(document) {
645
+ return definitionNodes(document)?.actions.some(hasSemantics) ?? !1;
646
+ }
647
+
648
+ function hasNodeSemantics(document) {
649
+ const nodes = definitionNodes(document);
650
+ return nodes === void 0 ? !1 : [ nodes.root, ...nodes.stages, ...nodes.activities ].some(hasSemantics) ? !0 : nodes.actions.some(action => hasSemantics(action) && action.semantics.some(isNodeSemanticValue));
651
+ }
652
+
653
+ function isNodeSemanticValue(semantic) {
654
+ return typeof semantic == "string" && (semantic.startsWith("signal.") || semantic.startsWith("custom."));
655
+ }
656
+
657
+ function hasSemantics(node) {
658
+ return Array.isArray(node.semantics);
659
+ }
660
+
661
+ function hasPersistedOpType(document, types) {
662
+ const tree = persistedDefinitionTree(document);
663
+ return tree === void 0 ? !1 : tree.actions.flatMap(action => recordsAt(action, "ops")).some(op => typeof op.type == "string" && types.includes(op.type)) ? !0 : tree.roots.some(root => recordsAt(root, "history").some(entry => entry._type === "opApplied" && typeof entry.opType == "string" && types.includes(entry.opType)));
594
664
  }
595
665
 
596
666
  function hasScalarValidation(document) {
@@ -623,7 +693,10 @@ const featureDetectors = {
623
693
  "effect-claim-tokens": hasClaimTokens,
624
694
  "classified-principal-ids": () => !0,
625
695
  "readiness-requirements": hasReadinessRequirements,
626
- "due-date-field-kinds": document => hasFieldKind(document, "dueDate") || hasFieldKind(document, "dueDatetime")
696
+ "due-date-field-kinds": document => hasFieldKind(document, "dueDate") || hasFieldKind(document, "dueDatetime"),
697
+ "node-semantics": hasNodeSemantics,
698
+ "field-patch-ops": document => hasPersistedOpType(document, [ "field.inc", "field.dec", "field.setIfMissing" ]),
699
+ "attributes-condition-var": () => !0
627
700
  };
628
701
 
629
702
  function requiredModelFeatures(documentType, document) {
@@ -1157,12 +1230,8 @@ function renderWorkflowRead(read, ctx) {
1157
1230
  text: groqConditionDescribe.guillemets(title)
1158
1231
  });
1159
1232
  }
1160
- if (read.variable === "can" && typeof read.path[0] == "string") return groqConditionDescribe.phrase("read.permission", {
1161
- params: {
1162
- permission: read.path[0]
1163
- },
1164
- text: `your ${read.path[0]} permission`
1165
- });
1233
+ const keyed = keyedCallerRead(read);
1234
+ if (keyed !== void 0) return keyed;
1166
1235
  if (read.variable === "actor" && read.path.length === 1 && read.path[0] === "id") return groqConditionDescribe.phrase("read.actor-id", {
1167
1236
  params: {},
1168
1237
  text: "your id"
@@ -1184,6 +1253,24 @@ function renderWorkflowRead(read, ctx) {
1184
1253
  });
1185
1254
  }
1186
1255
 
1256
+ function keyedCallerRead(read) {
1257
+ const key = read.path[0];
1258
+ if (typeof key == "string") {
1259
+ if (read.variable === "can") return groqConditionDescribe.phrase("read.permission", {
1260
+ params: {
1261
+ permission: key
1262
+ },
1263
+ text: `your ${key} permission`
1264
+ });
1265
+ if (read.variable === "attributes") return groqConditionDescribe.phrase("read.attribute", {
1266
+ params: {
1267
+ attribute: key
1268
+ },
1269
+ text: `your ${groqConditionDescribe.guillemets(key)} attribute`
1270
+ });
1271
+ }
1272
+ }
1273
+
1187
1274
  function workflowRequirement(requirement, ctx) {
1188
1275
  const {target: target} = requirement, clause = clauseVarRequirement(requirement);
1189
1276
  if (clause !== void 0) return clause;
@@ -2084,6 +2171,10 @@ function derefBase(ref, snapshot) {
2084
2171
  };
2085
2172
  }
2086
2173
 
2174
+ function isRecord(value) {
2175
+ return typeof value == "object" && value !== null && !Array.isArray(value);
2176
+ }
2177
+
2087
2178
  function buildParams(args) {
2088
2179
  const {instance: instance, now: now, snapshot: snapshot, extra: extra} = args, currentActivities2 = findOpenStageEntry(instance)?.activities ?? [];
2089
2180
  return {
@@ -2612,7 +2703,7 @@ class CascadeLimitError extends invariants.WorkflowError {
2612
2703
  }
2613
2704
  }
2614
2705
 
2615
- const FIELD_OP_TYPES = [ "field.set", "field.unset", "field.append", "field.updateWhere", "field.removeWhere" ];
2706
+ const FIELD_OP_TYPES = [ "field.set", "field.setIfMissing", "field.unset", "field.append", "field.inc", "field.dec", "field.updateWhere", "field.removeWhere" ];
2616
2707
 
2617
2708
  function isFieldOp(summary) {
2618
2709
  return FIELD_OP_TYPES.includes(summary.opType);
@@ -2713,7 +2804,7 @@ async function runOps(args) {
2713
2804
  refSurface: refSurface,
2714
2805
  opsFromDefinition: opsFromDefinition
2715
2806
  });
2716
- summaries.push(summary), mutation.history.push(opAppliedEntry({
2807
+ summaries.push(summary), shouldRecordOpApplied(summary) && mutation.history.push(opAppliedEntry({
2717
2808
  origin: origin,
2718
2809
  summary: summary,
2719
2810
  stage: stage,
@@ -2724,6 +2815,10 @@ async function runOps(args) {
2724
2815
  return summaries;
2725
2816
  }
2726
2817
 
2818
+ function shouldRecordOpApplied(summary) {
2819
+ return summary.opType !== "field.setIfMissing" || summary.resolved !== void 0;
2820
+ }
2821
+
2727
2822
  function opAppliedEntry(args) {
2728
2823
  const {origin: origin, summary: summary, stage: stage, actor: actor, now: now} = args;
2729
2824
  return {
@@ -2795,12 +2890,19 @@ async function applyOp(op, ctx) {
2795
2890
  case "field.set":
2796
2891
  return applyFieldSet(op, ctx);
2797
2892
 
2893
+ case "field.setIfMissing":
2894
+ return applyFieldSetIfMissing(op, ctx);
2895
+
2798
2896
  case "field.unset":
2799
2897
  return applyFieldUnset(op, ctx);
2800
2898
 
2801
2899
  case "field.append":
2802
2900
  return applyFieldAppend(op, ctx);
2803
2901
 
2902
+ case "field.inc":
2903
+ case "field.dec":
2904
+ return applyFieldArithmetic(op, ctx);
2905
+
2804
2906
  case "field.updateWhere":
2805
2907
  return applyFieldUpdateWhere(op, ctx);
2806
2908
 
@@ -2839,6 +2941,78 @@ function applyFieldSet(op, ctx) {
2839
2941
  };
2840
2942
  }
2841
2943
 
2944
+ function applyFieldSetIfMissing(op, ctx) {
2945
+ const entry = locateEntry(ctx, op.target);
2946
+ return invariants.isAlwaysArrayFieldKind(entry._type) && rejectFieldOpTarget({
2947
+ entry: entry,
2948
+ op: op,
2949
+ issue: "is always array-valued — setIfMissing applies to nullable entries only; an empty array entry already holds []"
2950
+ }), entry.value !== null && entry.value !== void 0 ? {
2951
+ opType: op.type,
2952
+ target: op.target
2953
+ } : {
2954
+ ...applyFieldSet({
2955
+ ...op,
2956
+ type: "field.set"
2957
+ }, ctx),
2958
+ opType: op.type
2959
+ };
2960
+ }
2961
+
2962
+ function applyFieldArithmetic(op, ctx) {
2963
+ const entry = locateEntry(ctx, op.target);
2964
+ entry._type !== "number" && rejectFieldOpTarget({
2965
+ entry: entry,
2966
+ op: op,
2967
+ issue: `is a ${entry._type} entry — arithmetic ops target \`number\` entries only`
2968
+ }), typeof entry.value != "number" && rejectFieldOpTarget({
2969
+ entry: entry,
2970
+ op: op,
2971
+ issue: "has no numeric value — initialize it before applying arithmetic"
2972
+ });
2973
+ const resolvedDelta = op.value === void 0 ? 1 : resolveOpValue({
2974
+ src: op.value,
2975
+ ctx: ctx,
2976
+ target: {
2977
+ kind: "number"
2978
+ }
2979
+ });
2980
+ (typeof resolvedDelta != "number" || !Number.isFinite(resolvedDelta)) && rejectFieldOpTarget({
2981
+ entry: entry,
2982
+ op: op,
2983
+ issue: "value must resolve to a finite number"
2984
+ });
2985
+ const nextValue = entry.value + (op.type === "field.inc" ? resolvedDelta : -resolvedDelta);
2986
+ Number.isFinite(nextValue) || rejectFieldOpTarget({
2987
+ entry: entry,
2988
+ op: op,
2989
+ issue: "result must be a finite number"
2990
+ });
2991
+ const value = invariants.validateFieldValue({
2992
+ entryType: entry._type,
2993
+ entryName: entry.name,
2994
+ value: nextValue,
2995
+ ...entryShape(entry)
2996
+ });
2997
+ return setEntryValue(entry, value), {
2998
+ opType: op.type,
2999
+ target: op.target,
3000
+ resolved: {
3001
+ value: value
3002
+ }
3003
+ };
3004
+ }
3005
+
3006
+ function rejectFieldOpTarget(args) {
3007
+ const {entry: entry, op: op, issue: issue} = args;
3008
+ throw new invariants.FieldValueShapeError({
3009
+ entryType: entry._type,
3010
+ entryName: entry.name,
3011
+ mode: "value",
3012
+ issues: [ `${op.type} target ${op.target.scope}:"${op.target.field}" ${issue}` ]
3013
+ });
3014
+ }
3015
+
2842
3016
  function entryShape(entry) {
2843
3017
  return entry._type === "object" ? {
2844
3018
  fields: entry.fields
@@ -2874,15 +3048,10 @@ function appendItemSlot(entry) {
2874
3048
  };
2875
3049
  }
2876
3050
 
2877
- const EMPTY_BY_KIND = {
2878
- "doc.refs": [],
2879
- array: [],
2880
- assignees: []
2881
- };
2882
-
2883
3051
  function applyFieldUnset(op, ctx) {
2884
3052
  const entry = locateEntry(ctx, op.target);
2885
- return setEntryValue(entry, EMPTY_BY_KIND[entry._type] ?? null), {
3053
+ return setEntryValue(entry, invariants.isAlwaysArrayFieldKind(entry._type) ? [] : null),
3054
+ {
2886
3055
  opType: op.type,
2887
3056
  target: op.target
2888
3057
  };
@@ -2921,7 +3090,7 @@ function applyFieldAppend(op, ctx) {
2921
3090
  }
2922
3091
 
2923
3092
  function withRowKey(item) {
2924
- return typeof item == "object" && item !== null && !Array.isArray(item) && !("_key" in item) ? {
3093
+ return isRecord(item) && !("_key" in item) ? {
2925
3094
  _key: randomKey(),
2926
3095
  ...item
2927
3096
  } : item;
@@ -3620,6 +3789,14 @@ function latestDeployedDefinitions(rows) {
3620
3789
  return [ ...byName.values() ];
3621
3790
  }
3622
3791
 
3792
+ function findStageNode(args) {
3793
+ return args.definition?.stages.find(entry => entry.name === args.stageName);
3794
+ }
3795
+
3796
+ function findActivityNode(args) {
3797
+ return findStageNode(args)?.activities?.find(entry => entry.name === args.activityName);
3798
+ }
3799
+
3623
3800
  function liveChildrenField(instance) {
3624
3801
  const live = liveSubworkflows(instance).length;
3625
3802
  return live > 0 ? {
@@ -3708,6 +3885,27 @@ function noTransitionFiresCause(input) {
3708
3885
  };
3709
3886
  }
3710
3887
 
3888
+ function stuckFromDocumentState(input) {
3889
+ return failedEffectCause(input) ?? failedActivityCause(input) ?? hungEffectCause(input);
3890
+ }
3891
+
3892
+ function documentStuckCause(args) {
3893
+ const stage = findOpenStageEntry(args.instance);
3894
+ if (stage !== void 0) return stuckFromDocumentState({
3895
+ instance: args.instance,
3896
+ activities: stage.activities.map(entry => ({
3897
+ status: entry.status,
3898
+ activity: findActivityNode({
3899
+ activityName: entry.name,
3900
+ definition: args.definition,
3901
+ stageName: stage.name
3902
+ }) ?? {
3903
+ name: entry.name
3904
+ }
3905
+ }))
3906
+ });
3907
+ }
3908
+
3711
3909
  function diagnoseInstance(input) {
3712
3910
  const {instance: instance} = input, terminal = terminalState(instance);
3713
3911
  if (terminal === "aborted" && instance.abortedAt !== void 0) {
@@ -3726,7 +3924,7 @@ function diagnoseInstance(input) {
3726
3924
  at: instance.completedAt,
3727
3925
  ...liveChildrenField(instance)
3728
3926
  };
3729
- const cause = failedEffectCause(input) ?? failedActivityCause(input) ?? hungEffectCause(input) ?? noTransitionFiresCause(input);
3927
+ const cause = stuckFromDocumentState(input) ?? noTransitionFiresCause(input);
3730
3928
  return cause !== void 0 ? {
3731
3929
  state: "stuck",
3732
3930
  cause: cause
@@ -4105,10 +4303,8 @@ function assertRequiredInputProvided({entryDefs: entryDefs, initialFields: initi
4105
4303
  });
4106
4304
  }
4107
4305
 
4108
- const ARRAY_FIELD_TYPES = /* @__PURE__ */ new Set([ "doc.refs", "array", "assignees" ]);
4109
-
4110
4306
  function defaultEntryValue(entryType) {
4111
- return ARRAY_FIELD_TYPES.has(entryType) ? [] : null;
4307
+ return invariants.isAlwaysArrayFieldKind(entryType) ? [] : null;
4112
4308
  }
4113
4309
 
4114
4310
  function resolveInputValue({entry: entry, initialFields: initialFields, defaultValue: defaultValue}) {
@@ -4709,7 +4905,9 @@ const SYNC_COMMIT = {
4709
4905
  effect: "workflow.effect",
4710
4906
  verifyDefinitions: "workflow.verify-definitions",
4711
4907
  accessResolveActor: "workflow.access.resolve-actor",
4712
- accessGrants: "workflow.access.grants"
4908
+ accessGrants: "workflow.access.grants",
4909
+ accessResolveOrg: "workflow.access.resolve-org",
4910
+ accessAttributes: "workflow.access.attributes"
4713
4911
  }, RAW_PATCH = /* @__PURE__ */ Symbol("workflow-engine.raw-patch");
4714
4912
 
4715
4913
  function unwrapPatch(patch) {
@@ -5677,10 +5875,6 @@ function userLoginProvider(user) {
5677
5875
  return user?.provider ?? user?.loginProvider;
5678
5876
  }
5679
5877
 
5680
- function isRecord(value) {
5681
- return typeof value == "object" && value !== null && !Array.isArray(value);
5682
- }
5683
-
5684
5878
  function optionalString(value) {
5685
5879
  return value === void 0 || typeof value == "string";
5686
5880
  }
@@ -7218,116 +7412,523 @@ async function advisoryCan({instance: instance, identity: identity, grants: gran
7218
7412
  return can;
7219
7413
  }
7220
7414
 
7221
- function requirementDescriptor(requirement) {
7222
- return {
7223
- name: requirement.name,
7224
- ...requirement.title !== void 0 ? {
7225
- title: requirement.title
7226
- } : {},
7227
- ...requirement.description !== void 0 ? {
7228
- description: requirement.description
7229
- } : {}
7230
- };
7231
- }
7415
+ const DANGEROUS_ATTRIBUTE_KEYS = /* @__PURE__ */ new Set([ "__proto__", "constructor", "prototype" ]);
7232
7416
 
7233
- function subjectDenialLabels(denied) {
7234
- return denied.map(d => `${d.permission} on ${d.subject} (${d.resource})`);
7417
+ function attributeEntry(row) {
7418
+ if (!(!isRecord(row) || typeof row.key != "string" || row.key.length === 0) && !DANGEROUS_ATTRIBUTE_KEYS.has(row.key) && !(!("activeValue" in row) || row.activeValue === void 0)) return [ row.key, row.activeValue ];
7235
7419
  }
7236
7420
 
7237
- class ActionDisabledError extends invariants.WorkflowError {
7238
- reason;
7239
- activity;
7240
- action;
7241
- constructor(args) {
7242
- super("action-disabled", formatDisabledReason({
7243
- activity: args.activity,
7244
- action: args.action,
7245
- reason: args.reason
7246
- })), this.name = "ActionDisabledError", this.reason = args.reason, this.activity = args.activity,
7247
- this.action = args.action;
7421
+ function normalizeUserAttributes(response) {
7422
+ if (!isRecord(response)) return;
7423
+ const rows = response.attributes;
7424
+ if (!Array.isArray(rows)) return;
7425
+ const out = {};
7426
+ for (const row of rows) {
7427
+ const entry = attributeEntry(row);
7428
+ entry !== void 0 && (out[entry[0]] = entry[1]);
7248
7429
  }
7430
+ return out;
7249
7431
  }
7250
7432
 
7251
- class StartNotAllowedError extends invariants.WorkflowError {
7252
- definition;
7253
- unmetRequirements;
7254
- constructor(args) {
7255
- super("start-not-allowed", `startInstance refused definition "${args.definition}": ${args.unmetRequirements.map(requirement => requirement.title ?? requirement.name).join(", ")}. Pre-flight the verdict with evaluateStart.`),
7256
- this.name = "StartNotAllowedError", this.definition = args.definition, this.unmetRequirements = args.unmetRequirements;
7257
- }
7258
- }
7433
+ const actorCache = /* @__PURE__ */ new WeakMap, grantsCache = /* @__PURE__ */ new WeakMap, orgIdCache = /* @__PURE__ */ new WeakMap, attributesCache = /* @__PURE__ */ new WeakMap;
7259
7434
 
7260
- function actionRendering(action) {
7261
- const kind = action.disabledReason?.kind;
7262
- return kind === "filter-failed" ? "absent" : action.triggered === !0 || kind === "cascade-fired" ? "automation" : "button";
7435
+ async function resolveAccess(taggedClient, args = {}) {
7436
+ const client = unwrapRequestTag(taggedClient), requestFn = lazyRequest(client);
7437
+ if (requestFn === void 0) throw new invariants.ContractViolationError("workflow: no actor available. The engine resolves the actor from the client's token via `client.request({ uri: '/users/me' })`. Supply a real `@sanity/client` configured with a token (the test bench serves these endpoints per registered token).");
7438
+ const grantsPromise = args.grantsFromPath !== void 0 ? cachedGrants({
7439
+ client: client,
7440
+ requestFn: requestFn,
7441
+ resourcePath: args.grantsFromPath
7442
+ }) : Promise.resolve(void 0), [identity, grants] = await Promise.all([ cachedActor(client, requestFn), grantsPromise ]);
7443
+ if (identity === void 0) throw new invariants.ContractViolationError("workflow: failed to resolve actor from `/users/me`. The client is configured but the endpoint returned no usable identity — check the token.");
7444
+ return {
7445
+ actor: identity.actor,
7446
+ ...identity.localPrincipalId !== void 0 ? {
7447
+ localPrincipalId: identity.localPrincipalId
7448
+ } : {},
7449
+ ...grants !== void 0 ? {
7450
+ grants: grants
7451
+ } : {}
7452
+ };
7263
7453
  }
7264
7454
 
7265
- const disabledReasonDetail = {
7266
- "filter-failed": r => `action filter returned false${r.detail ? ` (${r.detail})` : ""}`,
7267
- "cascade-fired": r => `the action is cascade-fired (when: ${JSON.stringify(r.when)}) — the engine fires it on truth; it cannot be invoked via fireAction`,
7268
- "activity-not-active": r => `activity status is "${r.status}"`,
7269
- "stage-terminal": r => `stage "${r.stage}" is terminal`,
7270
- "instance-completed": r => `instance completed at ${r.completedAt}`,
7271
- "instance-aborted": r => `instance aborted at ${r.abortedAt}`,
7272
- "requirements-unmet": r => `unmet requirement(s): ${r.unmetRequirements.map(requirement => requirement.name).join(", ")}`,
7273
- "subject-permission-denied": r => `missing subject permission(s): ${subjectDenialLabels(r.denied).join(", ")}`
7274
- };
7275
-
7276
- function actionDisabledDetail(reason) {
7277
- return disabledReasonDetail[reason.kind](reason);
7455
+ async function resolveUserAttributes(taggedClient) {
7456
+ const client = unwrapRequestTag(taggedClient), requestFn = lazyRequest(client);
7457
+ if (requestFn !== void 0) return cachedAttributes({
7458
+ client: client,
7459
+ requestFn: requestFn
7460
+ });
7278
7461
  }
7279
7462
 
7280
- function formatDisabledReason({activity: activity, action: action, reason: reason}) {
7281
- return `Action "${activity}:${action}" is not allowed: ${actionDisabledDetail(reason)}`;
7463
+ function cachedActor(client, requestFn) {
7464
+ const cached = actorCache.get(client);
7465
+ if (cached !== void 0) return cached;
7466
+ const pending = fetchActor(client, requestFn).catch(err => {
7467
+ throw actorCache.get(client) === pending && actorCache.delete(client), err;
7468
+ });
7469
+ return actorCache.set(client, pending), pending;
7282
7470
  }
7283
7471
 
7284
- class EditFieldDeniedError extends invariants.WorkflowError {
7285
- reason;
7286
- target;
7287
- constructor(args) {
7288
- super("edit-field-denied", formatEditDisabledReason(args.target, args.reason)),
7289
- this.name = "EditFieldDeniedError", this.reason = args.reason, this.target = args.target;
7290
- }
7472
+ function grantsForClientPath(taggedClient, resourcePath) {
7473
+ const client = unwrapRequestTag(taggedClient), requestFn = lazyRequest(client);
7474
+ return requestFn === void 0 ? Promise.resolve(void 0) : cachedGrants({
7475
+ client: client,
7476
+ requestFn: requestFn,
7477
+ resourcePath: resourcePath
7478
+ });
7291
7479
  }
7292
7480
 
7293
- const editDisabledReasonDetail = {
7294
- "not-editable": () => "field is not declared editable",
7295
- "instance-completed": r => `instance completed at ${r.completedAt}`,
7296
- "instance-aborted": r => `instance aborted at ${r.abortedAt}`,
7297
- "edit-window-closed": r => `edit window closed (${r.detail})`,
7298
- "editor-not-permitted": r => `editor not permitted (${r.predicate})`
7299
- };
7300
-
7301
- function formatEditDisabledReason(target, reason) {
7302
- const detail = editDisabledReasonDetail[reason.kind](reason), where = target.activity !== void 0 ? `${target.activity}.${target.field}` : target.field;
7303
- return `Field "${target.scope}:${where}" is not editable: ${detail}`;
7481
+ function lazyRequest(client) {
7482
+ return client.request === void 0 ? void 0 : opts => client.request(opts);
7304
7483
  }
7305
7484
 
7306
- async function fireAction(args) {
7307
- const {client: client, instanceId: instanceId, activity: activity, action: action, params: params, requestRecord: requestRecord, options: options} = args;
7308
- return retryOnRevisionConflict({
7485
+ function cachedGrants({client: client, requestFn: requestFn, resourcePath: resourcePath}) {
7486
+ return cachedByKey({
7487
+ store: grantsCache,
7309
7488
  client: client,
7310
- instanceId: instanceId,
7311
- options: options,
7312
- commit: ctx => commitAction({
7313
- ctx: ctx,
7314
- activityName: activity,
7315
- actionName: action,
7316
- callerParams: params,
7317
- requestRecord: requestRecord,
7318
- options: options
7319
- }),
7320
- onExhausted: () => new ConcurrentFireActionError({
7321
- instanceId: instanceId,
7322
- activity: activity,
7323
- action: action,
7324
- attempts: CONCURRENT_COMMIT_MAX_ATTEMPTS
7325
- })
7489
+ key: resourcePath,
7490
+ load: () => fetchGrantsCached(requestFn, resourcePath)
7326
7491
  });
7327
7492
  }
7328
7493
 
7329
- async function resolveActionCommit({ctx: ctx, activityName: activityName, actionName: actionName, callerParams: callerParams, options: options}) {
7330
- const actor = options?.actor, {stage: stage, activity: activity} = findActivityInCurrentStage(ctx, activityName), action = (activity.actions ?? []).find(a => a.name === actionName);
7494
+ function cachedByKey(args) {
7495
+ let byKey = args.store.get(args.client);
7496
+ byKey === void 0 && (byKey = /* @__PURE__ */ new Map, args.store.set(args.client, byKey));
7497
+ const cached = byKey.get(args.key);
7498
+ if (cached !== void 0) return cached;
7499
+ const pending = args.evictOnRejection ? args.load().catch(err => {
7500
+ throw byKey.get(args.key) === pending && byKey.delete(args.key), err;
7501
+ }) : args.load();
7502
+ return byKey.set(args.key, pending), pending;
7503
+ }
7504
+
7505
+ async function fetchActor(client, requestFn) {
7506
+ const [resourceUser, globalUser] = await Promise.all([ fetchCurrentUser(requestFn, "the workflow resource host"), fetchGlobalUser(client) ]), resourceId = usableId(resourceUser), globalHostId = usableId(globalUser.user), carriedId = invariants.firstCarriedGlobalId([ globalHostId, resourceId ]), sessionId = carriedId ?? globalHostId ?? resourceId;
7507
+ if (sessionId === void 0) return;
7508
+ const bridge = carriedId === void 0 ? await bridgeProjectPrincipal({
7509
+ client: client,
7510
+ sessionId: sessionId,
7511
+ resourceId: resourceId
7512
+ }) : NO_BRIDGE, id = bridge.status === "resolved" ? bridge.globalId : sessionId;
7513
+ refuseProjectScopedActor({
7514
+ id: id,
7515
+ globalUser: globalUser,
7516
+ globalHostId: globalHostId,
7517
+ bridgeFailure: bridge.status === "unavailable" ? bridge : void 0
7518
+ });
7519
+ const roleNames = roleNamesFor(resourceUser, globalUser);
7520
+ return {
7521
+ actor: {
7522
+ kind: "person",
7523
+ id: id,
7524
+ ...roleNames.length > 0 ? {
7525
+ roles: roleNames
7526
+ } : {}
7527
+ },
7528
+ ...resourceId !== void 0 && resourceId !== id ? {
7529
+ localPrincipalId: resourceId
7530
+ } : {}
7531
+ };
7532
+ }
7533
+
7534
+ function roleNamesFor(resourceUser, globalUser) {
7535
+ return (resourceUser?.roles?.length ? resourceUser : globalUser.user)?.roles?.map(r => r.name).filter(n => !!n) ?? [];
7536
+ }
7537
+
7538
+ const NO_BRIDGE = {
7539
+ status: "not-applicable"
7540
+ };
7541
+
7542
+ function globalRouteReason(globalUser, globalHostId) {
7543
+ return "reason" in globalUser ? globalUser.reason : globalHostId === void 0 ? "no record" : `the global host answered with project-scoped principal "${globalHostId}"`;
7544
+ }
7545
+
7546
+ async function bridgeProjectPrincipal(args) {
7547
+ const {client: client, sessionId: sessionId, resourceId: resourceId} = args;
7548
+ return invariants.classifyPrincipalId(sessionId).namespace !== "project" ? NO_BRIDGE : resourceId === void 0 || invariants.classifyPrincipalId(resourceId).namespace !== "project" ? {
7549
+ status: "unavailable",
7550
+ reason: "no project-scoped principal from the resource host to address the directory with"
7551
+ } : directoryGlobalId({
7552
+ client: client,
7553
+ principalId: resourceId
7554
+ });
7555
+ }
7556
+
7557
+ async function directoryGlobalId(args) {
7558
+ const {client: client, principalId: principalId} = args, projectId = projectIdOf(client);
7559
+ if (projectId === void 0) return {
7560
+ status: "unavailable",
7561
+ reason: "the client reports no projectId, so its user directory has no address"
7562
+ };
7563
+ const lookup = await clientProjectUserDirectory(withRequestTag(client, REQUEST_TAG.accessResolveActor), projectId).findById(principalId);
7564
+ if (lookup.status !== "resolved") return {
7565
+ status: "unavailable",
7566
+ reason: `project "${projectId}"'s user directory reported the principal ${lookup.status}`,
7567
+ ...lookup.status === "inaccessible" && lookup.cause !== void 0 ? {
7568
+ cause: lookup.cause
7569
+ } : {}
7570
+ };
7571
+ const globalId = invariants.directoryBridgeId(lookup.user.sanityUserId);
7572
+ return globalId === void 0 ? {
7573
+ status: "unavailable",
7574
+ reason: `project "${projectId}"'s user directory row for the principal carries no account-global sanityUserId`
7575
+ } : {
7576
+ status: "resolved",
7577
+ globalId: globalId
7578
+ };
7579
+ }
7580
+
7581
+ function refuseProjectScopedActor(args) {
7582
+ const {id: id, globalUser: globalUser, globalHostId: globalHostId, bridgeFailure: bridgeFailure} = args;
7583
+ if (invariants.classifyPrincipalId(id).namespace !== "project") return;
7584
+ const routes = [ `The account-global record: ${globalRouteReason(globalUser, globalHostId)}.` ];
7585
+ bridgeFailure !== void 0 && routes.push(`The project user directory: ${bridgeFailure.reason}.`);
7586
+ const cause = bridgeFailure?.cause ?? ("cause" in globalUser ? globalUser.cause : void 0);
7587
+ throw new Error(`workflow: the caller's identity resolves to the project-scoped principal "${id}" and its account-global id could not be resolved. ${routes.join(" ")} The engine speaks account-global user ids only and never acts as a project-scoped principal.`, cause === void 0 ? void 0 : {
7588
+ cause: cause
7589
+ });
7590
+ }
7591
+
7592
+ function usableId(user) {
7593
+ if (!(!user || typeof user.id != "string" || user.id.length === 0)) return user.id;
7594
+ }
7595
+
7596
+ async function fetchCurrentUser(requestFn, hostDescription) {
7597
+ try {
7598
+ return await requestFn({
7599
+ uri: "/users/me",
7600
+ tag: REQUEST_TAG.accessResolveActor
7601
+ });
7602
+ } catch (err) {
7603
+ throw new Error(`workflow: /users/me request against ${hostDescription} failed. The engine resolves the actor from the client's token via \`client.request({ uri: '/users/me' })\`. Check the token/connectivity.`, {
7604
+ cause: err
7605
+ });
7606
+ }
7607
+ }
7608
+
7609
+ async function fetchGlobalUser(client) {
7610
+ const sibling = globalHostRequestOf(client);
7611
+ if ("reason" in sibling) return {
7612
+ user: void 0,
7613
+ reason: sibling.reason,
7614
+ ...sibling.cause !== void 0 ? {
7615
+ cause: sibling.cause
7616
+ } : {}
7617
+ };
7618
+ try {
7619
+ const user = await sibling.request({
7620
+ uri: "/users/me",
7621
+ tag: REQUEST_TAG.accessResolveActor
7622
+ }), id = usableId(user);
7623
+ return sibling.requireGlobalPrincipal && id !== void 0 && invariants.classifyPrincipalId(id).namespace === "project" ? {
7624
+ user: void 0,
7625
+ reason: `global-host /users/me returned project-scoped principal "${id}"; the sibling client is still bound to a project API host`
7626
+ } : {
7627
+ user: user
7628
+ };
7629
+ } catch (err) {
7630
+ return {
7631
+ user: void 0,
7632
+ reason: `global-host /users/me failed: ${invariants.errorMessage(err)}`,
7633
+ cause: err
7634
+ };
7635
+ }
7636
+ }
7637
+
7638
+ function globalHostRequestOf(client) {
7639
+ if (typeof client.withConfig != "function") return {
7640
+ reason: "the client cannot reach the global API host (no withConfig)"
7641
+ };
7642
+ const hostResolution = resolveGlobalApiHost(client);
7643
+ try {
7644
+ const globalClient = client.withConfig({
7645
+ useProjectHostname: !1,
7646
+ apiVersion: ENGINE_API_VERSION,
7647
+ ...hostResolution.apiHost === void 0 ? {} : {
7648
+ apiHost: hostResolution.apiHost
7649
+ }
7650
+ }), request = lazyRequest(globalClient);
7651
+ return request === void 0 ? {
7652
+ reason: "the global-host sibling client cannot issue requests"
7653
+ } : {
7654
+ request: request,
7655
+ requireGlobalPrincipal: hostResolution.requireGlobalPrincipal
7656
+ };
7657
+ } catch (err) {
7658
+ return {
7659
+ reason: `building the global-host sibling client failed: ${invariants.errorMessage(err)}`,
7660
+ cause: err
7661
+ };
7662
+ }
7663
+ }
7664
+
7665
+ function resolveGlobalApiHost(client) {
7666
+ if (typeof client.config != "function") return {
7667
+ requireGlobalPrincipal: !1
7668
+ };
7669
+ const apiHost = client.config().apiHost;
7670
+ if (apiHost === void 0) return {
7671
+ requireGlobalPrincipal: !1
7672
+ };
7673
+ let parsed;
7674
+ try {
7675
+ parsed = new URL(apiHost);
7676
+ } catch {
7677
+ return {
7678
+ requireGlobalPrincipal: !0
7679
+ };
7680
+ }
7681
+ if (/^api\.sanity\.(io|work)$/.test(parsed.hostname)) return {
7682
+ requireGlobalPrincipal: !1
7683
+ };
7684
+ const match = /^[^.]+\.api\.sanity\.(io|work)$/.exec(parsed.hostname);
7685
+ return match === null ? {
7686
+ requireGlobalPrincipal: !0
7687
+ } : (parsed.hostname = `api.sanity.${match[1]}`, {
7688
+ apiHost: parsed.origin,
7689
+ requireGlobalPrincipal: !0
7690
+ });
7691
+ }
7692
+
7693
+ async function fetchGrantsCached(requestFn, resourcePath) {
7694
+ try {
7695
+ return await fetchGrants({
7696
+ client: {
7697
+ request: requestFn
7698
+ },
7699
+ resourcePath: resourcePath
7700
+ });
7701
+ } catch (err) {
7702
+ console.warn(`workflow: failed to fetch grants from "${resourcePath}"; advisory permission reads that depend on them are skipped — a rendered $can stays undefined (conditions referencing it fail closed) and the subject-write forecast omits this resource. The lake still enforces writes. Original error: ${invariants.errorMessage(err)}`);
7703
+ return;
7704
+ }
7705
+ }
7706
+
7707
+ function projectIdOf(client) {
7708
+ if (typeof client.config != "function") return;
7709
+ const projectId = client.config().projectId;
7710
+ return typeof projectId == "string" && projectId.length > 0 ? projectId : void 0;
7711
+ }
7712
+
7713
+ function cachedAttributes(args) {
7714
+ const projectId = projectIdOf(args.client);
7715
+ return projectId === void 0 ? Promise.resolve(void 0) : resolveOrganizationId({
7716
+ client: args.client,
7717
+ requestFn: args.requestFn,
7718
+ projectId: projectId
7719
+ }).then(orgId => {
7720
+ if (orgId !== void 0) return cachedByKey({
7721
+ store: attributesCache,
7722
+ client: args.client,
7723
+ key: orgId,
7724
+ evictOnRejection: !0,
7725
+ load: () => fetchAttributesCached({
7726
+ client: args.client,
7727
+ orgId: orgId
7728
+ })
7729
+ });
7730
+ });
7731
+ }
7732
+
7733
+ async function resolveOrganizationId(args) {
7734
+ const {client: client, requestFn: requestFn, projectId: projectId} = args;
7735
+ return cachedByKey({
7736
+ store: orgIdCache,
7737
+ client: client,
7738
+ key: projectId,
7739
+ evictOnRejection: !0,
7740
+ load: async () => {
7741
+ try {
7742
+ const project = await requestFn({
7743
+ uri: `/projects/${encodeURIComponent(projectId)}`,
7744
+ tag: REQUEST_TAG.accessResolveOrg
7745
+ });
7746
+ if (!isRecord(project)) return;
7747
+ const orgId = project.organizationId;
7748
+ return typeof orgId == "string" && orgId.length > 0 ? orgId : void 0;
7749
+ } catch (err) {
7750
+ const status = httpStatusOf(err);
7751
+ if (status !== void 0 && EXPECTED_ATTRIBUTES_ABSENCE.has(status)) return;
7752
+ throw new Error(`workflow: failed to resolve organizationId for project "${projectId}"; advisory $attributes cannot be bound. Original error: ${invariants.errorMessage(err)}`, {
7753
+ cause: err
7754
+ });
7755
+ }
7756
+ }
7757
+ });
7758
+ }
7759
+
7760
+ const ATTRIBUTES_FETCH_LIMIT = 100, EXPECTED_ATTRIBUTES_ABSENCE = /* @__PURE__ */ new Set([ 401, 402, 403, 404 ]);
7761
+
7762
+ function httpStatusOf(err) {
7763
+ if (!isRecord(err)) return;
7764
+ const status = err.statusCode;
7765
+ return typeof status == "number" ? status : void 0;
7766
+ }
7767
+
7768
+ async function fetchAttributesCached(args) {
7769
+ const {client: client, orgId: orgId} = args, sibling = globalHostRequestOf(client);
7770
+ if ("reason" in sibling) {
7771
+ console.warn(`workflow: skipped org attributes fetch (${sibling.reason}); advisory $attributes stays undefined (conditions referencing it fail closed).`);
7772
+ return;
7773
+ }
7774
+ try {
7775
+ const response = await sibling.request({
7776
+ uri: `/organizations/${encodeURIComponent(orgId)}/users/me/attributes?limit=${ATTRIBUTES_FETCH_LIMIT}`,
7777
+ tag: REQUEST_TAG.accessAttributes
7778
+ }), envelope = isRecord(response) ? response : void 0;
7779
+ if (envelope?.hasMore === !0) {
7780
+ const page = Array.isArray(envelope.attributes) ? envelope.attributes : [];
7781
+ console.warn(`workflow: org attributes for organization "${orgId}" report hasMore=true (bound ${page.length} of the requested limit ${ATTRIBUTES_FETCH_LIMIT}); advisory $attributes may be incomplete — the engine fetches no further pages. Conditions that read missing keys fail closed.`);
7782
+ }
7783
+ const normalized = normalizeUserAttributes(response);
7784
+ return normalized === void 0 && console.warn(`workflow: org attributes response for organization "${orgId}" was unusable; advisory $attributes stays undefined (conditions referencing it fail closed).`),
7785
+ normalized;
7786
+ } catch (err) {
7787
+ const status = httpStatusOf(err);
7788
+ if (status !== void 0 && EXPECTED_ATTRIBUTES_ABSENCE.has(status)) return;
7789
+ throw new Error(`workflow: failed to fetch org attributes for organization "${orgId}". Original error: ${invariants.errorMessage(err)}`, {
7790
+ cause: err
7791
+ });
7792
+ }
7793
+ }
7794
+
7795
+ function callerBoundVars(args) {
7796
+ const vars = {
7797
+ ...args.can !== void 0 ? {
7798
+ can: args.can
7799
+ } : {},
7800
+ ...args.attributes !== void 0 ? {
7801
+ attributes: args.attributes
7802
+ } : {}
7803
+ };
7804
+ return Object.keys(vars).length > 0 ? vars : void 0;
7805
+ }
7806
+
7807
+ async function callerBoundVarsForCall(args) {
7808
+ const can = await advisoryCanForCall({
7809
+ instance: args.instance,
7810
+ options: args.options
7811
+ }), attributes = args.options?.attributes !== void 0 ? args.options.attributes : await resolveUserAttributes(args.client);
7812
+ return callerBoundVars({
7813
+ ...can !== void 0 ? {
7814
+ can: can
7815
+ } : {},
7816
+ ...attributes !== void 0 ? {
7817
+ attributes: attributes
7818
+ } : {}
7819
+ });
7820
+ }
7821
+
7822
+ function requirementDescriptor(requirement) {
7823
+ return {
7824
+ name: requirement.name,
7825
+ ...requirement.title !== void 0 ? {
7826
+ title: requirement.title
7827
+ } : {},
7828
+ ...requirement.description !== void 0 ? {
7829
+ description: requirement.description
7830
+ } : {}
7831
+ };
7832
+ }
7833
+
7834
+ function subjectDenialLabels(denied) {
7835
+ return denied.map(d => `${d.permission} on ${d.subject} (${d.resource})`);
7836
+ }
7837
+
7838
+ class ActionDisabledError extends invariants.WorkflowError {
7839
+ reason;
7840
+ activity;
7841
+ action;
7842
+ constructor(args) {
7843
+ super("action-disabled", formatDisabledReason({
7844
+ activity: args.activity,
7845
+ action: args.action,
7846
+ reason: args.reason
7847
+ })), this.name = "ActionDisabledError", this.reason = args.reason, this.activity = args.activity,
7848
+ this.action = args.action;
7849
+ }
7850
+ }
7851
+
7852
+ class StartNotAllowedError extends invariants.WorkflowError {
7853
+ definition;
7854
+ unmetRequirements;
7855
+ constructor(args) {
7856
+ super("start-not-allowed", `startInstance refused definition "${args.definition}": ${args.unmetRequirements.map(requirement => requirement.title ?? requirement.name).join(", ")}. Pre-flight the verdict with evaluateStart.`),
7857
+ this.name = "StartNotAllowedError", this.definition = args.definition, this.unmetRequirements = args.unmetRequirements;
7858
+ }
7859
+ }
7860
+
7861
+ function actionRendering(action) {
7862
+ const kind = action.disabledReason?.kind;
7863
+ return kind === "filter-failed" ? "absent" : action.triggered === !0 || kind === "cascade-fired" ? "automation" : "button";
7864
+ }
7865
+
7866
+ const disabledReasonDetail = {
7867
+ "filter-failed": r => `action filter returned false${r.detail ? ` (${r.detail})` : ""}`,
7868
+ "cascade-fired": r => `the action is cascade-fired (when: ${JSON.stringify(r.when)}) — the engine fires it on truth; it cannot be invoked via fireAction`,
7869
+ "activity-not-active": r => `activity status is "${r.status}"`,
7870
+ "stage-terminal": r => `stage "${r.stage}" is terminal`,
7871
+ "instance-completed": r => `instance completed at ${r.completedAt}`,
7872
+ "instance-aborted": r => `instance aborted at ${r.abortedAt}`,
7873
+ "requirements-unmet": r => `unmet requirement(s): ${r.unmetRequirements.map(requirement => requirement.name).join(", ")}`,
7874
+ "subject-permission-denied": r => `missing subject permission(s): ${subjectDenialLabels(r.denied).join(", ")}`
7875
+ };
7876
+
7877
+ function actionDisabledDetail(reason) {
7878
+ return disabledReasonDetail[reason.kind](reason);
7879
+ }
7880
+
7881
+ function formatDisabledReason({activity: activity, action: action, reason: reason}) {
7882
+ return `Action "${activity}:${action}" is not allowed: ${actionDisabledDetail(reason)}`;
7883
+ }
7884
+
7885
+ class EditFieldDeniedError extends invariants.WorkflowError {
7886
+ reason;
7887
+ target;
7888
+ constructor(args) {
7889
+ super("edit-field-denied", formatEditDisabledReason(args.target, args.reason)),
7890
+ this.name = "EditFieldDeniedError", this.reason = args.reason, this.target = args.target;
7891
+ }
7892
+ }
7893
+
7894
+ const editDisabledReasonDetail = {
7895
+ "not-editable": () => "field is not declared editable",
7896
+ "instance-completed": r => `instance completed at ${r.completedAt}`,
7897
+ "instance-aborted": r => `instance aborted at ${r.abortedAt}`,
7898
+ "edit-window-closed": r => `edit window closed (${r.detail})`,
7899
+ "editor-not-permitted": r => `editor not permitted (${r.predicate})`
7900
+ };
7901
+
7902
+ function formatEditDisabledReason(target, reason) {
7903
+ const detail = editDisabledReasonDetail[reason.kind](reason), where = target.activity !== void 0 ? `${target.activity}.${target.field}` : target.field;
7904
+ return `Field "${target.scope}:${where}" is not editable: ${detail}`;
7905
+ }
7906
+
7907
+ async function fireAction(args) {
7908
+ const {client: client, instanceId: instanceId, activity: activity, action: action, params: params, requestRecord: requestRecord, options: options} = args;
7909
+ return retryOnRevisionConflict({
7910
+ client: client,
7911
+ instanceId: instanceId,
7912
+ options: options,
7913
+ commit: ctx => commitAction({
7914
+ ctx: ctx,
7915
+ activityName: activity,
7916
+ actionName: action,
7917
+ callerParams: params,
7918
+ requestRecord: requestRecord,
7919
+ options: options
7920
+ }),
7921
+ onExhausted: () => new ConcurrentFireActionError({
7922
+ instanceId: instanceId,
7923
+ activity: activity,
7924
+ action: action,
7925
+ attempts: CONCURRENT_COMMIT_MAX_ATTEMPTS
7926
+ })
7927
+ });
7928
+ }
7929
+
7930
+ async function resolveActionCommit({ctx: ctx, activityName: activityName, actionName: actionName, callerParams: callerParams, options: options}) {
7931
+ const actor = options?.actor, {stage: stage, activity: activity} = findActivityInCurrentStage(ctx, activityName), action = (activity.actions ?? []).find(a => a.name === actionName);
7331
7932
  if (action === void 0) throw new invariants.ContractViolationError(`Action "${actionName}" not declared on activity "${activityName}"`);
7332
7933
  if (action.when !== void 0) throw new ActionDisabledError({
7333
7934
  activity: activityName,
@@ -7338,7 +7939,8 @@ async function resolveActionCommit({ctx: ctx, activityName: activityName, action
7338
7939
  }
7339
7940
  });
7340
7941
  if (action.filter !== void 0) {
7341
- const can = await advisoryCanForCall({
7942
+ const vars = await callerBoundVarsForCall({
7943
+ client: ctx.client,
7342
7944
  instance: ctx.instance,
7343
7945
  options: options
7344
7946
  });
@@ -7350,10 +7952,8 @@ async function resolveActionCommit({ctx: ctx, activityName: activityName, action
7350
7952
  ...actor !== void 0 ? {
7351
7953
  actor: actor
7352
7954
  } : {},
7353
- ...can !== void 0 ? {
7354
- vars: {
7355
- can: can
7356
- }
7955
+ ...vars !== void 0 ? {
7956
+ vars: vars
7357
7957
  } : {}
7358
7958
  }
7359
7959
  })) throw new ActionDisabledError({
@@ -8614,334 +9214,91 @@ async function commitReport({ctx: ctx, effectKey: effectKey, claimToken: claimTo
8614
9214
  mutation: mutation,
8615
9215
  record: requestRecord,
8616
9216
  now: ctx.now
8617
- }), renewClaimLease({
8618
- mutation: mutation,
8619
- effectKey: effectKey,
8620
- now: ctx.now,
8621
- leaseMs: leaseMs
8622
- }), await runOps({
8623
- ops: validatedOps,
8624
- mutation: mutation,
8625
- stage: ctx.instance.currentStage,
8626
- origin: {
8627
- effect: pending.name
8628
- },
8629
- params: pending.params,
8630
- actor: ctx.actor,
8631
- self: invariants.selfGdr(ctx.instance),
8632
- now: ctx.now,
8633
- snapshot: ctx.snapshot,
8634
- refSurface: ctx.refSurface
8635
- }), await persistThenMaybeRefresh({
8636
- ctx: ctx,
8637
- mutation: mutation,
8638
- stageName: ctx.instance.currentStage,
8639
- didChangeState: !0
8640
- }), {
8641
- effectKey: effectKey,
8642
- effect: pending.name
8643
- };
8644
- }
8645
-
8646
- const RESET_ACTIVITY_TARGETS = [ "active", "skipped" ];
8647
-
8648
- function isResetActivityTarget(value) {
8649
- return RESET_ACTIVITY_TARGETS.includes(value);
8650
- }
8651
-
8652
- async function resetActivity(args) {
8653
- const {client: client, instanceId: instanceId, activity: activity, to: to, requestRecord: requestRecord, options: options} = args, ctx = await loadCallContext({
8654
- client: client,
8655
- instanceId: instanceId,
8656
- options: options
8657
- });
8658
- return commitResetActivity({
8659
- ctx: ctx,
8660
- activity: activity,
8661
- to: to,
8662
- requestRecord: requestRecord,
8663
- actor: options?.actor
8664
- });
8665
- }
8666
-
8667
- async function commitResetActivity({ctx: ctx, activity: activity, to: to, requestRecord: requestRecord, actor: actor}) {
8668
- if (assertRequestUnprocessed({
8669
- instance: ctx.instance,
8670
- record: requestRecord,
8671
- now: ctx.now
8672
- }), isTerminal(ctx)) return {
8673
- fired: !1
8674
- };
8675
- const mutation = startMutation(ctx.instance), openStage2 = findOpenStageEntry(mutation), entry = findCurrentActivityEntry(mutation, activity);
8676
- if (openStage2 === void 0 || entry === void 0) throw new invariants.ContractViolationError(`resetActivity: activity "${activity}" is not in the current stage of instance "${ctx.instance._id}"`);
8677
- const from = entry.status;
8678
- if (from === to) return {
8679
- fired: !1
8680
- };
8681
- if (!invariants.isTerminalActivityStatus(from)) throw new invariants.ContractViolationError(`resetActivity: activity "${activity}" is "${from}", not a terminal status — a reset only recovers a resolved (typically failed) activity. Use setStage to force past a live stage.`);
8682
- return recordProcessedRequest({
8683
- mutation: mutation,
8684
- record: requestRecord,
8685
- now: ctx.now
8686
- }), applyActivityStatusChange({
8687
- entry: entry,
8688
- history: mutation.history,
8689
- stage: openStage2.name,
8690
- to: to,
8691
- at: ctx.now,
8692
- ...actor !== void 0 ? {
8693
- actor: actor
8694
- } : {}
8695
- }), await persist(ctx, mutation), {
8696
- fired: !0,
8697
- stage: openStage2.name,
8698
- activity: activity,
8699
- from: from,
8700
- to: to
8701
- };
8702
- }
8703
-
8704
- const actorCache = /* @__PURE__ */ new WeakMap, grantsCache = /* @__PURE__ */ new WeakMap;
8705
-
8706
- async function resolveAccess(taggedClient, args = {}) {
8707
- const client = unwrapRequestTag(taggedClient), requestFn = lazyRequest(client);
8708
- if (requestFn === void 0) throw new invariants.ContractViolationError("workflow: no actor available. The engine resolves the actor from the client's token via `client.request({ uri: '/users/me' })`. Supply a real `@sanity/client` configured with a token (the test bench serves these endpoints per registered token).");
8709
- const grantsPromise = args.grantsFromPath !== void 0 ? cachedGrants({
8710
- client: client,
8711
- requestFn: requestFn,
8712
- resourcePath: args.grantsFromPath
8713
- }) : Promise.resolve(void 0), [identity, grants] = await Promise.all([ cachedActor(client, requestFn), grantsPromise ]);
8714
- if (identity === void 0) throw new invariants.ContractViolationError("workflow: failed to resolve actor from `/users/me`. The client is configured but the endpoint returned no usable identity — check the token.");
8715
- return {
8716
- actor: identity.actor,
8717
- ...identity.localPrincipalId !== void 0 ? {
8718
- localPrincipalId: identity.localPrincipalId
8719
- } : {},
8720
- ...grants !== void 0 ? {
8721
- grants: grants
8722
- } : {}
8723
- };
8724
- }
8725
-
8726
- function cachedActor(client, requestFn) {
8727
- const cached = actorCache.get(client);
8728
- if (cached !== void 0) return cached;
8729
- const pending = fetchActor(client, requestFn).catch(err => {
8730
- throw actorCache.get(client) === pending && actorCache.delete(client), err;
8731
- });
8732
- return actorCache.set(client, pending), pending;
8733
- }
8734
-
8735
- function grantsForClientPath(taggedClient, resourcePath) {
8736
- const client = unwrapRequestTag(taggedClient), requestFn = lazyRequest(client);
8737
- return requestFn === void 0 ? Promise.resolve(void 0) : cachedGrants({
8738
- client: client,
8739
- requestFn: requestFn,
8740
- resourcePath: resourcePath
8741
- });
8742
- }
8743
-
8744
- function lazyRequest(client) {
8745
- return client.request === void 0 ? void 0 : opts => client.request(opts);
8746
- }
8747
-
8748
- function cachedGrants({client: client, requestFn: requestFn, resourcePath: resourcePath}) {
8749
- let byPath = grantsCache.get(client);
8750
- byPath === void 0 && (byPath = /* @__PURE__ */ new Map, grantsCache.set(client, byPath));
8751
- let cached = byPath.get(resourcePath);
8752
- return cached === void 0 && (cached = fetchGrantsCached(requestFn, resourcePath),
8753
- byPath.set(resourcePath, cached)), cached;
8754
- }
8755
-
8756
- async function fetchActor(client, requestFn) {
8757
- const [resourceUser, globalUser] = await Promise.all([ fetchCurrentUser(requestFn, "the workflow resource host"), fetchGlobalUser(client) ]), resourceId = usableId(resourceUser), globalHostId = usableId(globalUser.user), carriedId = invariants.firstCarriedGlobalId([ globalHostId, resourceId ]), sessionId = carriedId ?? globalHostId ?? resourceId;
8758
- if (sessionId === void 0) return;
8759
- const bridge = carriedId === void 0 ? await bridgeProjectPrincipal({
8760
- client: client,
8761
- sessionId: sessionId,
8762
- resourceId: resourceId
8763
- }) : NO_BRIDGE, id = bridge.status === "resolved" ? bridge.globalId : sessionId;
8764
- refuseProjectScopedActor({
8765
- id: id,
8766
- globalUser: globalUser,
8767
- globalHostId: globalHostId,
8768
- bridgeFailure: bridge.status === "unavailable" ? bridge : void 0
8769
- });
8770
- const roleNames = roleNamesFor(resourceUser, globalUser);
8771
- return {
8772
- actor: {
8773
- kind: "person",
8774
- id: id,
8775
- ...roleNames.length > 0 ? {
8776
- roles: roleNames
8777
- } : {}
9217
+ }), renewClaimLease({
9218
+ mutation: mutation,
9219
+ effectKey: effectKey,
9220
+ now: ctx.now,
9221
+ leaseMs: leaseMs
9222
+ }), await runOps({
9223
+ ops: validatedOps,
9224
+ mutation: mutation,
9225
+ stage: ctx.instance.currentStage,
9226
+ origin: {
9227
+ effect: pending.name
8778
9228
  },
8779
- ...resourceId !== void 0 && resourceId !== id ? {
8780
- localPrincipalId: resourceId
8781
- } : {}
9229
+ params: pending.params,
9230
+ actor: ctx.actor,
9231
+ self: invariants.selfGdr(ctx.instance),
9232
+ now: ctx.now,
9233
+ snapshot: ctx.snapshot,
9234
+ refSurface: ctx.refSurface
9235
+ }), await persistThenMaybeRefresh({
9236
+ ctx: ctx,
9237
+ mutation: mutation,
9238
+ stageName: ctx.instance.currentStage,
9239
+ didChangeState: !0
9240
+ }), {
9241
+ effectKey: effectKey,
9242
+ effect: pending.name
8782
9243
  };
8783
9244
  }
8784
9245
 
8785
- function roleNamesFor(resourceUser, globalUser) {
8786
- return (resourceUser?.roles?.length ? resourceUser : globalUser.user)?.roles?.map(r => r.name).filter(n => !!n) ?? [];
8787
- }
8788
-
8789
- const NO_BRIDGE = {
8790
- status: "not-applicable"
8791
- };
9246
+ const RESET_ACTIVITY_TARGETS = [ "active", "skipped" ];
8792
9247
 
8793
- function globalRouteReason(globalUser, globalHostId) {
8794
- return "reason" in globalUser ? globalUser.reason : globalHostId === void 0 ? "no record" : `the global host answered with project-scoped principal "${globalHostId}"`;
9248
+ function isResetActivityTarget(value) {
9249
+ return RESET_ACTIVITY_TARGETS.includes(value);
8795
9250
  }
8796
9251
 
8797
- async function bridgeProjectPrincipal(args) {
8798
- const {client: client, sessionId: sessionId, resourceId: resourceId} = args;
8799
- return invariants.classifyPrincipalId(sessionId).namespace !== "project" ? NO_BRIDGE : resourceId === void 0 || invariants.classifyPrincipalId(resourceId).namespace !== "project" ? {
8800
- status: "unavailable",
8801
- reason: "no project-scoped principal from the resource host to address the directory with"
8802
- } : directoryGlobalId({
9252
+ async function resetActivity(args) {
9253
+ const {client: client, instanceId: instanceId, activity: activity, to: to, requestRecord: requestRecord, options: options} = args, ctx = await loadCallContext({
8803
9254
  client: client,
8804
- principalId: resourceId
9255
+ instanceId: instanceId,
9256
+ options: options
8805
9257
  });
8806
- }
8807
-
8808
- async function directoryGlobalId(args) {
8809
- const {client: client, principalId: principalId} = args, projectId = typeof client.config == "function" ? client.config().projectId : void 0;
8810
- if (projectId === void 0) return {
8811
- status: "unavailable",
8812
- reason: "the client reports no projectId, so its user directory has no address"
8813
- };
8814
- const lookup = await clientProjectUserDirectory(withRequestTag(client, REQUEST_TAG.accessResolveActor), projectId).findById(principalId);
8815
- if (lookup.status !== "resolved") return {
8816
- status: "unavailable",
8817
- reason: `project "${projectId}"'s user directory reported the principal ${lookup.status}`,
8818
- ...lookup.status === "inaccessible" && lookup.cause !== void 0 ? {
8819
- cause: lookup.cause
8820
- } : {}
8821
- };
8822
- const globalId = invariants.directoryBridgeId(lookup.user.sanityUserId);
8823
- return globalId === void 0 ? {
8824
- status: "unavailable",
8825
- reason: `project "${projectId}"'s user directory row for the principal carries no account-global sanityUserId`
8826
- } : {
8827
- status: "resolved",
8828
- globalId: globalId
8829
- };
8830
- }
8831
-
8832
- function refuseProjectScopedActor(args) {
8833
- const {id: id, globalUser: globalUser, globalHostId: globalHostId, bridgeFailure: bridgeFailure} = args;
8834
- if (invariants.classifyPrincipalId(id).namespace !== "project") return;
8835
- const routes = [ `The account-global record: ${globalRouteReason(globalUser, globalHostId)}.` ];
8836
- bridgeFailure !== void 0 && routes.push(`The project user directory: ${bridgeFailure.reason}.`);
8837
- const cause = bridgeFailure?.cause ?? ("cause" in globalUser ? globalUser.cause : void 0);
8838
- throw new Error(`workflow: the caller's identity resolves to the project-scoped principal "${id}" and its account-global id could not be resolved. ${routes.join(" ")} The engine speaks account-global user ids only and never acts as a project-scoped principal.`, cause === void 0 ? void 0 : {
8839
- cause: cause
9258
+ return commitResetActivity({
9259
+ ctx: ctx,
9260
+ activity: activity,
9261
+ to: to,
9262
+ requestRecord: requestRecord,
9263
+ actor: options?.actor
8840
9264
  });
8841
9265
  }
8842
9266
 
8843
- function usableId(user) {
8844
- if (!(!user || typeof user.id != "string" || user.id.length === 0)) return user.id;
8845
- }
8846
-
8847
- async function fetchCurrentUser(requestFn, hostDescription) {
8848
- try {
8849
- return await requestFn({
8850
- uri: "/users/me",
8851
- tag: REQUEST_TAG.accessResolveActor
8852
- });
8853
- } catch (err) {
8854
- throw new Error(`workflow: /users/me request against ${hostDescription} failed. The engine resolves the actor from the client's token via \`client.request({ uri: '/users/me' })\`. Check the token/connectivity.`, {
8855
- cause: err
8856
- });
8857
- }
8858
- }
8859
-
8860
- async function fetchGlobalUser(client) {
8861
- if (typeof client.withConfig != "function") return {
8862
- user: void 0,
8863
- reason: "the client cannot reach the global API host (no withConfig)"
8864
- };
8865
- let globalRequest, globalClient;
8866
- const hostResolution = resolveGlobalApiHost(client);
8867
- try {
8868
- globalClient = client.withConfig({
8869
- useProjectHostname: !1,
8870
- ...hostResolution.apiHost === void 0 ? {} : {
8871
- apiHost: hostResolution.apiHost
8872
- }
8873
- }), globalRequest = lazyRequest(globalClient);
8874
- } catch (err) {
8875
- return {
8876
- user: void 0,
8877
- reason: `building the global-host sibling client failed: ${invariants.errorMessage(err)}`,
8878
- cause: err
8879
- };
8880
- }
8881
- if (globalRequest === void 0) return {
8882
- user: void 0,
8883
- reason: "the global-host sibling client cannot issue requests"
8884
- };
8885
- try {
8886
- const user = await globalRequest({
8887
- uri: "/users/me",
8888
- tag: REQUEST_TAG.accessResolveActor
8889
- }), id = usableId(user);
8890
- return hostResolution.requireGlobalPrincipal && id !== void 0 && invariants.classifyPrincipalId(id).namespace === "project" ? {
8891
- user: void 0,
8892
- reason: `global-host /users/me returned project-scoped principal "${id}"; the sibling client is still bound to a project API host`
8893
- } : {
8894
- user: user
8895
- };
8896
- } catch (err) {
8897
- return {
8898
- user: void 0,
8899
- reason: `global-host /users/me failed: ${invariants.errorMessage(err)}`,
8900
- cause: err
8901
- };
8902
- }
8903
- }
8904
-
8905
- function resolveGlobalApiHost(client) {
8906
- if (typeof client.config != "function") return {
8907
- requireGlobalPrincipal: !1
9267
+ async function commitResetActivity({ctx: ctx, activity: activity, to: to, requestRecord: requestRecord, actor: actor}) {
9268
+ if (assertRequestUnprocessed({
9269
+ instance: ctx.instance,
9270
+ record: requestRecord,
9271
+ now: ctx.now
9272
+ }), isTerminal(ctx)) return {
9273
+ fired: !1
8908
9274
  };
8909
- const apiHost = client.config().apiHost;
8910
- if (apiHost === void 0) return {
8911
- requireGlobalPrincipal: !1
9275
+ const mutation = startMutation(ctx.instance), openStage2 = findOpenStageEntry(mutation), entry = findCurrentActivityEntry(mutation, activity);
9276
+ if (openStage2 === void 0 || entry === void 0) throw new invariants.ContractViolationError(`resetActivity: activity "${activity}" is not in the current stage of instance "${ctx.instance._id}"`);
9277
+ const from = entry.status;
9278
+ if (from === to) return {
9279
+ fired: !1
8912
9280
  };
8913
- let parsed;
8914
- try {
8915
- parsed = new URL(apiHost);
8916
- } catch {
8917
- return {
8918
- requireGlobalPrincipal: !0
8919
- };
8920
- }
8921
- if (/^api\.sanity\.(io|work)$/.test(parsed.hostname)) return {
8922
- requireGlobalPrincipal: !1
9281
+ if (!invariants.isTerminalActivityStatus(from)) throw new invariants.ContractViolationError(`resetActivity: activity "${activity}" is "${from}", not a terminal status — a reset only recovers a resolved (typically failed) activity. Use setStage to force past a live stage.`);
9282
+ return recordProcessedRequest({
9283
+ mutation: mutation,
9284
+ record: requestRecord,
9285
+ now: ctx.now
9286
+ }), applyActivityStatusChange({
9287
+ entry: entry,
9288
+ history: mutation.history,
9289
+ stage: openStage2.name,
9290
+ to: to,
9291
+ at: ctx.now,
9292
+ ...actor !== void 0 ? {
9293
+ actor: actor
9294
+ } : {}
9295
+ }), await persist(ctx, mutation), {
9296
+ fired: !0,
9297
+ stage: openStage2.name,
9298
+ activity: activity,
9299
+ from: from,
9300
+ to: to
8923
9301
  };
8924
- const match = /^[^.]+\.api\.sanity\.(io|work)$/.exec(parsed.hostname);
8925
- return match === null ? {
8926
- requireGlobalPrincipal: !0
8927
- } : (parsed.hostname = `api.sanity.${match[1]}`, {
8928
- apiHost: parsed.origin,
8929
- requireGlobalPrincipal: !0
8930
- });
8931
- }
8932
-
8933
- async function fetchGrantsCached(requestFn, resourcePath) {
8934
- try {
8935
- return await fetchGrants({
8936
- client: {
8937
- request: requestFn
8938
- },
8939
- resourcePath: resourcePath
8940
- });
8941
- } catch (err) {
8942
- console.warn(`workflow: failed to fetch grants from "${resourcePath}"; advisory permission reads that depend on them are skipped — a rendered $can stays undefined (conditions referencing it fail closed) and the subject-write forecast omits this resource. The lake still enforces writes. Original error: ${invariants.errorMessage(err)}`);
8943
- return;
8944
- }
8945
9302
  }
8946
9303
 
8947
9304
  const REPLAY_SURFACE = {
@@ -9125,7 +9482,7 @@ async function resourceActorId(client) {
9125
9482
  async function evaluateInstance(args) {
9126
9483
  const {client: client, tag: tag, workflowResource: workflowResource, instanceId: instanceId, resourceClients: resourceClients} = args, now = (args.clock ?? wallClock)();
9127
9484
  invariants.validateTag(tag);
9128
- const [access, instance] = await Promise.all([ resolveAccess(client, {
9485
+ const [access, instance, attributes] = await Promise.all([ resolveAccess(client, {
9129
9486
  ...args.grantsFromPath !== void 0 ? {
9130
9487
  grantsFromPath: args.grantsFromPath
9131
9488
  } : {}
@@ -9133,7 +9490,7 @@ async function evaluateInstance(args) {
9133
9490
  client: client,
9134
9491
  instanceId: instanceId,
9135
9492
  tag: tag
9136
- }) ]), {actor: actor, grants: grants, localPrincipalId: localPrincipalId} = access, definition = parseDefinitionSnapshot(instance), clientForGdr = buildClientForGdr({
9493
+ }), resolveUserAttributes(client) ]), {actor: actor, grants: grants, localPrincipalId: localPrincipalId} = access, definition = parseDefinitionSnapshot(instance), clientForGdr = buildClientForGdr({
9137
9494
  client: client,
9138
9495
  workflowResource: workflowResource,
9139
9496
  resourceClients: resourceClients
@@ -9158,6 +9515,9 @@ async function evaluateInstance(args) {
9158
9515
  } : {},
9159
9516
  ...grants !== void 0 ? {
9160
9517
  grants: grants
9518
+ } : {},
9519
+ ...attributes !== void 0 ? {
9520
+ attributes: attributes
9161
9521
  } : {}
9162
9522
  });
9163
9523
  }
@@ -9172,6 +9532,33 @@ function memoizedByName(render) {
9172
9532
  };
9173
9533
  }
9174
9534
 
9535
+ async function callerBoundProjectionScopes(args) {
9536
+ const can = await advisoryCan({
9537
+ instance: args.instance,
9538
+ identity: args.identity,
9539
+ grants: args.grants
9540
+ }), vars = callerBoundVars({
9541
+ ...can !== void 0 ? {
9542
+ can: can
9543
+ } : {},
9544
+ ...args.attributes !== void 0 ? {
9545
+ attributes: args.attributes
9546
+ } : {}
9547
+ }), opts = {
9548
+ actor: args.actor,
9549
+ ...vars !== void 0 ? {
9550
+ vars: vars
9551
+ } : {}
9552
+ };
9553
+ return {
9554
+ scope: await renderConditionScope(args.scopeSource, opts),
9555
+ scopeForActivity: memoizedByName(activityName => renderConditionScope(args.scopeSource, {
9556
+ activityName: activityName,
9557
+ ...opts
9558
+ }))
9559
+ };
9560
+ }
9561
+
9175
9562
  function currentStageOf(instance, definition) {
9176
9563
  try {
9177
9564
  return findStage(definition, instance.currentStage);
@@ -9197,7 +9584,7 @@ async function explainSite(args) {
9197
9584
  }
9198
9585
 
9199
9586
  async function evaluateFromSnapshot(args) {
9200
- const {instance: instance, definition: definition, actor: actor, grants: grants, snapshot: snapshot} = args, now = args.now ?? wallClock(), stage = currentStageOf(instance, definition), autonomy = autonomyOf(definition), stageAutonomy2 = stageAutonomyOf(autonomy, stage.name), fireConsequence = (activity, action) => whatIfFireAction({
9587
+ const {instance: instance, definition: definition, actor: actor, grants: grants, attributes: attributes, snapshot: snapshot} = args, now = args.now ?? wallClock(), stage = currentStageOf(instance, definition), autonomy = autonomyOf(definition), stageAutonomy2 = stageAutonomyOf(autonomy, stage.name), fireConsequence = (activity, action) => whatIfFireAction({
9201
9588
  instance: instance,
9202
9589
  definition: definition,
9203
9590
  snapshot: snapshot,
@@ -9214,22 +9601,14 @@ async function evaluateFromSnapshot(args) {
9214
9601
  }, anchorIdentity = invariants.lakePrincipalId({
9215
9602
  actor: actor,
9216
9603
  localPrincipalId: args.localPrincipalId
9217
- }), can = await advisoryCan({
9218
- instance: instance,
9219
- identity: anchorIdentity,
9220
- grants: grants
9221
- }), scope = await renderConditionScope(scopeSource, {
9222
- actor: actor,
9223
- vars: {
9224
- can: can
9225
- }
9226
- }), scopeForActivity = memoizedByName(activityName => renderConditionScope(scopeSource, {
9227
- activityName: activityName,
9604
+ }), {scope: scope, scopeForActivity: scopeForActivity} = await callerBoundProjectionScopes({
9605
+ scopeSource: scopeSource,
9228
9606
  actor: actor,
9229
- vars: {
9230
- can: can
9231
- }
9232
- })), cascadeScopeForActivity = memoizedByName(activityName => renderConditionScope(scopeSource, {
9607
+ instance: instance,
9608
+ grants: grants,
9609
+ attributes: attributes,
9610
+ identity: anchorIdentity
9611
+ }), cascadeScopeForActivity = memoizedByName(activityName => renderConditionScope(scopeSource, {
9233
9612
  activityName: activityName
9234
9613
  })), currentActivityEntries = findOpenStageEntry(instance)?.activities ?? [], guardDenial = await instanceGuardReason({
9235
9614
  instance: instance,
@@ -9278,6 +9657,7 @@ async function evaluateFromSnapshot(args) {
9278
9657
  }
9279
9658
  const currentStage = {
9280
9659
  stage: stage,
9660
+ ...semanticsOf(stage),
9281
9661
  activities: activityEvaluations,
9282
9662
  transitions: transitionEvaluations,
9283
9663
  autonomy: stageAutonomy2
@@ -9294,6 +9674,7 @@ async function evaluateFromSnapshot(args) {
9294
9674
  return {
9295
9675
  instance: instance,
9296
9676
  definition: definition,
9677
+ ...semanticsOf(definition),
9297
9678
  actor: actor,
9298
9679
  currentStage: currentStage,
9299
9680
  pendingOnYou: pendingOnYou,
@@ -9449,6 +9830,7 @@ async function evaluateActivity(args) {
9449
9830
  }));
9450
9831
  return {
9451
9832
  activity: activity,
9833
+ ...semanticsOf(activity),
9452
9834
  status: status,
9453
9835
  kind: invariants.deriveActivityKind(activity),
9454
9836
  classification: invariants.deriveExecutorClassification(activity),
@@ -9619,9 +10001,13 @@ function fireableActionVerdict({args: args, insights: insights}) {
9619
10001
  function actionEvaluationIdentity(action) {
9620
10002
  return {
9621
10003
  action: action,
9622
- ...action.semantics !== void 0 ? {
9623
- semantics: action.semantics
9624
- } : {}
10004
+ ...semanticsOf(action)
10005
+ };
10006
+ }
10007
+
10008
+ function semanticsOf(node) {
10009
+ return node.semantics === void 0 ? {} : {
10010
+ semantics: node.semantics
9625
10011
  };
9626
10012
  }
9627
10013
 
@@ -10100,7 +10486,8 @@ async function commitEdit({ctx: ctx, target: target, mode: mode, value: value, r
10100
10486
  }
10101
10487
 
10102
10488
  async function assertFieldEditable({ctx: ctx, site: site, options: options}) {
10103
- const actor = options?.actor, window = fieldWindowOpen(ctx.instance, site), can = await advisoryCanForCall({
10489
+ const actor = options?.actor, window = fieldWindowOpen(ctx.instance, site), vars = await callerBoundVarsForCall({
10490
+ client: ctx.client,
10104
10491
  instance: ctx.instance,
10105
10492
  options: options
10106
10493
  }), predicateSatisfied = window.open && site.effective !== !0 && site.effective !== void 0 ? await ctxEvaluateCondition({
@@ -10113,10 +10500,8 @@ async function assertFieldEditable({ctx: ctx, site: site, options: options}) {
10113
10500
  ...actor !== void 0 ? {
10114
10501
  actor: actor
10115
10502
  } : {},
10116
- ...can !== void 0 ? {
10117
- vars: {
10118
- can: can
10119
- }
10503
+ ...vars !== void 0 ? {
10504
+ vars: vars
10120
10505
  } : {}
10121
10506
  }
10122
10507
  }) : site.effective === !0, reason = editDisabledReason({
@@ -10606,6 +10991,9 @@ function buildEngineCallOptions(args) {
10606
10991
  ...args.grants !== void 0 ? {
10607
10992
  grants: args.grants
10608
10993
  } : {},
10994
+ ...args.attributes !== void 0 ? {
10995
+ attributes: args.attributes
10996
+ } : {},
10609
10997
  ...args.clock !== void 0 ? {
10610
10998
  clock: args.clock
10611
10999
  } : {},
@@ -12364,7 +12752,7 @@ function createInstanceSession(args) {
12364
12752
  refSurface: evalScope.refSurface
12365
12753
  });
12366
12754
  }, evaluateWith = async ({held: held, guards: guards, self: self}) => {
12367
- const {actor: actor, localPrincipalId: localPrincipalId, grants: grants} = await access(), resourceGrants = await subjectResourceGrants({
12755
+ const [{actor: actor, localPrincipalId: localPrincipalId, grants: grants}, attributes] = await Promise.all([ access(), resolveUserAttributes(client) ]), resourceGrants = await subjectResourceGrants({
12368
12756
  clientForGdr: evalScope.clientForGdr,
12369
12757
  instance: instance
12370
12758
  }), normalizedSelf = await normalizeInstanceIdentities({
@@ -12386,6 +12774,9 @@ function createInstanceSession(args) {
12386
12774
  } : {},
12387
12775
  ...grants !== void 0 ? {
12388
12776
  grants: grants
12777
+ } : {},
12778
+ ...attributes !== void 0 ? {
12779
+ attributes: attributes
12389
12780
  } : {}
12390
12781
  });
12391
12782
  }, settleAfterApply = async ({actor: actor, held: held, ranOps: ranOps, scope: scope}) => {
@@ -13081,7 +13472,7 @@ const HISTORY_DISPLAY = {
13081
13472
  },
13082
13473
  opApplied: {
13083
13474
  title: "Op applied",
13084
- description: "An inline field-mutation op (field.set / field.append / status.set / etc.) ran during an action commit."
13475
+ description: "An action, direct edit, or effect completion executed an operation and recorded its audit payload."
13085
13476
  },
13086
13477
  fieldQueryDiscarded: {
13087
13478
  title: "Query result discarded",
@@ -13186,6 +13577,10 @@ const HISTORY_DISPLAY = {
13186
13577
  title: "Set field entry",
13187
13578
  description: "Overwrite a field entry's value with a resolved value expression."
13188
13579
  },
13580
+ "field.setIfMissing": {
13581
+ title: "Set field entry if missing",
13582
+ description: "Set a nullable field entry's value only when it is currently null or undefined; array, assignees, and doc.refs entries are always arrays and are not supported."
13583
+ },
13189
13584
  "field.unset": {
13190
13585
  title: "Unset field entry",
13191
13586
  description: "Reset a field entry to its default (null for scalars, [] for array kinds)."
@@ -13194,6 +13589,14 @@ const HISTORY_DISPLAY = {
13194
13589
  title: "Append to field entry",
13195
13590
  description: "Push a resolved item onto an array-kind entry (array, assignees, doc.refs)."
13196
13591
  },
13592
+ "field.inc": {
13593
+ title: "Increment field entry",
13594
+ description: "Add a resolved numeric delta, defaulting to 1, to a number entry that already holds a finite number."
13595
+ },
13596
+ "field.dec": {
13597
+ title: "Decrement field entry",
13598
+ description: "Subtract a resolved numeric delta, defaulting to 1, from a number entry that already holds a finite number."
13599
+ },
13197
13600
  "field.updateWhere": {
13198
13601
  title: "Update matching rows",
13199
13602
  description: "Merge declared sub-fields into rows of an `array` entry that match the `where` condition (`array` only — `doc.refs`/`assignees` rows are engine-shaped)."
@@ -13319,6 +13722,8 @@ exports.CONDITION_VARS = invariants.CONDITION_VARS;
13319
13722
 
13320
13723
  exports.ContractViolationError = invariants.ContractViolationError;
13321
13724
 
13725
+ exports.DECISION_SEMANTICS = invariants.DECISION_SEMANTICS;
13726
+
13322
13727
  exports.DEFAULT_TRANSITION_WHEN = invariants.DEFAULT_TRANSITION_WHEN;
13323
13728
 
13324
13729
  exports.DRIVER_KINDS = invariants.DRIVER_KINDS;
@@ -13345,6 +13750,8 @@ exports.PersistedDocShapeError = invariants.PersistedDocShapeError;
13345
13750
 
13346
13751
  exports.RESERVED_CONDITION_VARS = invariants.RESERVED_CONDITION_VARS;
13347
13752
 
13753
+ exports.SIGNAL_SEMANTICS = invariants.SIGNAL_SEMANTICS;
13754
+
13348
13755
  exports.START_FILTER_VARS = invariants.START_FILTER_VARS;
13349
13756
 
13350
13757
  exports.START_REQUIREMENT_VARS = invariants.START_REQUIREMENT_VARS;
@@ -13777,6 +14184,8 @@ exports.documentActionDenials = documentActionDenials;
13777
14184
 
13778
14185
  exports.documentPrefilter = documentPrefilter;
13779
14186
 
14187
+ exports.documentStuckCause = documentStuckCause;
14188
+
13780
14189
  exports.effectOutputsMap = effectOutputsMap;
13781
14190
 
13782
14191
  exports.entryDocRefs = entryDocRefs;
@@ -13793,10 +14202,14 @@ exports.explainStartRequirement = explainStartRequirement;
13793
14202
 
13794
14203
  exports.fieldTreeShape = fieldTreeShape;
13795
14204
 
14205
+ exports.findActivityNode = findActivityNode;
14206
+
13796
14207
  exports.findCurrentActivityEntry = findCurrentActivityEntry;
13797
14208
 
13798
14209
  exports.findOpenStageEntry = findOpenStageEntry;
13799
14210
 
14211
+ exports.findStageNode = findStageNode;
14212
+
13800
14213
  exports.groupSitesOf = groupSitesOf;
13801
14214
 
13802
14215
  exports.guardMatches = guardMatches;
@@ -13899,6 +14312,8 @@ exports.resolveClientActor = resolveClientActor;
13899
14312
 
13900
14313
  exports.resolveFieldEntry = resolveFieldEntry$1;
13901
14314
 
14315
+ exports.resolveUserAttributes = resolveUserAttributes;
14316
+
13902
14317
  exports.retractStageGuards = retractStageGuards;
13903
14318
 
13904
14319
  exports.silentLogger = silentLogger;