@voltro/protocol 0.80.0 → 0.82.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/inspect.d.ts CHANGED
@@ -2,9 +2,9 @@ import { ParseOptions } from 'effect/SchemaAST';
2
2
  import { Schema } from 'effect';
3
3
 
4
4
  export declare const decodeInspectBackfillInput: (u: unknown, overrideOptions?: ParseOptions) => {
5
- readonly from: string;
6
5
  readonly name: string;
7
6
  readonly limit: number;
7
+ readonly from: string;
8
8
  readonly to: string;
9
9
  readonly requestId: string;
10
10
  readonly confirm: boolean;
@@ -78,6 +78,72 @@ export declare type InspectReplicaSelection = {
78
78
  readonly replicaIds: ReadonlyArray<string>;
79
79
  };
80
80
 
81
+ export declare type InspectWorkflowExecutionObservation = typeof InspectWorkflowExecutionObservationSchema.Type;
82
+
83
+ /** A projection of engine evidence, not a run-row or recorder-entry claim.
84
+ * Deliberately excludes payloads, outputs, raw causes and journal identifiers. */
85
+ export declare const InspectWorkflowExecutionObservationSchema: Schema.Union<[Schema.Struct<{
86
+ kind: Schema.Literal<["not-observed"]>;
87
+ workflowName: Schema.filter<Schema.filter<typeof Schema.NonEmptyString>>;
88
+ executionId: Schema.filter<Schema.filter<typeof Schema.NonEmptyString>>;
89
+ }>, Schema.Struct<{
90
+ kind: Schema.Literal<["pending"]>;
91
+ workflowName: Schema.filter<Schema.filter<typeof Schema.NonEmptyString>>;
92
+ executionId: Schema.filter<Schema.filter<typeof Schema.NonEmptyString>>;
93
+ }>, Schema.Struct<{
94
+ kind: Schema.Literal<["delivery-failed"]>;
95
+ workflowName: Schema.filter<Schema.filter<typeof Schema.NonEmptyString>>;
96
+ executionId: Schema.filter<Schema.filter<typeof Schema.NonEmptyString>>;
97
+ }>, Schema.Struct<{
98
+ kind: Schema.Literal<["suspended"]>;
99
+ workflowName: Schema.filter<Schema.filter<typeof Schema.NonEmptyString>>;
100
+ executionId: Schema.filter<Schema.filter<typeof Schema.NonEmptyString>>;
101
+ }>, Schema.Struct<{
102
+ kind: Schema.Literal<["completed"]>;
103
+ outcome: Schema.Literal<["succeeded", "failed", "cancelled"]>;
104
+ workflowName: Schema.filter<Schema.filter<typeof Schema.NonEmptyString>>;
105
+ executionId: Schema.filter<Schema.filter<typeof Schema.NonEmptyString>>;
106
+ }>]>;
107
+
108
+ export declare type InspectWorkflowExecutionTarget = typeof InspectWorkflowExecutionTargetSchema.Type;
109
+
110
+ /** Read an existing execution identity; this is never a start command. */
111
+ export declare const InspectWorkflowExecutionTargetSchema: Schema.Struct<{
112
+ workflowName: Schema.filter<Schema.filter<typeof Schema.NonEmptyString>>;
113
+ executionId: Schema.filter<Schema.filter<typeof Schema.NonEmptyString>>;
114
+ }>;
115
+
116
+ export declare type InspectWorkflowQueuedStartObservation = typeof InspectWorkflowQueuedStartObservationSchema.Type;
117
+
118
+ /** Read-only admission evidence. Accepted means an execution exists, not that it succeeded. */
119
+ export declare const InspectWorkflowQueuedStartObservationSchema: Schema.Union<[Schema.Struct<{
120
+ kind: Schema.Literal<["pending"]>;
121
+ workflowName: Schema.filter<Schema.filter<typeof Schema.NonEmptyString>>;
122
+ receiptId: Schema.filter<Schema.filter<typeof Schema.NonEmptyString>>;
123
+ tenantId: Schema.NullOr<Schema.filter<Schema.filter<typeof Schema.NonEmptyString>>>;
124
+ }>, Schema.Struct<{
125
+ kind: Schema.Literal<["accepted"]>;
126
+ executionId: Schema.filter<Schema.filter<typeof Schema.NonEmptyString>>;
127
+ workflowName: Schema.filter<Schema.filter<typeof Schema.NonEmptyString>>;
128
+ receiptId: Schema.filter<Schema.filter<typeof Schema.NonEmptyString>>;
129
+ tenantId: Schema.NullOr<Schema.filter<Schema.filter<typeof Schema.NonEmptyString>>>;
130
+ }>, Schema.Struct<{
131
+ kind: Schema.Literal<["not-executed"]>;
132
+ reason: Schema.Literal<["dropped", "skipped", "superseded", "cancelled", "abandoned", "rejected"]>;
133
+ workflowName: Schema.filter<Schema.filter<typeof Schema.NonEmptyString>>;
134
+ receiptId: Schema.filter<Schema.filter<typeof Schema.NonEmptyString>>;
135
+ tenantId: Schema.NullOr<Schema.filter<Schema.filter<typeof Schema.NonEmptyString>>>;
136
+ }>]>;
137
+
138
+ export declare type InspectWorkflowQueuedStartTarget = typeof InspectWorkflowQueuedStartTargetSchema.Type;
139
+
140
+ /** A caller receipt, never an execution or a reusable admission slot. */
141
+ export declare const InspectWorkflowQueuedStartTargetSchema: Schema.Struct<{
142
+ workflowName: Schema.filter<Schema.filter<typeof Schema.NonEmptyString>>;
143
+ receiptId: Schema.filter<Schema.filter<typeof Schema.NonEmptyString>>;
144
+ tenantId: Schema.NullOr<Schema.filter<Schema.filter<typeof Schema.NonEmptyString>>>;
145
+ }>;
146
+
81
147
  export declare class InvalidReplicaSelection extends Error {
82
148
  readonly name = "InvalidReplicaSelection";
83
149
  }
package/dist/inspect.js CHANGED
@@ -1,85 +1,119 @@
1
1
  import { Schema as e } from "effect";
2
- //#region src/inspectBackfill.ts
3
- var t = e.String.pipe(e.filter((e) => Number.isFinite(Date.parse(e)))), n = e.String.pipe(e.minLength(1), e.maxLength(256)), r = e.Struct({
4
- id: n,
5
- name: n,
6
- from: t,
7
- to: t,
2
+ //#region src/inspectWorkflowQueuedStart.ts
3
+ var t = e.NonEmptyString.pipe(e.maxLength(1024), e.filter((e) => e.trim().length > 0 && !/[\u0000-\u001f\u007f]/.test(e))), n = e.Struct({
4
+ workflowName: t,
5
+ receiptId: t,
6
+ tenantId: e.NullOr(t)
7
+ }), r = n.fields, i = e.Union(e.Struct({
8
+ ...r,
9
+ kind: e.Literal("pending")
10
+ }), e.Struct({
11
+ ...r,
12
+ kind: e.Literal("accepted"),
13
+ executionId: t
14
+ }), e.Struct({
15
+ ...r,
16
+ kind: e.Literal("not-executed"),
17
+ reason: e.Literal("dropped", "skipped", "superseded", "cancelled", "abandoned", "rejected")
18
+ })), a = e.String.pipe(e.filter((e) => Number.isFinite(Date.parse(e)))), o = e.String.pipe(e.minLength(1), e.maxLength(256)), s = e.Struct({
19
+ id: o,
20
+ name: o,
21
+ from: a,
22
+ to: a,
8
23
  occurrences: e.Array(e.Number),
9
24
  status: e.Literal("queued", "running", "completed", "interrupted"),
10
25
  owner: e.NullOr(e.String),
11
- createdAt: t,
12
- updatedAt: t,
26
+ createdAt: a,
27
+ updatedAt: a,
13
28
  cursor: e.Number,
14
29
  activeIndex: e.NullOr(e.Number),
15
30
  results: e.Array(e.Struct({
16
- runId: n,
31
+ runId: o,
17
32
  status: e.Literal("succeeded", "failed", "skipped", "missed")
18
33
  })),
19
34
  error: e.NullOr(e.String)
20
- }), i = e.Struct({
21
- requestId: n,
22
- name: n,
23
- from: t,
24
- to: t,
35
+ }), c = e.Struct({
36
+ requestId: o,
37
+ name: o,
38
+ from: a,
39
+ to: a,
25
40
  confirm: e.Boolean,
26
41
  limit: e.Number.pipe(e.int(), e.between(1, 1e4))
27
- }), a = e.decodeUnknownSync(i), o = e.Union(e.Struct({
42
+ }), l = e.decodeUnknownSync(c), u = e.Union(e.Struct({
28
43
  kind: e.Literal("accepted"),
29
- request: r
44
+ request: s
30
45
  }), e.Struct({
31
46
  kind: e.Literal("refused"),
32
- name: n,
47
+ name: o,
33
48
  occurrences: e.Number,
34
49
  reason: e.String
35
- })), s = class extends Error {
50
+ })), d = class extends Error {
36
51
  name = "ScheduleBackfillRequestConflict";
37
- }, c = (e) => typeof e == "object" && e && !Array.isArray(e) ? e : void 0, l = (e) => Array.isArray(e) && e.every((e) => c(e) !== void 0) ? e : void 0, u = (e) => typeof e == "number" && Number.isFinite(e) ? e : 0, d = /* @__PURE__ */ new Set([
52
+ }, f = e.NonEmptyString.pipe(e.maxLength(1024), e.filter((e) => e.trim().length > 0 && !/[\u0000-\u001f\u007f]/.test(e))), p = e.Struct({
53
+ workflowName: f,
54
+ executionId: f
55
+ }), m = p.fields, h = e.Union(e.Struct({
56
+ ...m,
57
+ kind: e.Literal("not-observed")
58
+ }), e.Struct({
59
+ ...m,
60
+ kind: e.Literal("pending")
61
+ }), e.Struct({
62
+ ...m,
63
+ kind: e.Literal("delivery-failed")
64
+ }), e.Struct({
65
+ ...m,
66
+ kind: e.Literal("suspended")
67
+ }), e.Struct({
68
+ ...m,
69
+ kind: e.Literal("completed"),
70
+ outcome: e.Literal("succeeded", "failed", "cancelled")
71
+ })), g = (e) => typeof e == "object" && e && !Array.isArray(e) ? e : void 0, _ = (e) => Array.isArray(e) && e.every((e) => g(e) !== void 0) ? e : void 0, v = (e) => typeof e == "number" && Number.isFinite(e) ? e : 0, y = /* @__PURE__ */ new Set([
38
72
  "counter",
39
73
  "gauge",
40
74
  "histogram",
41
75
  "summary",
42
76
  "frequency"
43
- ]), f = (e) => {
44
- let t = l(e);
45
- return t?.every((e) => typeof e.name == "string" && d.has(String(e.type))) ? t : void 0;
46
- }, p = (e) => {
47
- let t = c(e), n = c(t?.data), r = l(n?.results);
77
+ ]), b = (e) => {
78
+ let t = _(e);
79
+ return t?.every((e) => typeof e.name == "string" && y.has(String(e.type))) ? t : void 0;
80
+ }, x = (e) => {
81
+ let t = g(e), n = g(t?.data), r = _(n?.results);
48
82
  if (!t || !n?.selection || !r) return e;
49
83
  let i = r.filter((e) => e.status === "answered").map((e) => ({
50
84
  replicaId: String(e.replicaId),
51
- observation: c(e.observation),
52
- data: c(e.observation)?.data
85
+ observation: g(e.observation),
86
+ data: g(e.observation)?.data
53
87
  }));
54
88
  if (i.length === 0) throw Error("None of the selected replicas answered. Check replica availability and retry.");
55
89
  let a = i.map((e) => ({
56
90
  ...e,
57
- object: c(e.data)
58
- })), o = (e) => a.reduce((t, n) => t + u(n.object?.[e]), 0), s = Math.max(...i.map((e) => u(e.observation?.capturedAt))), d = {
91
+ object: g(e.data)
92
+ })), o = (e) => a.reduce((t, n) => t + v(n.object?.[e]), 0), s = Math.max(...i.map((e) => v(e.observation?.capturedAt))), c = {
59
93
  capturedAt: s,
60
94
  bufferSize: o("bufferSize"),
61
95
  bufferCapacity: o("bufferCapacity")
62
- }, p;
63
- if (a.every((e) => l(e.object?.records))) {
64
- let e = a.flatMap((e) => l(e.object?.records).map((t) => ({
96
+ }, l;
97
+ if (a.every((e) => _(e.object?.records))) {
98
+ let e = a.flatMap((e) => _(e.object?.records).map((t) => ({
65
99
  ...t,
66
100
  fields: {
67
- ...c(t.fields),
101
+ ...g(t.fields),
68
102
  "voltro.replica": e.replicaId
69
103
  }
70
- }))).sort((e, t) => u(e.ts) - u(t.ts));
71
- p = {
72
- ...d,
104
+ }))).sort((e, t) => v(e.ts) - v(t.ts));
105
+ l = {
106
+ ...c,
73
107
  returned: e.length,
74
108
  records: e
75
109
  };
76
- } else if (a.every((e) => l(e.object?.traces))) {
110
+ } else if (a.every((e) => _(e.object?.traces))) {
77
111
  let e = /* @__PURE__ */ new Map();
78
- for (let t of a) for (let n of l(t.object?.traces)) {
79
- let r = String(n.traceId), i = (l(n.spans) ?? []).map((e) => ({
112
+ for (let t of a) for (let n of _(t.object?.traces)) {
113
+ let r = String(n.traceId), i = (_(n.spans) ?? []).map((e) => ({
80
114
  ...e,
81
115
  attributes: {
82
- ...c(e.attributes),
116
+ ...g(e.attributes),
83
117
  "voltro.replica": t.replicaId
84
118
  }
85
119
  })), a = e.get(r) ?? [];
@@ -89,7 +123,7 @@ var t = e.String.pipe(e.filter((e) => Number.isFinite(Date.parse(e)))), n = e.St
89
123
  }), e.set(r, a);
90
124
  }
91
125
  let t = [...e.values()].map((e) => {
92
- let t = e.flatMap((e) => l(e.spans) ?? []).sort((e, t) => u(e.startMs) - u(t.startMs)), n = [...e].sort((e, t) => u(e.startMs) - u(t.startMs))[0], r = Math.min(...e.map((e) => u(e.startMs))), i = Math.max(...e.map((e) => u(e.startMs) + u(e.durationMs)));
126
+ let t = e.flatMap((e) => _(e.spans) ?? []).sort((e, t) => v(e.startMs) - v(t.startMs)), n = [...e].sort((e, t) => v(e.startMs) - v(t.startMs))[0], r = Math.min(...e.map((e) => v(e.startMs))), i = Math.max(...e.map((e) => v(e.startMs) + v(e.durationMs)));
93
127
  return {
94
128
  ...n,
95
129
  startMs: r,
@@ -97,65 +131,65 @@ var t = e.String.pipe(e.filter((e) => Number.isFinite(Date.parse(e)))), n = e.St
97
131
  spanCount: t.length,
98
132
  hasError: e.some((e) => e.hasError === !0),
99
133
  spans: t,
100
- ...e.some((e) => e.deliveryCount !== void 0) ? { deliveryCount: e.reduce((e, t) => e + u(t.deliveryCount), 0) } : {},
101
- ...e.some((e) => e.lastDeliveryMs !== void 0) ? { lastDeliveryMs: Math.max(...e.map((e) => u(e.lastDeliveryMs))) } : {}
134
+ ...e.some((e) => e.deliveryCount !== void 0) ? { deliveryCount: e.reduce((e, t) => e + v(t.deliveryCount), 0) } : {},
135
+ ...e.some((e) => e.lastDeliveryMs !== void 0) ? { lastDeliveryMs: Math.max(...e.map((e) => v(e.lastDeliveryMs))) } : {}
102
136
  };
103
137
  }).sort((e, t) => t.startMs - e.startMs);
104
- p = {
105
- ...d,
138
+ l = {
139
+ ...c,
106
140
  returned: t.length,
107
141
  traces: t
108
142
  };
109
- } else if (a.every((e) => l(e.object?.buckets) && typeof e.object?.windowMs == "number")) p = {
143
+ } else if (a.every((e) => _(e.object?.buckets) && typeof e.object?.windowMs == "number")) l = {
110
144
  capturedAt: s,
111
- windowMs: Math.max(...a.map((e) => u(e.object?.windowMs))),
145
+ windowMs: Math.max(...a.map((e) => v(e.object?.windowMs))),
112
146
  totalSamples: o("totalSamples"),
113
- buckets: a.flatMap((e) => l(e.object?.buckets).map((t) => ({
147
+ buckets: a.flatMap((e) => _(e.object?.buckets).map((t) => ({
114
148
  ...t,
115
149
  tag: `${String(t.tag)} [${e.replicaId}]`
116
150
  })))
117
151
  };
118
- else if (i.every((e) => f(e.data))) p = i.flatMap((e) => f(e.data).map((t) => ({
152
+ else if (i.every((e) => b(e.data))) l = i.flatMap((e) => b(e.data).map((t) => ({
119
153
  ...t,
120
154
  labels: {
121
- ...c(t.labels),
155
+ ...g(t.labels),
122
156
  voltro_replica: e.replicaId
123
157
  }
124
158
  })));
125
- else if (a.every((e) => f(e.object?.samples))) p = { samples: a.flatMap((e) => f(e.object?.samples).map((t) => ({
159
+ else if (a.every((e) => b(e.object?.samples))) l = { samples: a.flatMap((e) => b(e.object?.samples).map((t) => ({
126
160
  ...t,
127
161
  labels: {
128
- ...c(t.labels),
162
+ ...g(t.labels),
129
163
  voltro_replica: e.replicaId
130
164
  }
131
165
  }))) };
132
- else if (r.length === 1 && i.length === 1) p = i[0].data;
166
+ else if (r.length === 1 && i.length === 1) l = i[0].data;
133
167
  else throw Error("This view must compare observations per replica; its values cannot be safely combined.");
134
168
  return {
135
169
  ...t,
136
- data: p
170
+ data: l
137
171
  };
138
- }, m = class extends Error {
172
+ }, S = class extends Error {
139
173
  name = "InvalidReplicaSelection";
140
- }, h = (e) => {
174
+ }, C = (e) => {
141
175
  let t = e.getAll("replicas"), n = e.getAll("replica");
142
176
  if (t.length !== 0 || n.length !== 0) {
143
- if (t.length > 1 || t.length === 1 && t[0] !== "all" || t.length > 0 && n.length > 0) throw new m("Choose all replicas or explicit replica IDs, not both.");
177
+ if (t.length > 1 || t.length === 1 && t[0] !== "all" || t.length > 0 && n.length > 0) throw new S("Choose all replicas or explicit replica IDs, not both.");
144
178
  if (t[0] === "all") return { mode: "all" };
145
- if (n.some((e) => e.trim().length === 0 || e.length > 512 || /[\u0000-\u001f\u007f]/.test(e))) throw new m("Replica IDs must be nonempty and contain no control characters.");
179
+ if (n.some((e) => e.trim().length === 0 || e.length > 512 || /[\u0000-\u001f\u007f]/.test(e))) throw new S("Replica IDs must be nonempty and contain no control characters.");
146
180
  return {
147
181
  mode: "selected",
148
182
  replicaIds: [...new Set(n)].sort()
149
183
  };
150
184
  }
151
- }, g = (e, t) => {
185
+ }, w = (e, t) => {
152
186
  let n = e.indexOf("#"), r = n < 0 ? "" : e.slice(n), i = n < 0 ? e : e.slice(0, n), a = i.indexOf("?"), o = a < 0 ? i : i.slice(0, a), s = new URLSearchParams(a < 0 ? "" : i.slice(a + 1));
153
187
  if (s.delete("replica"), s.delete("replicas"), t.mode === "all") s.set("replicas", "all");
154
188
  else {
155
- if (t.replicaIds.length === 0) throw new m("Select at least one replica.");
189
+ if (t.replicaIds.length === 0) throw new S("Select at least one replica.");
156
190
  for (let e of [...new Set(t.replicaIds)].sort()) s.append("replica", e);
157
191
  }
158
- return h(s), `${o}?${s}${r}`;
192
+ return C(s), `${o}?${s}${r}`;
159
193
  };
160
194
  //#endregion
161
- export { i as InspectBackfillInputSchema, r as InspectBackfillReceiptSchema, o as InspectBackfillResultSchema, m as InvalidReplicaSelection, s as ScheduleBackfillRequestConflict, a as decodeInspectBackfillInput, h as parseReplicaSelection, p as projectReplicaObservation, g as withReplicaSelection };
195
+ export { c as InspectBackfillInputSchema, s as InspectBackfillReceiptSchema, u as InspectBackfillResultSchema, h as InspectWorkflowExecutionObservationSchema, p as InspectWorkflowExecutionTargetSchema, i as InspectWorkflowQueuedStartObservationSchema, n as InspectWorkflowQueuedStartTargetSchema, S as InvalidReplicaSelection, d as ScheduleBackfillRequestConflict, l as decodeInspectBackfillInput, C as parseReplicaSelection, x as projectReplicaObservation, w as withReplicaSelection };
package/dist/jwt.d.ts CHANGED
@@ -26,7 +26,7 @@ declare interface AuthStrategy {
26
26
  readonly resolve: (input: AuthStrategyInput) => Promise<StrategyResolution> | StrategyResolution;
27
27
  /**
28
28
  * The bearer-token PREFIX this strategy claims, when it gates on one
29
- * (`'sk_'`, `'awb_'`). Declared so a collision is DETECTABLE.
29
+ * (`'sk_'`, `'acme_'`). Declared so a collision is DETECTABLE.
30
30
  *
31
31
  * Two strategies claiming the same prefix is not a harmless duplicate: the
32
32
  * chain is first-match-wins, so whichever runs first decides the Subject —
@@ -339,6 +339,14 @@ declare const Subject: Schema.Union<[Schema.Struct<{
339
339
  tenantId: typeof Schema.Null;
340
340
  scopes: Schema.optional<Schema.Array$<typeof Schema.String>>;
341
341
  metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
342
+ }>, Schema.Struct<{
343
+ type: Schema.Literal<["service"]>;
344
+ id: typeof Schema.String;
345
+ /** The calling app's name, as the callee lists it in `serviceCallers`. */
346
+ service: typeof Schema.String;
347
+ tenantId: typeof Schema.Null;
348
+ scopes: Schema.optional<Schema.Array$<typeof Schema.String>>;
349
+ metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
342
350
  }>]>;
343
351
 
344
352
  declare type Subject = typeof Subject.Type;
package/dist/rest.d.ts CHANGED
@@ -30,6 +30,9 @@ declare interface ActionProcedureDescriptor<Name extends string, Input extends S
30
30
  * `openAccess: '<why>'`. Mutually exclusive with `guards`; together they are
31
31
  * the only two shapes `security.defaultDeny` accepts. */
32
32
  readonly openAccess: string | undefined;
33
+ /** Only these apps of the project may call this procedure — see `service` on
34
+ * the definer's options. Absent → the app's own clients call it. */
35
+ readonly service: ServiceSpec | undefined;
33
36
  /** Opt this action into a public REST endpoint (innovation/11). */
34
37
  readonly publicApi: PublicApiSpec | undefined;
35
38
  /** Opt this action into the auto-synthesized agent toolset (innovation/07). */
@@ -138,7 +141,7 @@ export declare const collectPublicApiRoutes: (entries: ReadonlyArray<PublicApiEn
138
141
  * because that is where the normalised decision lands, and because every
139
142
  * enforcement path reads the descriptor's array and nothing else.
140
143
  */
141
- declare type DeclaredAccess<Input = unknown> = ReadonlyArray<AnyGuardSpec<Input> | OpenAccessSpec>;
144
+ declare type DeclaredAccess<Input = unknown> = ReadonlyArray<AnyGuardSpec<Input> | OpenAccessSpec | ServiceAccessSpec>;
142
145
 
143
146
  /** The `_tag` literals a descriptor's `error:` union declares (empty for none). */
144
147
  export declare const declaredErrorTags: (error: Schema.Schema.All | undefined) => ReadonlySet<string>;
@@ -178,15 +181,16 @@ export declare const deriveResource: (tag: string, spec: PublicApiSpec, descript
178
181
 
179
182
  /**
180
183
  * Dispatch a request across REST routes that SHARE a path. Every REST route
181
- * mounts as `*` and gates its own method, returning a precise 405 BEFORE any
182
- * side effect when the method isn't its own — so probing the group in order and
183
- * taking the FIRST non-405 result yields the route that owns the request's
184
- * method. This is what lets the standard GET + POST on one resource path
185
- * coexist: the runtime's rpc server (`rpcServer.ts`, started by both boot
186
- * paths) mounts ONE dispatcher per path (the underlying
187
- * router rejects two mounts on the same `(method, path)`). A single-route group
188
- * returns that route's result directly (its own 405 included); when no route in
189
- * the group owns the method, the last 405 stands.
184
+ * gates its own method, returning a precise 405 BEFORE any side effect when the
185
+ * method isn't its own — so probing the group in order and taking the FIRST
186
+ * non-405 result yields the route that owns the request's method. This is what
187
+ * lets the standard GET + POST on one resource path coexist: `rpcServer.ts` in
188
+ * `@voltro/runtime` mounts ONE dispatcher per
189
+ * path (the underlying router rejects two mounts on the same
190
+ * `(method, path)`). A single-route group returns that route's result directly
191
+ * (its own 405 included); when no route in the group owns the method, the last
192
+ * 405 stands — its `Allow` names every method the path serves, because
193
+ * `restRoutesToHttpRoutes` gives each route its path's full set.
190
194
  *
191
195
  * `group` must be non-empty (the serve pipeline only builds a dispatcher for a
192
196
  * path that has at least one route).
@@ -283,14 +287,24 @@ declare interface IdempotencyStore {
283
287
  /** Current record for (scope, key), or null. Read-side (inspect/dashboard). */
284
288
  readonly get: (scope: string, key: string) => Promise<IdempotencyRecord | null>;
285
289
  /**
286
- * Atomically claim (scope, key) IF absent OR stale (older than `ttlMs`):
290
+ * Atomically claim (scope, key) IF absent OR stale — a COMPLETED record older
291
+ * than `ttlMs` (the replay window), or an IN-FLIGHT one whose lease
292
+ * (`inFlightLeaseMs`, renewed by the live handler) lapsed:
287
293
  * write a fresh `in_flight` row and return `'claimed'`. Otherwise return the
288
294
  * EXISTING (still-fresh) record. The atomicity here is the whole game — two
289
295
  * concurrent same-key requests both hit this, exactly one gets `'claimed'`,
290
296
  * the loser reads the winner's record. In SQL this is one
291
297
  * `INSERT … ON CONFLICT DO UPDATE … WHERE existing.createdAt < now - ttl`.
292
298
  */
293
- readonly claim: (scope: string, key: string, now: number, ttlMs: number) => Promise<'claimed' | IdempotencyRecord>;
299
+ readonly claim: (scope: string, key: string, now: number, ttlMs: number, inFlightLeaseMs: number) => Promise<'claimed' | IdempotencyRecord>;
300
+ /**
301
+ * Extend a RUNNING claim's lease: bump its timestamp while it is still
302
+ * `in_flight` (a completed or released claim is left alone). The server calls
303
+ * it while the handler runs; a process that dies stops calling it, and its
304
+ * claim goes stale after `inFlightLeaseMs` instead of after the whole replay
305
+ * window.
306
+ */
307
+ readonly renew: (scope: string, key: string, now: number) => Promise<void>;
294
308
  /** Flip the claim to `completed` and store the response. */
295
309
  readonly complete: (scope: string, key: string, response: IdempotencyResponse, now: number) => Promise<void>;
296
310
  /** Drop the claim (handler errored → a retry may re-process). */
@@ -370,6 +384,9 @@ declare interface MutationProcedureDescriptor<Name extends string, Input extends
370
384
  * `openAccess: '<why>'`. Mutually exclusive with `guards`; together they are
371
385
  * the only two shapes `security.defaultDeny` accepts. */
372
386
  readonly openAccess: string | undefined;
387
+ /** Only these apps of the project may call this procedure — see `service` on
388
+ * the definer's options. Absent → the app's own clients call it. */
389
+ readonly service: ServiceSpec | undefined;
373
390
  /** Opt this mutation into a public REST endpoint (innovation/11). */
374
391
  readonly publicApi: PublicApiSpec | undefined;
375
392
  /** Opt this mutation into the auto-synthesized agent toolset (innovation/07). */
@@ -423,7 +440,7 @@ declare interface NestedTargetFields<Input = unknown> {
423
440
  /** Guard WHICH cached query entries this target patches: only entries whose
424
441
  * CURRENT value satisfies the predicate. Pure + browser-safe. Prevents a
425
442
  * patch from bleeding across sibling subscriptions that share a source table
426
- * (the guard AWB hand-writes as `roadmap.id === input.roadmapId`). Absent →
443
+ * (the guard an app would otherwise hand-write as `roadmap.id === input.roadmapId`). Absent →
427
444
  * every entry matching the target `table` is patched. */
428
445
  readonly match?: ((value: unknown, input: Input) => boolean) | undefined;
429
446
  }
@@ -504,11 +521,17 @@ declare interface PluginErrorSchema {
504
521
 
505
522
  /** A public raw-HTTP route a plugin serves on the framework listener. */
506
523
  declare interface PluginHttpRoute {
507
- /** HTTP method, or `'*'` for any (the handler decides). PATCH/HEAD/OPTIONS
508
- * are first-class — the REST desugar used to mount `'*'` partly BECAUSE
509
- * this union lacked PATCH; that reason is gone (the `'*'` mount remains
510
- * for its other job: one dispatcher per shared path + a precise 405). */
524
+ /** HTTP method, or `'*'` for any (the handler decides). The server mounts the
525
+ * path for exactly the methods its routes declare; `'*'` mounts it for every
526
+ * method the router knows (34), so keep that for a route that really serves
527
+ * them — see `answersOtherMethods` for one that only wants to refuse them. */
511
528
  readonly method: '*' | 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
529
+ /** `true`: the handler gates its own method, so a request with a method the
530
+ * path does not declare is still handed to it — to answer with a precise
531
+ * `405` and `Allow` instead of the router's `404`. The path is still mounted
532
+ * only for `method`. A REST route sets it. Without it, a route that declares
533
+ * one method never sees another. */
534
+ readonly answersOtherMethods?: boolean | undefined;
512
535
  /** Absolute path, e.g. `/_voltro/storage`. Matched byte for byte: the
513
536
  * router folds case and a trailing slash to FIND the route, the dispatcher
514
537
  * then asks whether the request's path is one the route declared — so
@@ -776,7 +799,8 @@ export declare const problemTypeOf: (tag: string) => string;
776
799
  * and the status `errorStatus` assigns (`422` by default, the status the
777
800
  * OpenAPI projection already documents for a typed error); a plugin's
778
801
  * cross-cutting error answers under the status the plugin names. Anything
779
- * else is rethrown for the desugar's shared mapping (`ScopeError` → 403/401, a
802
+ * else is rethrown for the desugar's shared mapping (`ScopeError` → 403/401,
803
+ * a resource guard's `NoSuchResource` → 404, a
780
804
  * `status`-carrying error → itself, the rest → 500). The body is the tagged
781
805
  * `{ error, …fields }` form; `projectedFailureFor` speaks the profile.
782
806
  */
@@ -963,6 +987,13 @@ declare interface PublicApiSpec {
963
987
  * optional, the rest inherited. See `PublicApiProfileConfig`.
964
988
  */
965
989
  readonly format?: PublicApiProfileConfig;
990
+ /**
991
+ * GET only: how long a CDN may keep a successful answer, and the tags it can
992
+ * purge it by (`Cache-Tag`, `Surrogate-Key`). A request with a credential is
993
+ * always answered `private` — see `RestCache`. `tags` receives the answer as
994
+ * it goes on the wire.
995
+ */
996
+ readonly cache?: RestCache;
966
997
  }
967
998
 
968
999
  /**
@@ -1052,6 +1083,9 @@ declare interface QueryProcedureDescriptor<Name extends string, Input extends Sc
1052
1083
  * `openAccess: '<why>'`. Mutually exclusive with `guards`; together they are
1053
1084
  * the only two shapes `security.defaultDeny` accepts. */
1054
1085
  readonly openAccess: string | undefined;
1086
+ /** Only these apps of the project may call this procedure — see `service` on
1087
+ * the definer's options. Absent → the app's own clients call it. */
1088
+ readonly service: ServiceSpec | undefined;
1055
1089
  /** Opt this query into a public REST endpoint (innovation/11). */
1056
1090
  readonly publicApi: PublicApiSpec | undefined;
1057
1091
  /** Opt this query into the auto-synthesized agent toolset (innovation/07). */
@@ -1153,6 +1187,27 @@ export declare interface RestByteResponse {
1153
1187
  readonly headers?: Readonly<Record<string, string | ReadonlyArray<string>>>;
1154
1188
  }
1155
1189
 
1190
+ /**
1191
+ * Caching for a GET route's successful (200) answers.
1192
+ *
1193
+ * An anonymous request's answer is `public` for `maxAge` seconds (`max-age`
1194
+ * and `s-maxage`), `stale-while-revalidate` when set, and carries `tags` as
1195
+ * `Cache-Tag` (comma-separated) and `Surrogate-Key` (space-separated) — the
1196
+ * headers CDNs purge by. A request that carries a credential (`Authorization`
1197
+ * or a cookie) gets `private, no-cache` and no tags, whatever is declared:
1198
+ * what a key or a session may see must never land in a shared cache. Every
1199
+ * such answer says `Vary: Authorization, Cookie`. Errors are not affected.
1200
+ */
1201
+ export declare interface RestCache {
1202
+ /** Seconds a cache may serve the answer without asking again. */
1203
+ readonly maxAge: number;
1204
+ /** Seconds after that a stale answer may still be served while the cache refetches. */
1205
+ readonly staleWhileRevalidate?: number;
1206
+ /** Purge tags from the answer as it goes on the wire, and the input.
1207
+ * A tag with whitespace or a comma is dropped (no header can carry it). */
1208
+ readonly tags?: (output: unknown, input: unknown) => ReadonlyArray<string>;
1209
+ }
1210
+
1156
1211
  export declare type RestDates = 'iso' | 'epoch-ms';
1157
1212
 
1158
1213
  export declare type RestEnvelope = 'none' | 'data' | 'jsonapi';
@@ -1244,6 +1299,11 @@ export declare interface RestGuardRejection {
1244
1299
  readonly message: string;
1245
1300
  }
1246
1301
 
1302
+ /** What a REST handler may return: the decoded output, a `response(...)` with
1303
+ * its own status and headers, an `sse(...)` stream, or `bytes(...)` for a
1304
+ * non-JSON body (a file, an export, an XML document). */
1305
+ export declare type RestHandlerResult<O> = O | RestResponse<O> | RestStreamResponse | RestByteResponse;
1306
+
1247
1307
  export declare type RestMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
1248
1308
 
1249
1309
  export declare type RestNaming = 'camelCase' | 'snake_case';
@@ -1307,7 +1367,7 @@ export declare interface RestRouteDescriptor<I, O> {
1307
1367
  /** Output schema. The handler returns the decoded `Type`; the desugar
1308
1368
  * encodes it to JSON for the `200` response. */
1309
1369
  readonly output: Schema.Schema<O, any, never>;
1310
- readonly handler: (input: I, ctx: RestRouteContext) => Promise<O | RestResponse<O>> | O | RestResponse<O>;
1370
+ readonly handler: (input: I, ctx: RestRouteContext) => Promise<RestHandlerResult<O>> | RestHandlerResult<O>;
1311
1371
  readonly summary?: string;
1312
1372
  readonly description?: string;
1313
1373
  readonly example?: RestRouteExample;
@@ -1364,6 +1424,9 @@ export declare interface RestRouteDescriptor<I, O> {
1364
1424
  * of the JSON), so it is correct across content-encodings — the
1365
1425
  * transport's compression varies the bytes, not the representation. */
1366
1426
  readonly etag?: boolean;
1427
+ /** GET only: how long a shared cache (a CDN) may keep a successful answer,
1428
+ * and the tags it can be purged by. See {@link RestCache}. */
1429
+ readonly cache?: RestCache;
1367
1430
  }
1368
1431
 
1369
1432
  /** Intentional non-2xx REST control flow with an application-defined JSON
@@ -1490,6 +1553,37 @@ export declare const routeAcceptsPath: (route: {
1490
1553
  readonly match?: "prefix" | "exact" | undefined;
1491
1554
  }, path: string) => boolean;
1492
1555
 
1556
+ /**
1557
+ * The runtime-erased form of a procedure's `service: { callers }` — "only these
1558
+ * apps of the project may call this, and nobody else".
1559
+ *
1560
+ * A guard entry for the reason `OpenAccessSpec` is one: every enforcement path
1561
+ * is handed the `guards` array and nothing else, so a decision that lived on a
1562
+ * separate descriptor field would be enforced by the paths somebody remembered
1563
+ * and skipped by the rest.
1564
+ *
1565
+ * It admits exactly a `service` subject whose name is listed. `admin:full` does
1566
+ * NOT bypass it: a service procedure is a machine contract between two apps,
1567
+ * and a browser session holding the admin scope is not one of the parties.
1568
+ */
1569
+ declare interface ServiceAccessSpec {
1570
+ /** The calling apps' names, as the callee declares them in `serviceCallers`.
1571
+ * Non-empty by construction — the definers refuse an empty list. */
1572
+ readonly service: ReadonlyArray<string>;
1573
+ }
1574
+
1575
+ /**
1576
+ * A service procedure's declaration: which apps of the project may call it.
1577
+ *
1578
+ * service: { callers: ['worker'] }
1579
+ *
1580
+ * The names are the CALLING apps' names as this app lists them in
1581
+ * `serviceCallers` — the same name the caller's credential resolves to.
1582
+ */
1583
+ declare interface ServiceSpec {
1584
+ readonly callers: ReadonlyArray<string>;
1585
+ }
1586
+
1493
1587
  /**
1494
1588
  * Build a streaming (Server-Sent Events) handler result. `subscribe` gets an
1495
1589
  * `emit` for one already-framed SSE chunk and MUST return an unsubscribe, which
@@ -1558,6 +1652,14 @@ declare const Subject: Schema.Union<[Schema.Struct<{
1558
1652
  tenantId: typeof Schema.Null;
1559
1653
  scopes: Schema.optional<Schema.Array$<typeof Schema.String>>;
1560
1654
  metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
1655
+ }>, Schema.Struct<{
1656
+ type: Schema.Literal<["service"]>;
1657
+ id: typeof Schema.String;
1658
+ /** The calling app's name, as the callee lists it in `serviceCallers`. */
1659
+ service: typeof Schema.String;
1660
+ tenantId: typeof Schema.Null;
1661
+ scopes: Schema.optional<Schema.Array$<typeof Schema.String>>;
1662
+ metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
1561
1663
  }>]>;
1562
1664
 
1563
1665
  declare type Subject = typeof Subject.Type;