@voltro/protocol 0.86.0 → 0.88.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +5484 -0
  2. package/THIRD-PARTY-NOTICES.md +3 -227
  3. package/dist/apiRoleEndpoints.js +107 -0
  4. package/dist/apiRoleObservation.js +67 -0
  5. package/dist/apikey.d.ts +10 -0
  6. package/dist/apikey.js +1 -2
  7. package/dist/approval.js +1 -2
  8. package/dist/approvalDescriptors.js +1 -2
  9. package/dist/auth.js +40 -21
  10. package/dist/boundedRing.js +1 -2
  11. package/dist/businessRule.js +1 -2
  12. package/dist/canonicalJson.js +13 -0
  13. package/dist/connection.js +1 -2
  14. package/dist/devActivity.js +1 -2
  15. package/dist/errorSchema.js +23 -0
  16. package/dist/errorValue.js +1 -2
  17. package/dist/event.js +52 -47
  18. package/dist/formCapture.js +11 -0
  19. package/dist/httpStreamMaxDuration.js +8 -0
  20. package/dist/idempotency.js +89 -35
  21. package/dist/idempotencyInput.js +122 -0
  22. package/dist/identity.js +1 -2
  23. package/dist/index.d.ts +876 -124
  24. package/dist/index.js +51 -40
  25. package/dist/inspect.d.ts +1 -1
  26. package/dist/inspect.js +1 -2
  27. package/dist/inspectBackfill.js +1 -2
  28. package/dist/inspectCommandRejection.js +1 -2
  29. package/dist/inspectProjection.js +1 -2
  30. package/dist/inspectStorageProjection.js +1 -2
  31. package/dist/inspectValueIdentity.js +1 -2
  32. package/dist/inspectWorkflowExecution.js +1 -2
  33. package/dist/inspectWorkflowQueuedStart.js +1 -2
  34. package/dist/jsonSchema.js +1 -2
  35. package/dist/jwt.d.ts +10 -0
  36. package/dist/jwt.js +2 -3
  37. package/dist/outbox.js +1 -2
  38. package/dist/pairing.js +1 -2
  39. package/dist/plugin.js +44 -19
  40. package/dist/pluginAccountMerge.js +87 -0
  41. package/dist/pluginNaming.js +1 -2
  42. package/dist/pluginPrivacy.js +165 -0
  43. package/dist/procedure.js +115 -98
  44. package/dist/publicApi.js +191 -211
  45. package/dist/queryOptionalSources.js +13 -0
  46. package/dist/reactivityChannel.js +1 -2
  47. package/dist/rest.d.ts +227 -64
  48. package/dist/rest.js +296 -194
  49. package/dist/restFormat.js +1 -2
  50. package/dist/restReceipt.js +42 -0
  51. package/dist/revalidate.js +12 -0
  52. package/dist/rowEquality.js +1 -2
  53. package/dist/rowPatch.js +1 -2
  54. package/dist/schemaShape.js +1 -2
  55. package/dist/scopes.js +72 -40
  56. package/dist/secretWindows.js +70 -0
  57. package/dist/serverErrorBus.js +1 -2
  58. package/dist/serviceCall.js +1 -2
  59. package/dist/session.d.ts +89 -6
  60. package/dist/session.js +56 -56
  61. package/dist/sessionHeader.js +1 -2
  62. package/dist/ssrfPolicy.js +29 -0
  63. package/dist/storageAccess.js +1 -2
  64. package/dist/storeErrors.js +1 -2
  65. package/dist/storeScreenshots.js +1 -2
  66. package/dist/strictInput.js +1 -2
  67. package/dist/subscriptionEvent.js +1 -2
  68. package/dist/tenantSelection.js +1 -2
  69. package/dist/time.js +1 -2
  70. package/dist/undo.js +1 -2
  71. package/dist/validation.js +1 -2
  72. package/dist/webSocketGateway.js +8 -3
  73. package/dist/workflow.js +1 -2
  74. package/dist/workflowAccess.js +1 -2
  75. package/dist/workflowRestart.js +1 -2
  76. package/dist/workflowRetry.js +1 -2
  77. package/dist/workflowStartOutcome.js +1 -2
  78. package/package.json +7 -5
@@ -1,6 +1,6 @@
1
1
  import { Schema as e, SchemaGetter as t, SchemaTransformation as n } from "effect";
2
2
  import * as r from "effect/SchemaAST";
3
- //#region src/restFormat.ts
3
+ // src/restFormat.ts
4
4
  var i = {
5
5
  rpc: {
6
6
  name: "rpc",
@@ -159,5 +159,4 @@ var i = {
159
159
  ...r === void 0 ? {} : { links: { next: y(n, r, e.cursorParam).replace(/^<|>; rel="next"$/g, "") } }
160
160
  }, g);
161
161
  };
162
- //#endregion
163
162
  export { g as JSONAPI_MEDIA_TYPE, h as PROBLEM_JSON, a as REST_PROFILE_NAMES, _ as encodeRestError, x as encodeRestSuccess, y as nextLink, m as problemDetails, f as problemTypeOf, o as resolveRestFormat, d as restViewSchema, s as toSnakeCase };
@@ -0,0 +1,42 @@
1
+ import { Effect as e, Schema as t } from "effect";
2
+ // src/restReceipt.ts
3
+ var n = t.Struct({
4
+ _tag: t.Literal("VoltroRestOutput"),
5
+ version: t.Literal(1),
6
+ kind: t.Union([t.Literal("raw"), t.Literal("projection")]),
7
+ headers: t.optional(t.Record(t.String, t.Union([t.String, t.Array(t.String)]))),
8
+ contentType: t.optional(t.String)
9
+ }), r = (r, i) => {
10
+ let a = t.toCodecJson(r), o = t.encodeUnknownEffect(a), s = t.decodeUnknownEffect(a), c = t.decodeUnknownEffect(n), l = async (t) => {
11
+ let n = await e.runPromise(c(t.rest));
12
+ if (n.kind !== i) throw Error("REST receipt belongs to a different surface");
13
+ return {
14
+ body: await e.runPromise(s(t.body)),
15
+ headers: n.headers,
16
+ contentType: n.contentType
17
+ };
18
+ };
19
+ return {
20
+ read: l,
21
+ capture: async (r, a) => {
22
+ let s = await e.runPromise(o(r)), c = JSON.parse(JSON.stringify({
23
+ body: s,
24
+ rest: {
25
+ _tag: "VoltroRestOutput",
26
+ version: 1,
27
+ kind: i,
28
+ ...a.headers === void 0 ? {} : { headers: a.headers },
29
+ ...a.contentType === void 0 ? {} : { contentType: a.contentType }
30
+ }
31
+ })), u = await e.runPromise(t.decodeUnknownEffect(t.Struct({
32
+ body: t.optional(t.Unknown),
33
+ rest: n
34
+ }))(c)), d = {
35
+ body: u.body,
36
+ rest: u.rest
37
+ };
38
+ return await l(d), d;
39
+ }
40
+ };
41
+ };
42
+ export { r as makeRestReceiptCodec };
@@ -0,0 +1,12 @@
1
+ // src/revalidate.ts
2
+ var e = (e) => {
3
+ if (typeof e == "number" && Number.isFinite(e) && e > 0) return e * 1e3;
4
+ if (typeof e != "string") return null;
5
+ let t = /^\s*(\d+)\s*(s|seconds?|m|minutes?|h|hours?|d|days?)\s*$/i.exec(e);
6
+ if (!t) return null;
7
+ let n = parseInt(t[1], 10);
8
+ if (!Number.isFinite(n) || n <= 0) return null;
9
+ let r = t[2].toLowerCase();
10
+ return r === "s" || r.startsWith("second") ? n * 1e3 : r === "m" || r.startsWith("minute") ? n * 6e4 : r === "h" || r.startsWith("hour") ? n * 36e5 : r === "d" || r.startsWith("day") ? n * 864e5 : null;
11
+ };
12
+ export { e as parseRevalidateMs };
@@ -1,4 +1,4 @@
1
- //#region src/rowEquality.ts
1
+ // src/rowEquality.ts
2
2
  var e = (e) => e instanceof Uint8Array && Object.getPrototypeOf(e) === Uint8Array.prototype && !("toJSON" in e) && Object.keys(e).length === e.length, t = (e) => {
3
3
  let t = Object.getPrototypeOf(e);
4
4
  return (t === null || t === Object.prototype) && !("toJSON" in e) && !("toJSON" in Object.prototype);
@@ -27,5 +27,4 @@ var e = (e) => e instanceof Uint8Array && Object.getPrototypeOf(e) === Uint8Arra
27
27
  }
28
28
  return s;
29
29
  };
30
- //#endregion
31
30
  export { i as flatRowsEqual };
package/dist/rowPatch.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { flatRowsEqual as e } from "./rowEquality.js";
2
2
  import { Result as t, Schema as n, SchemaGetter as r } from "effect";
3
3
  import * as i from "effect/encoding/Base64";
4
- //#region src/rowPatch.ts
4
+ // src/rowPatch.ts
5
5
  var a = n.Union([n.String, n.Number]), o = (e) => typeof e == "object" && !!e && !Array.isArray(e) && Object.keys(e).length === 1 && typeof e.$bytes == "string", s = n.Unknown.pipe(n.decodeTo(n.Unknown, {
6
6
  decode: r.transform((e) => {
7
7
  if (!o(e)) return e;
@@ -178,5 +178,4 @@ var a = n.Union([n.String, n.Number]), o = (e) => typeof e == "object" && !!e &&
178
178
  }
179
179
  };
180
180
  };
181
- //#endregion
182
181
  export { v as applyRowPatch, b as createRowPatchApplier, g as diffRows, d as idToPath, _ as isIdKeyed, s as patchCellSchema, f as pathToId, l as rowPatchOpSchema, u as rowPatchSchema };
@@ -1,5 +1,5 @@
1
1
  import { RpcSchema as e } from "effect/rpc";
2
- //#region src/schemaShape.ts
2
+ // src/schemaShape.ts
3
3
  var t = (e) => typeof e == "object" && !!e && typeof e._tag == "string", n = 64, r = (e, i = 0) => {
4
4
  if (!t(e)) return String(e);
5
5
  if (i >= n) return "…";
@@ -63,5 +63,4 @@ var t = (e) => typeof e == "object" && !!e && typeof e._tag == "string", n = 64,
63
63
  missingHere: r.sort()
64
64
  };
65
65
  };
66
- //#endregion
67
66
  export { c as compareSurfaces, s as groupShapeDigests, o as procedureShapeDigest, r as schemaShape, i as shapeDigest };
package/dist/scopes.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { RequestedTenantRefused as e } from "./tenantSelection.js";
2
2
  import { Unauthenticated as t, currentSubjectAuthority as n, reauthorizeSubject as r } from "./auth.js";
3
3
  import { Effect as i, Schema as a } from "effect";
4
- //#region src/scopes.ts
4
+ // src/scopes.ts
5
5
  var o = "admin:full", s = "apikeys:issue:self", c = "apikeys:issue:org", l = "apikeys:issue:other", u = "devices:manage", d = class extends a.TaggedError()("ScopeError", {
6
6
  required: a.String,
7
7
  message: a.String
@@ -47,18 +47,41 @@ var o = "admin:full", s = "apikeys:issue:self", c = "apikeys:issue:org", l = "ap
47
47
  });
48
48
  return n;
49
49
  }, w = (e) => (typeof e.action == "string" || typeof e.action == "function") && typeof e.resourceType == "string", T = (e) => e !== void 0 && e.some((e) => w(e) || typeof e.resource == "function"), E = (e) => typeof e.open == "string", D = (e) => Array.isArray(e.service), O = (e) => ({ service: [...e] }), k = (e) => e?.find(D)?.service, A = (e, t) => {
50
+ for (let n of t ?? []) {
51
+ let t = N(n);
52
+ if (t) return K(e, t);
53
+ }
54
+ if (t?.some(M)) {
55
+ let t = P(e);
56
+ if (t) return t;
57
+ }
50
58
  let n = k(t);
51
59
  if (e.type === "service") return n !== void 0 && n.includes(e.service) ? null : new d({
52
60
  required: "service-procedure",
53
61
  message: n === void 0 ? `service '${e.service}' may only call procedures that declare it in \`service: { callers }\`` : `service '${e.service}' is not among this procedure's callers [${n.join(", ")}]`
54
62
  });
55
- if (n !== void 0) return U(e, new d({
63
+ if (n !== void 0) return K(e, new d({
56
64
  required: `service:${n.join("|")}`,
57
65
  message: `this procedure is callable only by the service${n.length === 1 ? "" : "s"} [${n.join(", ")}]`
58
66
  }));
59
- }, j = (e) => e.authenticated === !0, M = (e) => ({ open: e }), N = (e) => typeof e == "string" ? [e] : e, P, F = (e) => {
60
- P = e;
61
- }, I = (e) => P === void 0 ? e.filter((e) => (e.guards ?? []).some((e) => !w(e) && e.resource !== void 0)).map((e) => e.tag) : [], L = (e) => [
67
+ }, j = (e) => e.authenticated === !0, M = (e) => e.upgraded === !0, N = (e) => typeof e != "object" || !e || !("upgraded" in e) || e.upgraded !== !0 ? null : [
68
+ "scope",
69
+ "mode",
70
+ "resource",
71
+ "action",
72
+ "resourceType",
73
+ "authenticated",
74
+ "open",
75
+ "service"
76
+ ].some((t) => t in e) ? new d({
77
+ required: "guard-shape",
78
+ message: "An upgraded guard must be a separate entry; put each additional scope, policy or identity requirement in its own guard entry."
79
+ }) : null, P = (e) => e.type === "user" && e.kind !== "guest" ? null : K(e, new d({
80
+ required: "upgraded",
81
+ message: "this procedure requires a registered user account"
82
+ })), F = (e) => ({ open: e }), I = (e) => typeof e == "string" ? [e] : e, L, R = (e) => {
83
+ L = e;
84
+ }, z = (e) => L === void 0 ? e.filter((e) => (e.guards ?? []).some((e) => !w(e) && e.resource !== void 0)).map((e) => e.tag) : [], B = (e) => [
62
85
  `${e.length} procedure${e.length === 1 ? "" : "s"} declare a per-resource guard that nothing enforces:`,
63
86
  ...e.slice(0, 10).map((e) => ` ${e}`),
64
87
  ...e.length > 10 ? [` … and ${e.length - 10} more`] : [],
@@ -72,53 +95,58 @@ var o = "admin:full", s = "apikeys:issue:self", c = "apikeys:issue:org", l = "ap
72
95
  " • setResourceScopeResolver((req) => Effect<boolean>) — answers \"does this",
73
96
  " subject hold `req.scope` on `req.resource`\" from your own tables.",
74
97
  " • Drop the `resource` extractor if a global scope check is what you meant."
75
- ].join("\n"), R = () => P, z, B = (e) => {
76
- z = e;
77
- }, V = () => z, H = (e) => new d({
98
+ ].join("\n"), V = () => L, H, U = (e) => {
99
+ H = e;
100
+ }, W = () => H, G = (e) => new d({
78
101
  required: "access-decision",
79
102
  message: `${e ?? "this procedure"} declares no access decision: it has neither \`guards:\` nor \`openAccess:\`, and the app runs with \`security.defaultDeny\`. Add the scope(s) a caller must hold (\`guards: [{ scope: '…' }]\`), or declare it open on purpose with \`openAccess: '<why anyone may call this>'\`.`
80
- }), U = (e, n) => {
103
+ }), K = (e, n) => {
81
104
  let r = e.credentialRejected;
82
105
  return r === void 0 ? n : new t({ reason: `${r} — the request carried a credential and it was REJECTED, so this caller resolved to anonymous. The guard then refused (${n.message}); the scope is not the problem. Re-authenticate and retry.` });
83
- }, W = (e) => {
106
+ }, q = (e) => {
84
107
  let t = Array.isArray(e), n = Object.getPrototypeOf(e);
85
108
  if (t ? n !== Array.prototype : n !== Object.prototype && n !== null) return !1;
86
109
  for (let n of Reflect.ownKeys(e)) if (t && (typeof n != "string" || n !== "length" && !/^(0|[1-9][0-9]*)$/.test(n)) || !("value" in Object.getOwnPropertyDescriptor(e, n))) return !1;
87
110
  return !0;
88
- }, G = (e, t) => {
111
+ }, J = (e, t) => {
89
112
  let n, r = 0, i, a = 0, o;
90
113
  return (s) => {
91
114
  let c = y(e);
92
- return (c !== n || c.length !== r) && (n = c, r = c.length, i = void 0, a = 0, o = void 0), i === void 0 && c.length >= 32 && ++a > 1 && (o ??= W(c) && W(e) && W(t) && t.every((e) => W(e) && (!("scope" in e) || typeof e.scope == "string" || W(e.scope))), o && (i = new Set(c))), i === void 0 ? c.includes("admin:full") || c.includes(s) : i.has("admin:full") || i.has(s);
115
+ return (c !== n || c.length !== r) && (n = c, r = c.length, i = void 0, a = 0, o = void 0), i === void 0 && c.length >= 32 && ++a > 1 && (o ??= q(c) && q(e) && q(t) && t.every((e) => q(e) && (!("scope" in e) || typeof e.scope == "string" || q(e.scope))), o && (i = new Set(c))), i === void 0 ? c.includes("admin:full") || c.includes(s) : i.has("admin:full") || i.has(s);
93
116
  };
94
- }, K = (e, t, n) => {
117
+ }, Y = (e, t, n) => {
95
118
  let r = A(e, t);
96
119
  if (r !== void 0) return r;
97
- if (!t || t.length === 0) return n?.defaultDeny === !0 ? H(n.procedure) : null;
120
+ if (!t || t.length === 0) return n?.defaultDeny === !0 ? G(n.procedure) : null;
98
121
  let i = 0, a;
99
122
  for (let n of t) {
100
123
  if (E(n) || D(n)) continue;
124
+ if (M(n)) {
125
+ let t = P(e);
126
+ if (t) return t;
127
+ continue;
128
+ }
101
129
  if (j(n)) {
102
130
  if (e.id !== null && e.id !== void 0) continue;
103
- return U(e, new d({
131
+ return K(e, new d({
104
132
  required: "authenticated",
105
133
  message: "this procedure requires an authenticated caller"
106
134
  }));
107
135
  }
108
136
  if (w(n)) {
109
- if (i++, a === void 0 && (t.length >= 512 || i >= 512) && (a = G(e, t)), a === void 0 ? b(e, "admin:full") : a("admin:full")) continue;
110
- return U(e, new d({
137
+ if (i++, a === void 0 && (t.length >= 512 || i >= 512) && (a = J(e, t)), a === void 0 ? b(e, "admin:full") : a("admin:full")) continue;
138
+ return K(e, new d({
111
139
  required: `${n.resourceType}:${n.action}`,
112
140
  message: `relationship guard '${n.action}' on '${n.resourceType}' cannot be checked synchronously`
113
141
  }));
114
142
  }
115
- let r = N(n.scope);
116
- if (n.resourceOnly === !0) return U(e, new d({
143
+ let r = I(n.scope);
144
+ if (n.resourceOnly === !0) return K(e, new d({
117
145
  required: r[0] ?? "resource",
118
146
  message: "resource-only guard cannot be checked synchronously"
119
147
  }));
120
148
  if (r.length === 0) continue;
121
- i += r.length, a === void 0 && (t.length >= 512 || i >= 512) && (a = G(e, t));
149
+ i += r.length, a === void 0 && (t.length >= 512 || i >= 512) && (a = J(e, t));
122
150
  let o = n.mode ?? "all", s;
123
151
  if (o === "any") {
124
152
  if (a === void 0 ? r.some((t) => b(e, t)) : r.some(a)) continue;
@@ -128,13 +156,13 @@ var o = "admin:full", s = "apikeys:issue:self", c = "apikeys:issue:org", l = "ap
128
156
  if (t === void 0) continue;
129
157
  s = t;
130
158
  }
131
- return U(e, new d({
159
+ return K(e, new d({
132
160
  required: s,
133
161
  message: o === "any" ? `missing all of required scopes [${r.join(", ")}]` : `missing required scope '${s}'`
134
162
  }));
135
163
  }
136
164
  return null;
137
- }, q = (e, t, n) => {
165
+ }, X = (e, t, n) => {
138
166
  let r;
139
167
  try {
140
168
  r = C(t, n);
@@ -146,7 +174,7 @@ var o = "admin:full", s = "apikeys:issue:self", c = "apikeys:issue:org", l = "ap
146
174
  message: e
147
175
  });
148
176
  if (b(e, "admin:full")) return i.succeed(null);
149
- let o = z;
177
+ let o = H;
150
178
  if (o === void 0) return i.succeed(a(`no policy resolver registered — cannot authorize '${r}' on '${t.resourceType}'`));
151
179
  let s = t.resource(n);
152
180
  return s === void 0 || s === "" ? i.succeed(a(`input does not identify a '${t.resourceType}' to authorize`)) : i.suspend(() => o({
@@ -154,34 +182,39 @@ var o = "admin:full", s = "apikeys:issue:self", c = "apikeys:issue:org", l = "ap
154
182
  action: r,
155
183
  resourceType: t.resourceType,
156
184
  resourceId: s
157
- })).pipe(i.catchCause(() => i.succeed(!1)), i.map((n) => J(e, n, t.resourceType, s, () => a(`no '${r}' relation on ${t.resourceType}:${s}`))));
158
- }, J = (e, t, n, r, i) => t === !0 ? null : t === "missing" && e.id !== null && e.id !== void 0 ? new f({
185
+ })).pipe(i.catchCause(() => i.succeed(!1)), i.map((n) => Z(e, n, t.resourceType, s, () => a(`no '${r}' relation on ${t.resourceType}:${s}`))));
186
+ }, Z = (e, t, n, r, i) => t === !0 ? null : t === "missing" && e.id !== null && e.id !== void 0 ? new f({
159
187
  resourceType: n,
160
188
  resourceId: r,
161
189
  message: n === null ? `no such resource '${r}'` : `no such ${n} '${r}'`
162
- }) : i(), Y = (e, t, n, r) => {
190
+ }) : i(), Q = (e, t, n, r) => {
163
191
  let a = A(e, t);
164
192
  if (a !== void 0) return i.succeed(a);
165
- if (!t || t.length === 0) return i.succeed(K(e, t, r));
166
- let o = P, s = t.some(w), c = t.some((e) => "scope" in e && e.resourceOnly === !0);
167
- return !s && !c && (!o || !t.some((e) => e.resource !== void 0)) ? i.succeed(K(e, t, r)) : i.gen(function* () {
193
+ if (!t || t.length === 0) return i.succeed(Y(e, t, r));
194
+ let o = L, s = t.some(w), c = t.some((e) => "scope" in e && e.resourceOnly === !0);
195
+ return !s && !c && (!o || !t.some((e) => e.resource !== void 0)) ? i.succeed(Y(e, t, r)) : i.gen(function* () {
168
196
  for (let r of t) {
169
197
  if (E(r) || D(r)) continue;
198
+ if (M(r)) {
199
+ let t = P(e);
200
+ if (t) return t;
201
+ continue;
202
+ }
170
203
  if (j(r)) {
171
204
  if (e.id !== null && e.id !== void 0) continue;
172
- return U(e, new d({
205
+ return K(e, new d({
173
206
  required: "authenticated",
174
207
  message: "this procedure requires an authenticated caller"
175
208
  }));
176
209
  }
177
210
  if (w(r)) {
178
- let t = yield* q(e, r, n);
211
+ let t = yield* X(e, r, n);
179
212
  if (t) return t;
180
213
  continue;
181
214
  }
182
- let t = N(r.scope);
215
+ let t = I(r.scope);
183
216
  if (t.length === 0) {
184
- if (r.resourceOnly === !0) return U(e, new d({
217
+ if (r.resourceOnly === !0) return K(e, new d({
185
218
  required: "resource",
186
219
  message: "resource-only guard requires a non-empty scope list"
187
220
  }));
@@ -192,12 +225,12 @@ var o = "admin:full", s = "apikeys:issue:self", c = "apikeys:issue:org", l = "ap
192
225
  a = r.resource ? r.resource(n) : void 0;
193
226
  } catch (n) {
194
227
  if (r.resourceOnly !== !0) throw n;
195
- return U(e, new d({
228
+ return K(e, new d({
196
229
  required: t[0],
197
230
  message: "resource selector failed"
198
231
  }));
199
232
  }
200
- if (r.resourceOnly === !0 && (o === void 0 || typeof a != "string" || a === "")) return U(e, new d({
233
+ if (r.resourceOnly === !0 && (o === void 0 || typeof a != "string" || a === "")) return K(e, new d({
201
234
  required: t[0],
202
235
  message: "resource-only guard requires a resolver and a non-empty resource id"
203
236
  }));
@@ -215,7 +248,7 @@ var o = "admin:full", s = "apikeys:issue:self", c = "apikeys:issue:org", l = "ap
215
248
  if (!(l === "any" ? s.some(Boolean) : s.every(Boolean))) {
216
249
  let n = l === "any" ? t[0] : t[t.findIndex((e, t) => !s[t])];
217
250
  if (c && a !== void 0) {
218
- let t = J(e, "missing", null, a, () => new d({
251
+ let t = Z(e, "missing", null, a, () => new d({
219
252
  required: n,
220
253
  message: `missing required scope '${n}' on resource '${a}'`
221
254
  }));
@@ -229,12 +262,11 @@ var o = "admin:full", s = "apikeys:issue:self", c = "apikeys:issue:org", l = "ap
229
262
  }
230
263
  return null;
231
264
  });
232
- }, X = (n, o, s, c) => i.tryPromise({
265
+ }, $ = (n, o, s, c) => i.tryPromise({
233
266
  try: async () => r(n, (e) => _(n, e)),
234
267
  catch: (e) => e
235
268
  }).pipe(i.matchEffect({
236
269
  onFailure: (n) => a.is(t)(n) || a.is(e)(n) ? i.succeed(n) : i.die(n),
237
- onSuccess: (e) => Y(e, o, s, c)
270
+ onSuccess: (e) => Q(e, o, s, c)
238
271
  }));
239
- //#endregion
240
- export { o as ADMIN_SCOPE, c as APIKEY_ISSUE_ORG_SCOPE, l as APIKEY_ISSUE_OTHER_SCOPE, s as APIKEY_ISSUE_SELF_SCOPE, u as DEVICES_MANAGE_SCOPE, f as NoSuchResource, d as ScopeError, L as advisoryResourceGuardWarning, K as checkGuards, Y as checkGuardsEffect, T as declaresResourceGuard, y as effectiveScopes, I as findAdvisoryResourceGuards, V as getPolicyGuardResolver, R as getResourceScopeResolver, b as hasEffectiveScope, x as hasScope, j as isAuthenticatedCheck, E as isOpenAccess, w as isPolicyCheck, D as isServiceAccess, H as missingAccessDecision, M as openAccessSpec, X as recheckGuardsEffect, _ as refreshEffectiveScopes, S as requireScope, C as resolvePolicyAction, O as serviceAccessSpec, k as serviceCallersOf, v as setEffectiveScopes, g as setEffectiveScopesRefresher, B as setPolicyGuardResolver, F as setResourceScopeResolver, p as subjectScopes };
272
+ export { o as ADMIN_SCOPE, c as APIKEY_ISSUE_ORG_SCOPE, l as APIKEY_ISSUE_OTHER_SCOPE, s as APIKEY_ISSUE_SELF_SCOPE, u as DEVICES_MANAGE_SCOPE, f as NoSuchResource, d as ScopeError, B as advisoryResourceGuardWarning, Y as checkGuards, Q as checkGuardsEffect, T as declaresResourceGuard, y as effectiveScopes, z as findAdvisoryResourceGuards, W as getPolicyGuardResolver, V as getResourceScopeResolver, b as hasEffectiveScope, x as hasScope, j as isAuthenticatedCheck, E as isOpenAccess, w as isPolicyCheck, D as isServiceAccess, M as isUpgradedCheck, G as missingAccessDecision, F as openAccessSpec, $ as recheckGuardsEffect, _ as refreshEffectiveScopes, S as requireScope, C as resolvePolicyAction, O as serviceAccessSpec, k as serviceCallersOf, v as setEffectiveScopes, g as setEffectiveScopesRefresher, U as setPolicyGuardResolver, R as setResourceScopeResolver, p as subjectScopes, N as upgradedGuardShapeError };
@@ -0,0 +1,70 @@
1
+ import { createHash as e, createHmac as t, timingSafeEqual as n } from "node:crypto";
2
+ // src/secretWindows.ts
3
+ var r = (e) => t("sha256", e).update("voltro:secret-fingerprint:v1").digest("hex").slice(0, 16), i = Symbol.for("voltro.secretWindows"), a = () => {
4
+ let e = globalThis;
5
+ return e[i] ??= {
6
+ signed: /* @__PURE__ */ new Map(),
7
+ configured: /* @__PURE__ */ new Map(),
8
+ presented: /* @__PURE__ */ new Map(),
9
+ declared: /* @__PURE__ */ new Map(),
10
+ windows: /* @__PURE__ */ new Map()
11
+ }, e[i];
12
+ }, o = (e, t) => {
13
+ let n = e.get(t);
14
+ if (n !== void 0) return n;
15
+ let r = /* @__PURE__ */ new Map();
16
+ return e.set(t, r), r;
17
+ }, s = (e, t, n, i = Date.now()) => {
18
+ let s = o(a().signed, e), c = r(t), l = n ?? Infinity, u = s.get(c), d = Math.max(u?.longestMs ?? 0, l - i);
19
+ s.set(c, {
20
+ until: Math.max(u?.until ?? -Infinity, l),
21
+ longestMs: d
22
+ });
23
+ }, c = (e, t) => {
24
+ let n = a().declared;
25
+ n.set(e, Math.max(n.get(e) ?? 0, t));
26
+ }, l = (e, t, n) => {
27
+ a().configured.set(e, {
28
+ current: r(t),
29
+ previous: n === void 0 || n === "" || n === t ? null : r(n)
30
+ });
31
+ }, u = () => {
32
+ let e = a(), t = [...e.signed].flatMap(([e, t]) => [...t].map(([t, n]) => ({
33
+ purpose: e,
34
+ fingerprint: t,
35
+ signedUntil: n.until,
36
+ longestMs: n.longestMs
37
+ }))), n = [...e.presented].flatMap(([e, t]) => [...t].map(([t, n]) => ({
38
+ purpose: e,
39
+ fingerprint: t,
40
+ at: n
41
+ }))), r = [...e.configured].map(([e, t]) => ({
42
+ purpose: e,
43
+ ...t
44
+ })), i = [...e.declared].map(([e, t]) => ({
45
+ purpose: e,
46
+ lifetimeMs: t
47
+ }));
48
+ return e.signed.clear(), e.presented.clear(), {
49
+ signed: t,
50
+ configured: r,
51
+ presented: n,
52
+ declared: i
53
+ };
54
+ }, d = (e) => {
55
+ let t = /* @__PURE__ */ new Map();
56
+ for (let n of e) o(t, n.purpose).set(n.fingerprint, n.closesAt);
57
+ a().windows = t;
58
+ }, f = (e, t, n) => {
59
+ o(a().windows, e).set(r(t), n);
60
+ }, p = () => {
61
+ let e = a();
62
+ e.windows = /* @__PURE__ */ new Map(), e.signed.clear(), e.configured.clear(), e.presented.clear(), e.declared.clear();
63
+ }, m = (e, t, n = Date.now()) => {
64
+ let i = a().windows.get(e)?.get(r(t));
65
+ return i == null || n <= i;
66
+ }, h = (t) => e("sha256").update(t).digest(), g = (e, t, i, s) => {
67
+ let c = h(t), l = i !== "" && n(c, h(i)), u = s !== void 0 && s !== "" && s !== i && n(c, h(s));
68
+ return l ? !0 : !u || !m(e, s) ? !1 : (o(a().presented, e).set(r(s), Date.now()), !0);
69
+ };
70
+ export { d as applySecretWindows, g as bearerMatches, p as clearSecretWindows, u as drainSecretActivity, l as noteConfigured, s as noteSigned, c as noteSigningLifetime, m as previousAccepted, r as secretFingerprint, f as setSecretWindow };
@@ -1,4 +1,4 @@
1
- //#region src/serverErrorBus.ts
1
+ // src/serverErrorBus.ts
2
2
  var e = (e) => {
3
3
  let t = e.error, n = t instanceof Error ? t.name : typeof t?._tag == "string" ? String(t._tag) : "Error", r = [e.name === void 0 ? e.source : `${e.source} '${e.name}'`, ...typeof e.fields?.runId == "string" ? [`run ${e.fields.runId}`] : []].join(", "), i = /* @__PURE__ */ Error(`[message withheld: ${r} is declared encrypted — read it where the run is shown]`);
4
4
  i.name = n;
@@ -11,5 +11,4 @@ var e = (e) => {
11
11
  }, r = (e) => (t.add(e), () => {
12
12
  t.delete(e);
13
13
  });
14
- //#endregion
15
14
  export { n as publishServerError, r as subscribeServerErrors, e as withheldServerError };
@@ -1,5 +1,5 @@
1
1
  import { Schema as e } from "effect";
2
- //#region src/serviceCall.ts
2
+ // src/serviceCall.ts
3
3
  var t = class extends e.TaggedError()("ApiUnavailable", {
4
4
  api: e.String,
5
5
  tag: e.String,
@@ -10,5 +10,4 @@ var t = class extends e.TaggedError()("ApiUnavailable", {
10
10
  ]),
11
11
  message: e.String
12
12
  }) {}, n = (e) => typeof e == "object" && !!e && e._tag === "ApiUnavailable";
13
- //#endregion
14
13
  export { t as ApiUnavailable, n as isApiUnavailable };
package/dist/session.d.ts CHANGED
@@ -1,5 +1,12 @@
1
1
  import { Schema } from 'effect';
2
2
 
3
+ /** Replace every recorded window with the boot's computed ones. */
4
+ export declare const applySecretWindows: (windows: ReadonlyArray<{
5
+ readonly purpose: string;
6
+ readonly fingerprint: string;
7
+ readonly closesAt: number | null;
8
+ }>) => void;
9
+
3
10
  /**
4
11
  * Fail-closed check that a real session secret is configured. Throws a plain
5
12
  * `Error` (a fatal boot condition, not a wire error) when
@@ -8,6 +15,14 @@ import { Schema } from 'effect';
8
15
  */
9
16
  export declare const assertProductionSessionSecret: (env?: NodeJS.ProcessEnv) => void;
10
17
 
18
+ /**
19
+ * Does a presented bearer token match the current value or — during a
20
+ * rotation, while its window is open — the previous one? Both comparisons run,
21
+ * in constant time over digests, so neither which one matched nor a length
22
+ * leaks through timing. A match on the previous value is recorded.
23
+ */
24
+ export declare const bearerMatches: (purpose: string, presented: string, current: string, previous?: string) => boolean;
25
+
11
26
  /**
12
27
  * Build a `Set-Cookie` header value. Pure helper — the caller adds it
13
28
  * to the actual HTTP response. Used by the framework's login route
@@ -46,6 +61,9 @@ export declare const checkBearer: (headers: Readonly<Record<string, string | und
46
61
  readonly openWhenUnset?: boolean;
47
62
  }) => boolean;
48
63
 
64
+ /** Forget every record — a test seam, and a process that stops verifying. */
65
+ export declare const clearSecretWindows: () => void;
66
+
49
67
  /** Key id assigned to `VOLTRO_SESSION_SECRET_PREVIOUS` when no explicit
50
68
  * `VOLTRO_SESSION_KID_PREVIOUS` is set. The kid is a non-secret label —
51
69
  * rotation works with just the `_PREVIOUS` secret var. */
@@ -56,6 +74,9 @@ export declare const DEFAULT_PREVIOUS_SESSION_KID: "k-previous";
56
74
  * string produce the same label the resolver would. */
57
75
  export declare const DEFAULT_SESSION_KID: "k0";
58
76
 
77
+ /** What this process signed, holds and was presented since the last drain — for the boot to persist. */
78
+ export declare const drainSecretActivity: () => SecretActivity;
79
+
59
80
  /** A keyed signing secret. `kid` is stamped into the payload on sign and
60
81
  * matched on verify; it's an opaque, non-secret label (e.g. `'2026-06'`). */
61
82
  export declare interface KeyedSecret {
@@ -68,6 +89,31 @@ export declare interface KeyedSecret {
68
89
  * placeholders, not real secrets. */
69
90
  export declare const MIN_SESSION_SECRET_LENGTH: 32;
70
91
 
92
+ /** A resolver read `current` (and, during a rotation, `previous`) for `purpose`. */
93
+ export declare const noteConfigured: (purpose: string, current: string, previous: string | undefined) => void;
94
+
95
+ /**
96
+ * A signer signed something under `secret` that is valid until `expiresAtMs`
97
+ * (undefined: no expiry). The lifetime it signed with is recorded too: it is
98
+ * what bounds the tokens signed before anything was recorded.
99
+ */
100
+ export declare const noteSigned: (purpose: string, secret: string, expiresAtMs: number | undefined, now?: number) => void;
101
+
102
+ /**
103
+ * A configured signer can sign `purpose` tokens valid for up to `lifetimeMs` —
104
+ * declared when it is built, before it signs anything. The first rotation after
105
+ * an upgrade needs it: the previous value signed tokens nothing recorded, and
106
+ * only the configuration says how long they can live.
107
+ */
108
+ export declare const noteSigningLifetime: (purpose: string, lifetimeMs: number) => void;
109
+
110
+ /**
111
+ * Is this previous value still accepted? Yes while its window is open, and
112
+ * when no window is recorded for it (nothing has decided otherwise). No once
113
+ * the latest expiry it signed has passed, or a compromise closed it.
114
+ */
115
+ export declare const previousAccepted: (purpose: string, previous: string, now?: number) => boolean;
116
+
71
117
  /**
72
118
  * Read a cookie value by name from a `Cookie:` header string.
73
119
  * Returns undefined if the cookie isn't present.
@@ -123,6 +169,37 @@ export declare const resolveSessionSecret: () => string;
123
169
  */
124
170
  export declare const resolveSessionSecrets: () => SessionSecrets;
125
171
 
172
+ /** What a process recorded since the last drain: signatures, configured values, previous bearer values that answered. */
173
+ export declare interface SecretActivity {
174
+ /** Per purpose and fingerprint: the latest expiry signed and the longest lifetime signed with. */
175
+ readonly signed: ReadonlyArray<{
176
+ readonly purpose: string;
177
+ readonly fingerprint: string;
178
+ readonly signedUntil: number;
179
+ readonly longestMs: number;
180
+ }>;
181
+ /** Per purpose: the fingerprints of the current and the previous value this process holds. */
182
+ readonly configured: ReadonlyArray<{
183
+ readonly purpose: string;
184
+ readonly current: string;
185
+ readonly previous: string | null;
186
+ }>;
187
+ /** Per purpose: when a previous bearer value last answered. */
188
+ readonly presented: ReadonlyArray<{
189
+ readonly purpose: string;
190
+ readonly fingerprint: string;
191
+ readonly at: number;
192
+ }>;
193
+ /** Per purpose: the longest lifetime a configured signer declared. */
194
+ readonly declared: ReadonlyArray<{
195
+ readonly purpose: string;
196
+ readonly lifetimeMs: number;
197
+ }>;
198
+ }
199
+
200
+ /** A non-reversible name for a secret value: 16 hex of an HMAC under a fixed label. */
201
+ export declare const secretFingerprint: (value: string) => string;
202
+
126
203
  /**
127
204
  * The cookie the framework's own session strategy writes and reads.
128
205
  *
@@ -166,15 +243,16 @@ export declare const SESSION_PAYLOAD_VERSION: 2;
166
243
  */
167
244
  export declare const sessionExpiryFromHeaders: (headers: Record<string, string | undefined>) => number | undefined;
168
245
 
169
- export declare type SessionPayload = typeof SessionPayload_2.Type;
246
+ export declare type SessionPayload = typeof SessionPayloadSchema.Type;
170
247
 
171
- declare const SessionPayload_2: Schema.Struct<{
248
+ declare const SessionPayloadSchema: Schema.Struct<{
172
249
  /** Payload format version. Required and pinned — see the header. */
173
250
  readonly v: Schema.Literal<2>;
174
251
  /** WHO, never WHAT-THEY-MAY-DO. `SubjectIdentity` has no `scopes` field. */
175
252
  readonly subject: Schema.Union<readonly [Schema.Struct<{
176
253
  readonly type: Schema.Literal<"user">;
177
254
  readonly id: Schema.String;
255
+ readonly kind: Schema.optional<Schema.Literal<"guest">>;
178
256
  readonly tenantId: Schema.String;
179
257
  readonly metadata: Schema.optional<Schema.$Record<Schema.String, Schema.Unknown>>;
180
258
  }>, Schema.Struct<{
@@ -201,9 +279,9 @@ declare const SessionPayload_2: Schema.Struct<{
201
279
  }>, Schema.Struct<{
202
280
  readonly type: Schema.Literal<"service">;
203
281
  readonly id: Schema.String;
282
+ readonly service: Schema.String;
204
283
  readonly tenantId: Schema.Null;
205
284
  readonly metadata: Schema.optional<Schema.$Record<Schema.String, Schema.Unknown>>;
206
- readonly service: Schema.String;
207
285
  }>]>;
208
286
  /** Unix seconds when this session expires. Verify rejects past expiry. */
209
287
  readonly exp: Schema.Number;
@@ -217,8 +295,6 @@ declare const SessionPayload_2: Schema.Struct<{
217
295
  readonly kid: Schema.optional<Schema.String>;
218
296
  }>;
219
297
 
220
- declare type SessionPayload = typeof SessionPayload_2.Type;
221
-
222
298
  /** The resolved secret set: `current` always signs; `previous` is accepted
223
299
  * on verify during a rotation window. */
224
300
  export declare interface SessionSecrets {
@@ -242,6 +318,9 @@ export declare interface SetCookieOptions {
242
318
  readonly domain?: string;
243
319
  }
244
320
 
321
+ /** Record one window by value — a test seam and a single-process embedder's. */
322
+ export declare const setSecretWindow: (purpose: string, previous: string, closesAt: number | null) => void;
323
+
245
324
  export declare interface SignOptions {
246
325
  /** TTL in seconds. Defaults to 7 days. */
247
326
  readonly ttlSeconds?: number;
@@ -278,6 +357,9 @@ export declare const signSession: (subject: Subject | SubjectIdentity, secret: s
278
357
 
279
358
  declare const Subject: Schema.Union<readonly [Schema.Struct<{
280
359
  readonly type: Schema.Literal<"user">;
360
+ /** A guest has a stable identity but has not linked a registered account.
361
+ * This identity mark survives session signing; it never grants authority. */
362
+ readonly kind: Schema.optional<Schema.Literal<"guest">>;
281
363
  readonly id: Schema.String;
282
364
  readonly tenantId: Schema.String;
283
365
  readonly scopes: Schema.optional<Schema.$Array<Schema.String>>;
@@ -339,6 +421,7 @@ declare type Subject = typeof Subject.Type;
339
421
  declare const SubjectIdentity: Schema.Union<readonly [Schema.Struct<{
340
422
  readonly type: Schema.Literal<"user">;
341
423
  readonly id: Schema.String;
424
+ readonly kind: Schema.optional<Schema.Literal<"guest">>;
342
425
  readonly tenantId: Schema.String;
343
426
  readonly metadata: Schema.optional<Schema.$Record<Schema.String, Schema.Unknown>>;
344
427
  }>, Schema.Struct<{
@@ -381,9 +464,9 @@ declare const SubjectIdentity: Schema.Union<readonly [Schema.Struct<{
381
464
  }>, Schema.Struct<{
382
465
  readonly type: Schema.Literal<"service">;
383
466
  readonly id: Schema.String;
467
+ readonly service: Schema.String;
384
468
  readonly tenantId: Schema.Null;
385
469
  readonly metadata: Schema.optional<Schema.$Record<Schema.String, Schema.Unknown>>;
386
- readonly service: Schema.String;
387
470
  }>]>;
388
471
 
389
472
  declare type SubjectIdentity = typeof SubjectIdentity.Type;