@voltro/protocol 0.1.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/rest.js ADDED
@@ -0,0 +1,142 @@
1
+ import { s as e } from "./auth-DCE6m7Bo.js";
2
+ import { a as t, i as n, o as r, r as i, t as a } from "./serverErrorBus-C1KVL0dr.js";
3
+ import { Effect as o, Schema as s } from "effect";
4
+ //#region src/publicApi.ts
5
+ var c = (e, t) => t.method ?? (e === "query" ? "GET" : "POST"), l = (e, t) => t.path ?? `/${t.version ?? "v1"}/${e.replace(/\./g, "/")}`, u = (e, t) => {
6
+ let n = e.publicApi;
7
+ if (n === void 0) throw Error(`publicApiRoute: descriptor '${e.name}' has no publicApi annotation`);
8
+ let r = c(e.kind, n), i = l(e.name, n), a = r === "GET" ? "query" : "body", o = s.Struct({ [a]: e.input }), u = (n.scopes ?? []).map((e) => f(e));
9
+ return p({
10
+ method: r,
11
+ path: i,
12
+ input: o,
13
+ output: e.output,
14
+ handler: (e, n) => t(e[a], n),
15
+ ...u.length > 0 ? { guards: u } : {},
16
+ ...n.summary === void 0 ? {} : { summary: n.summary },
17
+ ...n.description === void 0 ? {} : { description: n.description },
18
+ ...n.deprecated === void 0 ? {} : { deprecated: n.deprecated },
19
+ ...n.sunset === void 0 ? {} : { sunset: n.sunset }
20
+ });
21
+ }, d = (e) => e.filter((e) => e.descriptor.publicApi !== void 0).map((e) => u(e.descriptor, e.invoke)).sort((e, t) => e.path.localeCompare(t.path)), f = (e) => (t) => {
22
+ let n = t.subject.scopes;
23
+ if (!(n && (n.includes(e) || n.includes("admin:full")))) return {
24
+ status: 403,
25
+ message: `Missing required scope: ${e}`
26
+ };
27
+ }, p = (e) => e, m = (e, t, n) => ({
28
+ status: e,
29
+ contentType: "application/json; charset=utf-8",
30
+ body: JSON.stringify(t),
31
+ ...n ? { headers: n } : {}
32
+ }), h = (e) => {
33
+ if (e.length === 0) return;
34
+ let t = new TextDecoder().decode(e);
35
+ if (t.trim() !== "") return JSON.parse(t);
36
+ }, g = (e) => {
37
+ let t = {};
38
+ for (let [n, r] of new URLSearchParams(e)) t[n] = r;
39
+ return t;
40
+ }, _ = (e, t) => {
41
+ let n = e.split("/").filter((e) => e.length > 0), r = t.split("/").filter((e) => e.length > 0), i = {};
42
+ for (let e = 0; e < n.length; e++) {
43
+ let t = n[e];
44
+ if (t !== void 0 && t.startsWith(":")) {
45
+ let n = r[e];
46
+ if (n !== void 0) try {
47
+ i[t.slice(1)] = decodeURIComponent(n);
48
+ } catch {
49
+ i[t.slice(1)] = n;
50
+ }
51
+ }
52
+ }
53
+ return i;
54
+ }, v = (e, t) => ({
55
+ query: g(e.query),
56
+ params: t,
57
+ body: h(e.rawBody)
58
+ }), y = /* @__PURE__ */ new Set([
59
+ "POST",
60
+ "PUT",
61
+ "PATCH",
62
+ "DELETE"
63
+ ]), b = (c, l) => {
64
+ let u = c.input ? s.decodeUnknown(c.input) : void 0, d = s.encode(c.output), f = {};
65
+ return c.deprecated !== void 0 && (f.Deprecation = "true"), c.sunset !== void 0 && (f.Sunset = c.sunset), {
66
+ method: "*",
67
+ path: c.path,
68
+ handle: async (s) => {
69
+ if (s.method.toUpperCase() !== c.method) return m(405, {
70
+ error: "Method Not Allowed",
71
+ allow: c.method
72
+ }, {
73
+ ...f,
74
+ Allow: c.method
75
+ });
76
+ if (c.sunset !== void 0) {
77
+ let e = Date.parse(c.sunset);
78
+ if (!Number.isNaN(e) && Date.now() >= e) return m(410, {
79
+ error: "Gone",
80
+ ...c.deprecated === void 0 ? {} : { replacement: c.deprecated }
81
+ }, f);
82
+ }
83
+ let p;
84
+ if (u) {
85
+ let e = v(s, _(c.path, s.path)), t = await o.runPromise(u(e).pipe(o.map((e) => ({
86
+ ok: !0,
87
+ value: e
88
+ })), o.catchAll((e) => o.succeed({
89
+ ok: !1,
90
+ cause: e
91
+ }))));
92
+ if (!t.ok) return m(400, {
93
+ error: "Invalid request",
94
+ detail: String(t.cause)
95
+ }, f);
96
+ p = t.value;
97
+ }
98
+ let h = l.resolveSubject ? await l.resolveSubject(s.headers) : e(s.headers["x-tenant"] ?? null), g = {
99
+ subject: h,
100
+ headers: s.headers,
101
+ store: l.store
102
+ };
103
+ if (c.guards) for (let e of c.guards) {
104
+ let t = await e(g);
105
+ if (t) return m(t.status, { error: t.message }, f);
106
+ }
107
+ let b = l.idempotency, x = b && y.has(c.method) ? s.headers[b.header.toLowerCase()] : void 0, S = b && x ? r(h.tenantId, c.method, c.path) : "";
108
+ if (b && x) {
109
+ let e = await i(b.store, S, x, b.ttlMs, Date.now());
110
+ if (e.kind === "replay") return m(e.response.status, e.response.body, {
111
+ ...f,
112
+ "Idempotency-Replayed": "true"
113
+ });
114
+ if (e.kind === "conflict") return m(409, { error: "A request with this Idempotency-Key is already being processed" }, f);
115
+ }
116
+ try {
117
+ let e = await c.handler(p, g), n = await o.runPromise(d(e));
118
+ return b && x && await t(b.store, S, x, {
119
+ status: 200,
120
+ body: n
121
+ }, Date.now()), m(200, n, f);
122
+ } catch (e) {
123
+ if (b && x && await n(b.store, S, x), typeof e == "object" && e && typeof e.status == "number") {
124
+ let t = e;
125
+ return m(t.status, { error: t.message ?? "Error" }, f);
126
+ }
127
+ return a({
128
+ error: e,
129
+ source: "rest",
130
+ name: `${c.method} ${c.path}`,
131
+ fields: { status: 500 }
132
+ }), m(500, { error: "Internal Server Error" }, f);
133
+ }
134
+ }
135
+ };
136
+ }, x = (e, t = {}) => e.map((e) => b(e, t)), S = async (e, t) => {
137
+ let n = await e[0].handle(t);
138
+ for (let r = 1; r < e.length && n.status === 405; r++) n = await e[r].handle(t);
139
+ return n;
140
+ };
141
+ //#endregion
142
+ export { d as collectPublicApiRoutes, p as defineRestRoute, c as derivePublicMethod, l as derivePublicPath, S as dispatchSharedPath, _ as matchPathParams, u as publicApiRoute, f as requireScope, x as restRoutesToHttpRoutes };
Binary file
@@ -0,0 +1,262 @@
1
+ import { Schema } from 'effect';
2
+
3
+ /**
4
+ * Fail-closed check that a real session secret is configured. Throws a plain
5
+ * `Error` (a fatal boot condition, not a wire error) when `NODE_ENV` is
6
+ * `'production'` and `VOLTRO_SESSION_SECRET` is missing, the dev fallback, or
7
+ * too short. No-op otherwise. Idempotent — safe to call from every prod entry.
8
+ */
9
+ export declare const assertProductionSessionSecret: (env?: NodeJS.ProcessEnv) => void;
10
+
11
+ /**
12
+ * Build a `Set-Cookie` header value. Pure helper — the caller adds it
13
+ * to the actual HTTP response. Used by the framework's login route
14
+ * pattern + by user code that wants to set its own cookies during a
15
+ * mutation/action.
16
+ */
17
+ export declare const buildSetCookie: (name: string, value: string, options?: SetCookieOptions) => string;
18
+
19
+ /**
20
+ * Check an `Authorization: Bearer <token>` header against an expected token.
21
+ *
22
+ * - `expected` unset/empty → `true` (no token configured = open; the caller
23
+ * decided not to gate this surface).
24
+ * - header absent, or not using the `Bearer ` scheme → `false`.
25
+ * - presented token compared to `expected` in CONSTANT TIME.
26
+ *
27
+ * `headers` keys may be any casing (`authorization` / `Authorization`).
28
+ */
29
+ export declare const checkBearer: (headers: Readonly<Record<string, string | undefined>>, expected: string | undefined) => boolean;
30
+
31
+ /** Key id assigned to `VOLTRO_SESSION_SECRET_PREVIOUS` when no explicit
32
+ * `VOLTRO_SESSION_KID_PREVIOUS` is set. The kid is a non-secret label —
33
+ * rotation works with just the `_PREVIOUS` secret var. */
34
+ export declare const DEFAULT_PREVIOUS_SESSION_KID: "k-previous";
35
+
36
+ /** Key id stamped for bare-string secrets / when `VOLTRO_SESSION_KID` is
37
+ * unset. Exported so callers building a `SessionSecrets` from a plain
38
+ * string produce the same label the resolver would. */
39
+ export declare const DEFAULT_SESSION_KID: "k0";
40
+
41
+ /** A keyed signing secret. `kid` is stamped into the payload on sign and
42
+ * matched on verify; it's an opaque, non-secret label (e.g. `'2026-06'`). */
43
+ export declare interface KeyedSecret {
44
+ readonly kid: string;
45
+ readonly secret: string;
46
+ }
47
+
48
+ /** Minimum length we accept for a production session secret. 32 bytes of
49
+ * entropy is the floor for HMAC-SHA256; shorter values are almost always
50
+ * hand-typed placeholders, not real secrets. */
51
+ export declare const MIN_SESSION_SECRET_LENGTH: 32;
52
+
53
+ /**
54
+ * Read a cookie value by name from a `Cookie:` header string.
55
+ * Returns undefined if the cookie isn't present.
56
+ */
57
+ export declare const readCookie: (header: string | undefined, name: string) => string | undefined;
58
+
59
+ /** Resolve the HMAC session secret with the dev fallback applied. */
60
+ export declare const resolveSessionSecret: () => string;
61
+
62
+ /**
63
+ * Resolve the keyed session-secret set for multi-key rotation.
64
+ *
65
+ * - `current` is built from `VOLTRO_SESSION_SECRET` (dev fallback
66
+ * applied), keyed `VOLTRO_SESSION_KID` (defaults to `'k0'`).
67
+ * - `previous` is built from `VOLTRO_SESSION_SECRET_PREVIOUS`, keyed
68
+ * `VOLTRO_SESSION_KID_PREVIOUS` (defaults to `'k-previous'` — the kid
69
+ * is a non-secret label, so rotation needs only the secret var).
70
+ * During a rotation window you set the new secret as current and move
71
+ * the old one into `VOLTRO_SESSION_SECRET_PREVIOUS`; sessions signed
72
+ * with the old key keep verifying until they expire, then you drop
73
+ * the `_PREVIOUS` var.
74
+ *
75
+ * Sign always uses `current`. Verify tries `current` first, then
76
+ * `previous` if present.
77
+ */
78
+ export declare const resolveSessionSecrets: () => SessionSecrets;
79
+
80
+ export declare type SessionPayload = typeof SessionPayload_2.Type;
81
+
82
+ declare const SessionPayload_2: Schema.Struct<{
83
+ subject: Schema.Union<[Schema.Struct<{
84
+ type: Schema.Literal<["user"]>;
85
+ id: typeof Schema.String;
86
+ tenantId: typeof Schema.String;
87
+ scopes: Schema.optional<Schema.Array$<typeof Schema.String>>;
88
+ metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
89
+ }>, Schema.Struct<{
90
+ type: Schema.Literal<["apiKey"]>;
91
+ id: typeof Schema.String;
92
+ tenantId: typeof Schema.String;
93
+ scopes: Schema.optional<Schema.Array$<typeof Schema.String>>;
94
+ metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
95
+ }>, Schema.Struct<{
96
+ type: Schema.Literal<["serviceAccount"]>;
97
+ id: typeof Schema.String;
98
+ tenantId: typeof Schema.String;
99
+ scopes: Schema.optional<Schema.Array$<typeof Schema.String>>;
100
+ metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
101
+ }>, Schema.Struct<{
102
+ type: Schema.Literal<["anonymous"]>;
103
+ id: typeof Schema.Null;
104
+ tenantId: Schema.NullOr<typeof Schema.String>;
105
+ }>, Schema.Struct<{
106
+ type: Schema.Literal<["system"]>;
107
+ id: typeof Schema.String;
108
+ tenantId: typeof Schema.Null;
109
+ scopes: Schema.optional<Schema.Array$<typeof Schema.String>>;
110
+ metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
111
+ }>]>;
112
+ /** Unix seconds when this session expires. Verify rejects past expiry. */
113
+ exp: typeof Schema.Number;
114
+ /** Unix seconds when this session was issued. Drives sliding-window
115
+ * auto-renewal: verify signals a renew once `now - iat` passes the
116
+ * renewal threshold (default 70% of the session's lifetime). */
117
+ iat: typeof Schema.Number;
118
+ /** Key id of the signing secret. Stamped from the current key on sign;
119
+ * read on verify to pick which secret to check first. Absent on
120
+ * values minted with a bare-string secret (single-key path). */
121
+ kid: Schema.optional<typeof Schema.String>;
122
+ }>;
123
+
124
+ declare type SessionPayload = typeof SessionPayload_2.Type;
125
+
126
+ /** The resolved secret set: `current` always signs; `previous` is accepted
127
+ * on verify during a rotation window. */
128
+ export declare interface SessionSecrets {
129
+ readonly current: KeyedSecret;
130
+ readonly previous?: KeyedSecret;
131
+ }
132
+
133
+ export declare interface SetCookieOptions {
134
+ /** Cookie path. Default `/`. */
135
+ readonly path?: string;
136
+ /** Max-Age in seconds. Omit for a session cookie that clears on
137
+ * browser close. Set 0 (or a past date) to delete. */
138
+ readonly maxAgeSeconds?: number;
139
+ /** Defaults to true. Block JS access to the cookie. */
140
+ readonly httpOnly?: boolean;
141
+ /** Defaults to 'lax'. Use 'strict' for higher-security flows. */
142
+ readonly sameSite?: 'strict' | 'lax' | 'none';
143
+ /** Defaults to true. Required when sameSite='none'. */
144
+ readonly secure?: boolean;
145
+ /** Cookie domain. Omit to scope to the current host. */
146
+ readonly domain?: string;
147
+ }
148
+
149
+ export declare interface SignOptions {
150
+ /** TTL in seconds. Defaults to 7 days. */
151
+ readonly ttlSeconds?: number;
152
+ /** Clock override (epoch milliseconds). Defaults to `Date.now`. A
153
+ * testing seam — lets suites mint back-dated sessions to exercise the
154
+ * sliding-window renewal without wall-clock sleeps. */
155
+ readonly now?: () => number;
156
+ }
157
+
158
+ /**
159
+ * Mint a signed session cookie value for the given subject. Returns the
160
+ * value you'd put in `Set-Cookie: <name>=<value>; HttpOnly; SameSite=Lax;
161
+ * Secure; Path=/; Max-Age=<ttl>`.
162
+ *
163
+ * `secret` accepts either a bare string (single-key path — the `kid` is
164
+ * omitted from the payload) or a `KeyedSecret` (multi-key rotation — the
165
+ * `kid` is stamped so verify can pick the matching key first).
166
+ *
167
+ * The cookie value itself is opaque — clients can read it but can't
168
+ * forge a new one without the secret. `HttpOnly` blocks JS access for
169
+ * defence-in-depth; the verify side doesn't care whether the client
170
+ * could read it.
171
+ */
172
+ export declare const signSession: (subject: Subject, secret: string | KeyedSecret, options?: SignOptions) => string;
173
+
174
+ declare const Subject: Schema.Union<[Schema.Struct<{
175
+ type: Schema.Literal<["user"]>;
176
+ id: typeof Schema.String;
177
+ tenantId: typeof Schema.String;
178
+ scopes: Schema.optional<Schema.Array$<typeof Schema.String>>;
179
+ metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
180
+ }>, Schema.Struct<{
181
+ type: Schema.Literal<["apiKey"]>;
182
+ id: typeof Schema.String;
183
+ tenantId: typeof Schema.String;
184
+ scopes: Schema.optional<Schema.Array$<typeof Schema.String>>;
185
+ metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
186
+ }>, Schema.Struct<{
187
+ type: Schema.Literal<["serviceAccount"]>;
188
+ id: typeof Schema.String;
189
+ tenantId: typeof Schema.String;
190
+ scopes: Schema.optional<Schema.Array$<typeof Schema.String>>;
191
+ metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
192
+ }>, Schema.Struct<{
193
+ type: Schema.Literal<["anonymous"]>;
194
+ id: typeof Schema.Null;
195
+ tenantId: Schema.NullOr<typeof Schema.String>;
196
+ }>, Schema.Struct<{
197
+ type: Schema.Literal<["system"]>;
198
+ id: typeof Schema.String;
199
+ tenantId: typeof Schema.Null;
200
+ scopes: Schema.optional<Schema.Array$<typeof Schema.String>>;
201
+ metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
202
+ }>]>;
203
+
204
+ declare type Subject = typeof Subject.Type;
205
+
206
+ /**
207
+ * Constant-time string equality via `timingSafeEqual`. Returns false fast
208
+ * on a length mismatch (the lengths aren't secret); only equal-length inputs
209
+ * reach the timing-safe compare, so an attacker can't recover the secret a
210
+ * byte at a time from the response time.
211
+ */
212
+ export declare const timingSafeStringEqual: (a: string, b: string) => boolean;
213
+
214
+ export declare interface VerifyOptions {
215
+ /** Fraction of [iat, exp] after which `renew` flips true. Default 0.7. */
216
+ readonly renewFraction?: number;
217
+ /** Clock override (epoch milliseconds). Defaults to `Date.now`. */
218
+ readonly now?: () => number;
219
+ }
220
+
221
+ /** Outcome of a keyed verify. `renew` is true once the session has crossed
222
+ * the sliding-window threshold — the caller re-issues the cookie so an
223
+ * active user never gets logged out mid-session. */
224
+ export declare interface VerifyResult {
225
+ readonly subject: Subject;
226
+ readonly renew: boolean;
227
+ /** The key id that verified the value (current or previous). */
228
+ readonly kid: string;
229
+ /** Unix seconds the verified session expires. */
230
+ readonly exp: number;
231
+ /** Unix seconds the verified session was issued. `exp - iat` is the
232
+ * session's original lifetime — a renewing caller re-issues with the
233
+ * same lifetime rather than resetting to the default TTL. */
234
+ readonly iat: number;
235
+ }
236
+
237
+ /**
238
+ * Verify a session cookie value. Returns the decoded Subject on success,
239
+ * null on any failure (malformed, signature mismatch, expired). Never
240
+ * throws — callers shouldn't have to wrap this in try/catch.
241
+ *
242
+ * Uses `timingSafeEqual` for the signature comparison so attackers
243
+ * can't recover bytes one-at-a-time via response-time analysis.
244
+ *
245
+ * This is the single-secret entry point and stays a pure
246
+ * `Subject | null`. Use `verifySessionKeyed` for the multi-key +
247
+ * sliding-window result shape.
248
+ */
249
+ export declare const verifySession: (cookieValue: string, secret: string) => Subject | null;
250
+
251
+ /**
252
+ * Multi-key + sliding-window verify. Tries `secrets.current` first, then
253
+ * `secrets.previous` (if present). On success returns the Subject, the
254
+ * `kid` that verified, and a `renew` flag the caller acts on by
255
+ * re-issuing the cookie (so the session slides forward while in use).
256
+ * Returns null on any failure.
257
+ */
258
+ export declare const verifySessionKeyed: (cookieValue: string, secrets: SessionSecrets, options?: VerifyOptions) => VerifyResult | null;
259
+
260
+ export declare const VOLTRO_DEV_SESSION_SECRET: "voltro-dev-session-secret-32b-DO-NOT-USE-IN-PROD";
261
+
262
+ export { }
@@ -0,0 +1,106 @@
1
+ import { i as e } from "./auth-DCE6m7Bo.js";
2
+ import { Schema as t } from "effect";
3
+ import { createHmac as n, timingSafeEqual as r } from "node:crypto";
4
+ //#region src/session.ts
5
+ var i = t.Struct({
6
+ subject: e,
7
+ exp: t.Number,
8
+ iat: t.Number,
9
+ kid: t.optional(t.String)
10
+ }), a = "k0", o = "k-previous", s = "voltro-dev-session-secret-32b-DO-NOT-USE-IN-PROD", c = () => process.env.VOLTRO_SESSION_SECRET ?? "voltro-dev-session-secret-32b-DO-NOT-USE-IN-PROD", l = () => {
11
+ let e = {
12
+ kid: process.env.VOLTRO_SESSION_KID ?? "k0",
13
+ secret: process.env.VOLTRO_SESSION_SECRET ?? "voltro-dev-session-secret-32b-DO-NOT-USE-IN-PROD"
14
+ }, t = process.env.VOLTRO_SESSION_SECRET_PREVIOUS;
15
+ return t ? {
16
+ current: e,
17
+ previous: {
18
+ kid: process.env.VOLTRO_SESSION_KID_PREVIOUS ?? "k-previous",
19
+ secret: t
20
+ }
21
+ } : { current: e };
22
+ }, u = 32, d = (e = process.env) => {
23
+ if (e.NODE_ENV !== "production") return;
24
+ let t = e.VOLTRO_SESSION_SECRET;
25
+ if (t === void 0 || t === "") throw Error("VOLTRO_SESSION_SECRET is not set. In production the framework refuses to boot rather than sign session cookies with the PUBLIC dev fallback secret (which would let anyone forge any user's session). Set VOLTRO_SESSION_SECRET to a high-entropy value (>= 32 chars). Generate one with `voltro secret generate session`.");
26
+ if (t === "voltro-dev-session-secret-32b-DO-NOT-USE-IN-PROD") throw Error("VOLTRO_SESSION_SECRET is set to the built-in dev fallback secret, which is PUBLIC in the framework source. Set a real, high-entropy secret in production. Generate one with `voltro secret generate session`.");
27
+ if (t.length < 32) throw Error(`VOLTRO_SESSION_SECRET is only ${t.length} chars — too short for a production session secret (need >= 32). This looks like a placeholder. Generate a real one with \`voltro secret generate session\`.`);
28
+ }, f = t.encodeSync(i), p = t.decodeUnknownSync(i), m = (e) => (typeof e == "string" ? Buffer.from(e, "utf8") : e).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""), h = (e) => {
29
+ let t = e.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - e.length % 4) % 4);
30
+ return Buffer.from(t, "base64");
31
+ }, g = 3600 * 24 * 7, _ = .7, v = (e, t) => m(n("sha256", t).update(e).digest()), y = (e) => typeof e == "string" ? {
32
+ kid: "k0",
33
+ secret: e
34
+ } : e, b = (e, t, n = {}) => {
35
+ let r = y(t);
36
+ if (!r.secret) throw Error("signSession: secret must be a non-empty string");
37
+ let i = n.ttlSeconds ?? g, a = Math.floor((n.now ?? Date.now)() / 1e3), o = {
38
+ subject: e,
39
+ exp: a + i,
40
+ iat: a,
41
+ ...typeof t == "string" ? {} : { kid: r.kid }
42
+ }, s = m(JSON.stringify(f(o)));
43
+ return `${s}.${v(s, r.secret)}`;
44
+ }, x = (e, t, i, a) => {
45
+ let o = n("sha256", i).update(e).digest(), s = h(t);
46
+ if (s.length !== o.length || !r(s, o)) return null;
47
+ try {
48
+ let t = h(e).toString("utf8"), n = p(JSON.parse(t));
49
+ return n.exp < Math.floor(a() / 1e3) ? null : n;
50
+ } catch {
51
+ return null;
52
+ }
53
+ }, S = (e, t) => {
54
+ if (!e || !t) return null;
55
+ let n = e.indexOf(".");
56
+ if (n <= 0 || n === e.length - 1) return null;
57
+ let r = x(e.slice(0, n), e.slice(n + 1), t, Date.now);
58
+ return r ? r.subject : null;
59
+ }, C = (e, t, n = {}) => {
60
+ if (!e) return null;
61
+ let r = e.indexOf(".");
62
+ if (r <= 0 || r === e.length - 1) return null;
63
+ let i = e.slice(0, r), a = e.slice(r + 1), o = t.previous ? [t.current, t.previous] : [t.current], s = n.now ?? Date.now;
64
+ for (let e of o) {
65
+ if (!e.secret) continue;
66
+ let t = x(i, a, e.secret, s);
67
+ if (!t) continue;
68
+ let r = n.renewFraction ?? _, o = Math.floor(s() / 1e3), c = t.exp - t.iat, l = t.iat + c * r;
69
+ return {
70
+ subject: t.subject,
71
+ renew: o >= l,
72
+ kid: e.kid,
73
+ exp: t.exp,
74
+ iat: t.iat
75
+ };
76
+ }
77
+ return null;
78
+ }, w = (e, t) => {
79
+ let n = Buffer.from(e, "utf8"), i = Buffer.from(t, "utf8");
80
+ return n.length === i.length && r(n, i);
81
+ }, T = (e, t) => {
82
+ if (!t || t.length === 0) return !0;
83
+ let n = e.authorization ?? e.Authorization;
84
+ return !n || !n.startsWith("Bearer ") ? !1 : w(n.slice(7).trim(), t);
85
+ }, E = (e, t) => {
86
+ if (e) for (let n of e.split(";")) {
87
+ let e = n.indexOf("=");
88
+ if (e < 0 || n.slice(0, e).trim() !== t) continue;
89
+ let r = n.slice(e + 1).trim();
90
+ r.startsWith("\"") && r.endsWith("\"") && (r = r.slice(1, -1));
91
+ try {
92
+ return decodeURIComponent(r);
93
+ } catch {
94
+ return r;
95
+ }
96
+ }
97
+ }, D = (e, t, n = {}) => {
98
+ let r = [`${e}=${encodeURIComponent(t)}`];
99
+ if (r.push(`Path=${n.path ?? "/"}`), n.maxAgeSeconds !== void 0 && r.push(`Max-Age=${n.maxAgeSeconds}`), r.push("HttpOnly"), n.httpOnly === !1) {
100
+ let e = r.indexOf("HttpOnly");
101
+ e >= 0 && r.splice(e, 1);
102
+ }
103
+ return r.push(`SameSite=${n.sameSite ?? "Lax"}`), (n.secure ?? !0) && r.push("Secure"), n.domain && r.push(`Domain=${n.domain}`), r.join("; ");
104
+ };
105
+ //#endregion
106
+ export { o as DEFAULT_PREVIOUS_SESSION_KID, a as DEFAULT_SESSION_KID, u as MIN_SESSION_SECRET_LENGTH, s as VOLTRO_DEV_SESSION_SECRET, d as assertProductionSessionSecret, D as buildSetCookie, T as checkBearer, E as readCookie, c as resolveSessionSecret, l as resolveSessionSecrets, b as signSession, w as timingSafeStringEqual, S as verifySession, C as verifySessionKeyed };
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@voltro/protocol",
3
+ "version": "0.1.0",
4
+ "description": "The Voltro wire + plugin contract — defineQuery/Mutation/Action/Stream, definePlugin, sessions / JWT / API-keys, and the RPC protocol.",
5
+ "keywords": [
6
+ "voltro",
7
+ "typescript",
8
+ "framework"
9
+ ],
10
+ "license": "SEE LICENSE IN LICENSE",
11
+ "homepage": "https://voltro.dev",
12
+ "bugs": {
13
+ "email": "support@voltro.dev"
14
+ },
15
+ "author": {
16
+ "name": "Voltro UG",
17
+ "url": "https://voltro.dev"
18
+ },
19
+ "type": "module",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js",
24
+ "default": "./dist/index.js"
25
+ },
26
+ "./rest": {
27
+ "types": "./dist/rest.d.ts",
28
+ "import": "./dist/rest.js",
29
+ "default": "./dist/rest.js"
30
+ },
31
+ "./session": {
32
+ "types": "./dist/session.d.ts",
33
+ "import": "./dist/session.js",
34
+ "default": "./dist/session.js"
35
+ },
36
+ "./jwt": {
37
+ "types": "./dist/jwt.d.ts",
38
+ "import": "./dist/jwt.js",
39
+ "default": "./dist/jwt.js"
40
+ },
41
+ "./apikey": {
42
+ "types": "./dist/apikey.d.ts",
43
+ "import": "./dist/apikey.js",
44
+ "default": "./dist/apikey.js"
45
+ }
46
+ },
47
+ "main": "./dist/index.js",
48
+ "module": "./dist/index.js",
49
+ "types": "./dist/index.d.ts",
50
+ "sideEffects": false,
51
+ "engines": {
52
+ "node": ">=24.0.0"
53
+ },
54
+ "dependencies": {
55
+ "@effect/sql": "^0.51.1",
56
+ "@voltro/database": "0.1.0",
57
+ "jose": "^6.2.3"
58
+ },
59
+ "peerDependencies": {
60
+ "@effect/platform": "^0.96.2",
61
+ "@effect/rpc": "^0.75.1",
62
+ "effect": "^3.21.4"
63
+ },
64
+ "publishConfig": {
65
+ "access": "public"
66
+ }
67
+ }