@voltro/protocol 0.25.0 → 0.27.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/CHANGELOG.md +541 -0
- package/dist/apikey.d.ts +19 -6
- package/dist/{auth-BPdyOBsd.js → auth-CXMrvPyX.js} +9 -3
- package/dist/index.d.ts +156 -17
- package/dist/index.js +31 -8
- package/dist/jwt.d.ts +19 -6
- package/dist/jwt.js +19 -13
- package/dist/rest.d.ts +66 -0
- package/dist/rest.js +1 -1
- package/dist/session.d.ts +27 -0
- package/dist/session.js +7 -2
- package/package.json +2 -2
package/dist/apikey.d.ts
CHANGED
|
@@ -150,15 +150,28 @@ declare interface AuthStrategyInput {
|
|
|
150
150
|
readonly store?: DataStore;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
/** A strategy's verdict on a request.
|
|
154
|
-
* - `matched`: this strategy claims the request; here's the Subject.
|
|
155
|
-
* - `skip`: not my request (e.g. cookie absent); try next strategy.
|
|
156
|
-
* - `failed`: this IS my request BUT validation failed (signature
|
|
157
|
-
* mismatch, expired JWT). Composer bails to anonymous + logs;
|
|
158
|
-
* silently falling through would mask attacks. */
|
|
159
153
|
declare type StrategyResolution = {
|
|
160
154
|
readonly kind: 'matched';
|
|
161
155
|
readonly subject: Subject;
|
|
156
|
+
/**
|
|
157
|
+
* When this credential expires, in unix SECONDS — if the strategy knows.
|
|
158
|
+
*
|
|
159
|
+
* The strategy is the ONLY place in the system that has verified the token
|
|
160
|
+
* and holds its `exp`, and until now it could not say so. The credential
|
|
161
|
+
* bound therefore read one source: the `voltro:session` cookie. For an app
|
|
162
|
+
* authenticating with Bearer JWTs — six of our own catalog strategies do,
|
|
163
|
+
* and every one of them verifies an `exp` — it was silently `undefined`,
|
|
164
|
+
* so "a subscription can no longer outlive the credential that authorized
|
|
165
|
+
* it" was a no-op that read as a guarantee.
|
|
166
|
+
*
|
|
167
|
+
* A reporter found it by expecting black screens an hour after a deploy
|
|
168
|
+
* and getting none. Their conclusion is the one to keep: the guarantee was
|
|
169
|
+
* not false, it was scoped to an auth shape the sentence did not name.
|
|
170
|
+
*
|
|
171
|
+
* Optional, and absent still means no bound — the failure direction is the
|
|
172
|
+
* behaviour that already existed.
|
|
173
|
+
*/
|
|
174
|
+
readonly credentialExpiresAt?: number;
|
|
162
175
|
} | {
|
|
163
176
|
readonly kind: 'skip';
|
|
164
177
|
} | {
|
|
@@ -64,7 +64,13 @@ var r = t.Record({
|
|
|
64
64
|
};
|
|
65
65
|
for (let n of e) {
|
|
66
66
|
let e = await n.resolve(i);
|
|
67
|
-
if (e.kind === "matched")
|
|
67
|
+
if (e.kind === "matched") {
|
|
68
|
+
let n = t?.resolveScopes === void 0 ? e.subject : b(e.subject, await t.resolveScopes(e.subject, i));
|
|
69
|
+
return e.credentialExpiresAt === void 0 ? { subject: n } : {
|
|
70
|
+
subject: n,
|
|
71
|
+
credentialExpiresAt: e.credentialExpiresAt
|
|
72
|
+
};
|
|
73
|
+
}
|
|
68
74
|
if (e.kind === "failed") {
|
|
69
75
|
t?.onStrategyFailed?.({
|
|
70
76
|
strategyId: n.id,
|
|
@@ -73,10 +79,10 @@ var r = t.Record({
|
|
|
73
79
|
break;
|
|
74
80
|
}
|
|
75
81
|
}
|
|
76
|
-
if (t?.fallback) return t.fallback(i);
|
|
82
|
+
if (t?.fallback) return { subject: await t.fallback(i) };
|
|
77
83
|
let a = i.headers["x-tenant"] ?? null;
|
|
78
84
|
if (a === null && t?.anonymousTenantRequired === !0) throw new S({ reason: "tenant required (x-tenant header missing)" });
|
|
79
|
-
return d(a);
|
|
85
|
+
return { subject: d(a) };
|
|
80
86
|
}, S = class extends t.TaggedError()("Unauthenticated", { reason: t.optional(t.String) }) {}, C = (e, t) => {
|
|
81
87
|
if (e.type === "anonymous") throw new S(t === void 0 ? {} : { reason: t });
|
|
82
88
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -38,6 +38,28 @@ export declare interface ActionProcedureDescriptor<Name extends string, Input ex
|
|
|
38
38
|
readonly exposeAsTool: ExposeAsTool | undefined;
|
|
39
39
|
/** True when the procedure is kept OFF the wire — no client-group entry and no
|
|
40
40
|
* route in dev or serve. See `internal` on the definer's options. */
|
|
41
|
+
/**
|
|
42
|
+
* Replace a PLUGIN route that answers to this same tag.
|
|
43
|
+
*
|
|
44
|
+
* Without it, a user route and a plugin route sharing a tag is a hard error,
|
|
45
|
+
* and correctly so — two handlers behind one name is not a thing a caller can
|
|
46
|
+
* reason about. But refusing is the wrong answer when the app deliberately
|
|
47
|
+
* wants its own version: the two escapes available otherwise are to rename
|
|
48
|
+
* your procedure (so the split runs along "who built it" rather than along a
|
|
49
|
+
* domain boundary) or to `alias` the whole plugin away (same, one level up).
|
|
50
|
+
* For a frontend developer that is the worst possible partition.
|
|
51
|
+
*
|
|
52
|
+
* A reporter wanted exactly this: adopt `@voltro/plugin-notifications`, whose
|
|
53
|
+
* surface is richer than theirs, add `archive`/`unarchive` beside it — which
|
|
54
|
+
* already composes, since the collision check compares FULL tags and not
|
|
55
|
+
* prefixes — and replace `markRead`, because theirs maintains archive state.
|
|
56
|
+
*
|
|
57
|
+
* Explicit, never inferred. Silently letting the app win would mean a plugin
|
|
58
|
+
* upgrade that adds a route could shadow an app procedure with no diff to
|
|
59
|
+
* read; declaring it makes the intent reviewable and puts the override in the
|
|
60
|
+
* file that performs it.
|
|
61
|
+
*/
|
|
62
|
+
readonly overridesPlugin: boolean | undefined;
|
|
41
63
|
readonly internal: boolean | undefined;
|
|
42
64
|
}
|
|
43
65
|
|
|
@@ -414,7 +436,7 @@ export declare const composeAuthStrategies: (strategies: ReadonlyArray<AuthStrat
|
|
|
414
436
|
* class this repo has been bitten by most.
|
|
415
437
|
*/
|
|
416
438
|
readonly getStore?: () => unknown;
|
|
417
|
-
}) => ((input: AuthStrategyInput) => Promise<
|
|
439
|
+
}) => ((input: AuthStrategyInput) => Promise<SubjectResolution>);
|
|
418
440
|
|
|
419
441
|
/**
|
|
420
442
|
* Compose a list of Effect-native interceptors into ONE function the
|
|
@@ -488,6 +510,28 @@ export declare interface ConnectionInfoValue {
|
|
|
488
510
|
* (every `useMutation` call does). Read by `bindMutation` to dedupe a retried
|
|
489
511
|
* mutation. Absent for callers that don't send it. */
|
|
490
512
|
readonly idempotencyKey?: string;
|
|
513
|
+
/**
|
|
514
|
+
* Unix-SECONDS expiry of the credential that authorized this call, when it
|
|
515
|
+
* has one. Absent for credentials with no expiry (anonymous, a non-expiring
|
|
516
|
+
* strategy) — and absent means "no bound", so the failure direction is the
|
|
517
|
+
* behaviour that already existed.
|
|
518
|
+
*
|
|
519
|
+
* It exists for LONG-LIVED work. A request is checked once and is over in
|
|
520
|
+
* milliseconds, so expiry never mattered; an event subscription is a
|
|
521
|
+
* standing state that reconnects forever by design, so one opened a minute
|
|
522
|
+
* before the token dies would otherwise keep delivering for days on a
|
|
523
|
+
* credential that is long gone. `bindEvent` ends the stream here, and
|
|
524
|
+
* `useEvent`'s existing reconnect immediately re-opens it — which is a NEW
|
|
525
|
+
* request, so it re-resolves the subject and re-runs the guards for real.
|
|
526
|
+
* That is what makes the bound seamless rather than a disconnection the app
|
|
527
|
+
* has to handle: still entitled, it continues; no longer entitled, it fails
|
|
528
|
+
* loudly instead of quietly continuing.
|
|
529
|
+
*
|
|
530
|
+
* This bounds EXPIRY, not revocation. A role revoked mid-session is not
|
|
531
|
+
* observed until the credential runs out — do not let this field grow a
|
|
532
|
+
* doc comment that claims otherwise.
|
|
533
|
+
*/
|
|
534
|
+
readonly credentialExpiresAt?: number;
|
|
491
535
|
}
|
|
492
536
|
|
|
493
537
|
/** The two credential shapes a connection can hold. `oauth2` = an
|
|
@@ -694,6 +738,9 @@ export declare const defineAction: <const Name extends string, Input extends Sch
|
|
|
694
738
|
* need to check who is asking.
|
|
695
739
|
*/
|
|
696
740
|
readonly internal?: boolean;
|
|
741
|
+
/** Replace a PLUGIN route answering to this same tag. Explicit, never
|
|
742
|
+
* inferred — see `overridesPlugin` on the descriptor. */
|
|
743
|
+
readonly overridesPlugin?: boolean;
|
|
697
744
|
}) => ActionProcedureDescriptor<Name, Input, Output, Error>;
|
|
698
745
|
|
|
699
746
|
/**
|
|
@@ -825,6 +872,9 @@ export declare const defineMutation: <const Name extends string, Input extends S
|
|
|
825
872
|
* need to check who is asking.
|
|
826
873
|
*/
|
|
827
874
|
readonly internal?: boolean;
|
|
875
|
+
/** Replace a PLUGIN route answering to this same tag. Explicit, never
|
|
876
|
+
* inferred — see `overridesPlugin` on the descriptor. */
|
|
877
|
+
readonly overridesPlugin?: boolean;
|
|
828
878
|
}) => MutationProcedureDescriptor<Name, Input, Output, Error>;
|
|
829
879
|
|
|
830
880
|
/**
|
|
@@ -919,6 +969,9 @@ export declare const defineQuery: <const Name extends string, Input extends Sche
|
|
|
919
969
|
* need to check who is asking.
|
|
920
970
|
*/
|
|
921
971
|
readonly internal?: boolean;
|
|
972
|
+
/** Replace a PLUGIN route answering to this same tag. Explicit, never
|
|
973
|
+
* inferred — see `overridesPlugin` on the descriptor. */
|
|
974
|
+
readonly overridesPlugin?: boolean;
|
|
922
975
|
}) => QueryProcedureDescriptor<Name, Input, Output, Error>;
|
|
923
976
|
|
|
924
977
|
/**
|
|
@@ -935,6 +988,22 @@ export declare const defineStream: <const Name extends string, Input extends Sch
|
|
|
935
988
|
* dev or serve. Same contract as `internal` on the other definers; a stream
|
|
936
989
|
* without it would be a hole in the same boundary. */
|
|
937
990
|
readonly internal?: boolean;
|
|
991
|
+
/** Replace a PLUGIN route answering to this same tag. Explicit, never
|
|
992
|
+
* inferred — see `overridesPlugin` on the descriptor. */
|
|
993
|
+
readonly overridesPlugin?: boolean;
|
|
994
|
+
/**
|
|
995
|
+
* WHO MAY LISTEN.
|
|
996
|
+
*
|
|
997
|
+
* A stream is the same long-lived grant a subscription is, and it was the one
|
|
998
|
+
* primitive that could not express authorization at all — queries, mutations
|
|
999
|
+
* and actions carry `guards:`, streams did not, so any protection lived
|
|
1000
|
+
* hand-written inside an executor where nothing could verify it existed.
|
|
1001
|
+
*
|
|
1002
|
+
* Checked at subscribe and re-checked before every element, so a resource
|
|
1003
|
+
* un-shared or a membership ended stops the stream rather than continuing to
|
|
1004
|
+
* push. Same shape and same semantics as a query's.
|
|
1005
|
+
*/
|
|
1006
|
+
readonly guards?: Guards;
|
|
938
1007
|
}) => StreamProcedureDescriptor<Name, Input, Element, Error>;
|
|
939
1008
|
|
|
940
1009
|
export declare interface DeleteTarget<Input = unknown> extends NestedTargetFields<Input> {
|
|
@@ -1269,11 +1338,6 @@ export declare interface EventWebhookSpec {
|
|
|
1269
1338
|
readonly description?: string;
|
|
1270
1339
|
/** Payload schema version. Bump when subscribers must adapt. Default 1. */
|
|
1271
1340
|
readonly version?: number;
|
|
1272
|
-
/** Default retry policy for new subscriptions (`{ attempts, backoffMs }`). */
|
|
1273
|
-
readonly retry?: {
|
|
1274
|
-
readonly attempts?: number;
|
|
1275
|
-
readonly backoffMs?: number;
|
|
1276
|
-
};
|
|
1277
1341
|
/** Shared ceiling across ALL deliveries of this event — the runaway-emit
|
|
1278
1342
|
* guard. Over-limit deliveries are deferred, never dropped. */
|
|
1279
1343
|
readonly rateLimit?: {
|
|
@@ -1597,6 +1661,28 @@ export declare interface MutationProcedureDescriptor<Name extends string, Input
|
|
|
1597
1661
|
readonly exposeAsTool: ExposeAsTool | undefined;
|
|
1598
1662
|
/** True when the procedure is kept OFF the wire — no client-group entry and no
|
|
1599
1663
|
* route in dev or serve. See `internal` on the definer's options. */
|
|
1664
|
+
/**
|
|
1665
|
+
* Replace a PLUGIN route that answers to this same tag.
|
|
1666
|
+
*
|
|
1667
|
+
* Without it, a user route and a plugin route sharing a tag is a hard error,
|
|
1668
|
+
* and correctly so — two handlers behind one name is not a thing a caller can
|
|
1669
|
+
* reason about. But refusing is the wrong answer when the app deliberately
|
|
1670
|
+
* wants its own version: the two escapes available otherwise are to rename
|
|
1671
|
+
* your procedure (so the split runs along "who built it" rather than along a
|
|
1672
|
+
* domain boundary) or to `alias` the whole plugin away (same, one level up).
|
|
1673
|
+
* For a frontend developer that is the worst possible partition.
|
|
1674
|
+
*
|
|
1675
|
+
* A reporter wanted exactly this: adopt `@voltro/plugin-notifications`, whose
|
|
1676
|
+
* surface is richer than theirs, add `archive`/`unarchive` beside it — which
|
|
1677
|
+
* already composes, since the collision check compares FULL tags and not
|
|
1678
|
+
* prefixes — and replace `markRead`, because theirs maintains archive state.
|
|
1679
|
+
*
|
|
1680
|
+
* Explicit, never inferred. Silently letting the app win would mean a plugin
|
|
1681
|
+
* upgrade that adds a route could shadow an app procedure with no diff to
|
|
1682
|
+
* read; declaring it makes the intent reviewable and puts the override in the
|
|
1683
|
+
* file that performs it.
|
|
1684
|
+
*/
|
|
1685
|
+
readonly overridesPlugin: boolean | undefined;
|
|
1600
1686
|
readonly internal: boolean | undefined;
|
|
1601
1687
|
}
|
|
1602
1688
|
|
|
@@ -2533,6 +2619,28 @@ export declare interface QueryProcedureDescriptor<Name extends string, Input ext
|
|
|
2533
2619
|
readonly exposeAsTool: ExposeAsTool | undefined;
|
|
2534
2620
|
/** True when the procedure is kept OFF the wire — no client-group entry and no
|
|
2535
2621
|
* route in dev or serve. See `internal` on the definer's options. */
|
|
2622
|
+
/**
|
|
2623
|
+
* Replace a PLUGIN route that answers to this same tag.
|
|
2624
|
+
*
|
|
2625
|
+
* Without it, a user route and a plugin route sharing a tag is a hard error,
|
|
2626
|
+
* and correctly so — two handlers behind one name is not a thing a caller can
|
|
2627
|
+
* reason about. But refusing is the wrong answer when the app deliberately
|
|
2628
|
+
* wants its own version: the two escapes available otherwise are to rename
|
|
2629
|
+
* your procedure (so the split runs along "who built it" rather than along a
|
|
2630
|
+
* domain boundary) or to `alias` the whole plugin away (same, one level up).
|
|
2631
|
+
* For a frontend developer that is the worst possible partition.
|
|
2632
|
+
*
|
|
2633
|
+
* A reporter wanted exactly this: adopt `@voltro/plugin-notifications`, whose
|
|
2634
|
+
* surface is richer than theirs, add `archive`/`unarchive` beside it — which
|
|
2635
|
+
* already composes, since the collision check compares FULL tags and not
|
|
2636
|
+
* prefixes — and replace `markRead`, because theirs maintains archive state.
|
|
2637
|
+
*
|
|
2638
|
+
* Explicit, never inferred. Silently letting the app win would mean a plugin
|
|
2639
|
+
* upgrade that adds a route could shadow an app procedure with no diff to
|
|
2640
|
+
* read; declaring it makes the intent reviewable and puts the override in the
|
|
2641
|
+
* file that performs it.
|
|
2642
|
+
*/
|
|
2643
|
+
readonly overridesPlugin: boolean | undefined;
|
|
2536
2644
|
readonly internal: boolean | undefined;
|
|
2537
2645
|
}
|
|
2538
2646
|
|
|
@@ -2858,15 +2966,28 @@ export declare const setPolicyGuardResolver: (resolver: PolicyGuardResolver | un
|
|
|
2858
2966
|
*/
|
|
2859
2967
|
export declare const setResourceScopeResolver: (resolver: ResourceScopeResolver | undefined) => void;
|
|
2860
2968
|
|
|
2861
|
-
/** A strategy's verdict on a request.
|
|
2862
|
-
* - `matched`: this strategy claims the request; here's the Subject.
|
|
2863
|
-
* - `skip`: not my request (e.g. cookie absent); try next strategy.
|
|
2864
|
-
* - `failed`: this IS my request BUT validation failed (signature
|
|
2865
|
-
* mismatch, expired JWT). Composer bails to anonymous + logs;
|
|
2866
|
-
* silently falling through would mask attacks. */
|
|
2867
2969
|
export declare type StrategyResolution = {
|
|
2868
2970
|
readonly kind: 'matched';
|
|
2869
2971
|
readonly subject: Subject;
|
|
2972
|
+
/**
|
|
2973
|
+
* When this credential expires, in unix SECONDS — if the strategy knows.
|
|
2974
|
+
*
|
|
2975
|
+
* The strategy is the ONLY place in the system that has verified the token
|
|
2976
|
+
* and holds its `exp`, and until now it could not say so. The credential
|
|
2977
|
+
* bound therefore read one source: the `voltro:session` cookie. For an app
|
|
2978
|
+
* authenticating with Bearer JWTs — six of our own catalog strategies do,
|
|
2979
|
+
* and every one of them verifies an `exp` — it was silently `undefined`,
|
|
2980
|
+
* so "a subscription can no longer outlive the credential that authorized
|
|
2981
|
+
* it" was a no-op that read as a guarantee.
|
|
2982
|
+
*
|
|
2983
|
+
* A reporter found it by expecting black screens an hour after a deploy
|
|
2984
|
+
* and getting none. Their conclusion is the one to keep: the guarantee was
|
|
2985
|
+
* not false, it was scoped to an auth shape the sentence did not name.
|
|
2986
|
+
*
|
|
2987
|
+
* Optional, and absent still means no bound — the failure direction is the
|
|
2988
|
+
* behaviour that already existed.
|
|
2989
|
+
*/
|
|
2990
|
+
readonly credentialExpiresAt?: number;
|
|
2870
2991
|
} | {
|
|
2871
2992
|
readonly kind: 'skip';
|
|
2872
2993
|
} | {
|
|
@@ -2890,6 +3011,10 @@ export declare interface StreamProcedureDescriptor<Name extends string, Input ex
|
|
|
2890
3011
|
/** True when the stream is kept OFF the wire — no client-group entry and no
|
|
2891
3012
|
* route in dev or serve. See `internal` on the definer's options. */
|
|
2892
3013
|
readonly internal: boolean | undefined;
|
|
3014
|
+
/** WHO MAY LISTEN. Checked at subscribe AND re-checked before every element,
|
|
3015
|
+
* the same as a query's — a stream is a long-lived grant and the scopes that
|
|
3016
|
+
* justified it can be withdrawn while it is still open. */
|
|
3017
|
+
readonly guards: Guards | undefined;
|
|
2893
3018
|
}
|
|
2894
3019
|
|
|
2895
3020
|
export declare const streamToRpc: <Name extends string, Input extends Schema.Schema.Any, Element extends Schema.Schema.Any, Err extends Schema.Schema.All>(descriptor: StreamProcedureDescriptor<Name, Input, Element, Err>, extraErrors?: ExtraErrors) => Rpc.Rpc<Name, Input extends Schema.Struct.Fields ? Schema.Struct<Input> : Input, Stream<Element, Schema.Schema.All>, typeof Schema.Never, never>;
|
|
@@ -2926,6 +3051,20 @@ export declare const Subject: Schema.Union<[Schema.Struct<{
|
|
|
2926
3051
|
|
|
2927
3052
|
export declare type Subject = typeof Subject.Type;
|
|
2928
3053
|
|
|
3054
|
+
/** A strategy's verdict on a request.
|
|
3055
|
+
* - `matched`: this strategy claims the request; here's the Subject.
|
|
3056
|
+
* - `skip`: not my request (e.g. cookie absent); try next strategy.
|
|
3057
|
+
* - `failed`: this IS my request BUT validation failed (signature
|
|
3058
|
+
* mismatch, expired JWT). Composer bails to anonymous + logs;
|
|
3059
|
+
* silently falling through would mask attacks. */
|
|
3060
|
+
/** What the composed chain returns: the subject, plus what it learned about the
|
|
3061
|
+
* credential's lifetime on the way. */
|
|
3062
|
+
export declare interface SubjectResolution {
|
|
3063
|
+
readonly subject: Subject;
|
|
3064
|
+
/** Unix SECONDS, when the matching strategy could tell. */
|
|
3065
|
+
readonly credentialExpiresAt?: number;
|
|
3066
|
+
}
|
|
3067
|
+
|
|
2929
3068
|
/** The subject's scopes (empty for anonymous / unscoped). */
|
|
2930
3069
|
export declare const subjectScopes: (subject: Subject) => ReadonlyArray<string>;
|
|
2931
3070
|
|
|
@@ -2934,36 +3073,36 @@ export declare class SubjectService extends SubjectService_base {
|
|
|
2934
3073
|
|
|
2935
3074
|
declare const SubjectService_base: Context.TagClass<SubjectService, "@voltro/Subject", {
|
|
2936
3075
|
readonly id: string;
|
|
2937
|
-
readonly tenantId: string;
|
|
2938
3076
|
readonly type: "user";
|
|
3077
|
+
readonly tenantId: string;
|
|
2939
3078
|
readonly scopes?: readonly string[] | undefined;
|
|
2940
3079
|
readonly metadata?: {
|
|
2941
3080
|
readonly [x: string]: unknown;
|
|
2942
3081
|
} | undefined;
|
|
2943
3082
|
} | {
|
|
2944
3083
|
readonly id: string;
|
|
2945
|
-
readonly tenantId: string;
|
|
2946
3084
|
readonly type: "apiKey";
|
|
3085
|
+
readonly tenantId: string;
|
|
2947
3086
|
readonly scopes?: readonly string[] | undefined;
|
|
2948
3087
|
readonly metadata?: {
|
|
2949
3088
|
readonly [x: string]: unknown;
|
|
2950
3089
|
} | undefined;
|
|
2951
3090
|
} | {
|
|
2952
3091
|
readonly id: string;
|
|
2953
|
-
readonly tenantId: string;
|
|
2954
3092
|
readonly type: "serviceAccount";
|
|
3093
|
+
readonly tenantId: string;
|
|
2955
3094
|
readonly scopes?: readonly string[] | undefined;
|
|
2956
3095
|
readonly metadata?: {
|
|
2957
3096
|
readonly [x: string]: unknown;
|
|
2958
3097
|
} | undefined;
|
|
2959
3098
|
} | {
|
|
2960
3099
|
readonly id: null;
|
|
2961
|
-
readonly tenantId: string | null;
|
|
2962
3100
|
readonly type: "anonymous";
|
|
3101
|
+
readonly tenantId: string | null;
|
|
2963
3102
|
} | {
|
|
2964
3103
|
readonly id: string;
|
|
2965
|
-
readonly tenantId: null;
|
|
2966
3104
|
readonly type: "system";
|
|
3105
|
+
readonly tenantId: null;
|
|
2967
3106
|
readonly scopes?: readonly string[] | undefined;
|
|
2968
3107
|
readonly metadata?: {
|
|
2969
3108
|
readonly [x: string]: unknown;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { C as e, D as t, E as n, O as r, S as i, T as a, _ as o, a as ee, b as te, c as ne, d as re, f as ie, g as ae, h as oe, i as se, l as ce, m as le, n as ue, o as de, p as s, r as fe, s as pe, t as me, u as he, v as ge, w as _e, x as ve, y as ye } from "./serverErrorBus-C3JTqgIc.js";
|
|
2
|
-
import { a as be, c as xe, d as Se, f as Ce, i as we, l as Te, n as Ee, o as De, p as Oe, r as ke, s as Ae, t as je, u as Me } from "./auth-
|
|
2
|
+
import { a as be, c as xe, d as Se, f as Ce, i as we, l as Te, n as Ee, o as De, p as Oe, r as ke, s as Ae, t as je, u as Me } from "./auth-CXMrvPyX.js";
|
|
3
3
|
import { Context as Ne, Layer as Pe, Schema as c } from "effect";
|
|
4
4
|
import { Rpc as l } from "@effect/rpc";
|
|
5
5
|
//#region src/rowPatch.ts
|
|
@@ -77,10 +77,19 @@ var Fe = c.Union(c.String, c.Number), u = c.Record({
|
|
|
77
77
|
error: c.Unknown,
|
|
78
78
|
revision: c.optional(c.Number)
|
|
79
79
|
})), Ve = (e) => e.internal !== !0, g = (e) => {
|
|
80
|
+
if (Array.isArray(e.guards) && e.guards.length === 0) throw Error(`${e.name}: \`guards: []\` is empty, so it enforces nothing — but it reads\n at the call site as if this procedure were protected. Omit the field for an
|
|
81
|
+
unguarded procedure, or list the scopes required to call it.`);
|
|
82
|
+
let t = typeof e.source == "string" ? [e.source] : Array.isArray(e.source) ? e.source : void 0;
|
|
83
|
+
if (t !== void 0 && (t.length === 0 || t.some((e) => String(e).trim() === ""))) throw Error(`${e.name}: \`source\` is empty, so this query declares reactivity and\n subscribes to nothing — it serves one snapshot and never updates again,
|
|
84
|
+
which is indistinguishable from "nothing changed". Name the table(s) it
|
|
85
|
+
reads, or omit \`source\` for a non-reactive query.`);
|
|
80
86
|
if (e.internal !== !0) return e;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
if (e.overridesPlugin === !0) throw Error(`${e.name}: \`internal: true\` cannot be combined with \`overridesPlugin\`.\n The override REMOVES the plugin route, and an internal procedure is not
|
|
88
|
+
wire-reachable — so the plugin's route would vanish with nothing callable
|
|
89
|
+
in its place, and callers would get a 404 for something that used to work.`);
|
|
90
|
+
let n = [e.publicApi === void 0 ? void 0 : "publicApi", e.exposeAsTool === void 0 ? void 0 : "exposeAsTool"].filter((e) => e !== void 0);
|
|
91
|
+
if (n.length === 0) return e;
|
|
92
|
+
throw Error(`${e.name}: \`internal: true\` cannot be combined with ${n.map((e) => `\`${e}\``).join(" or ")}. \`internal\` takes the procedure OFF the wire; those put it back ON a different one (${n.includes("publicApi") ? "a REST route" : "an agent tool"}), and that surface is projected without consulting the flag — so the procedure would be unreachable from your client and reachable from the internet. Drop \`internal: true\` if the wider surface is intended, or remove the ${n.join(" / ")} annotation if it is not.`);
|
|
84
93
|
}, He = (e) => e.action !== void 0 && e.resourceType !== void 0, _ = (e) => g({
|
|
85
94
|
kind: "query",
|
|
86
95
|
name: e.name,
|
|
@@ -92,7 +101,8 @@ var Fe = c.Union(c.String, c.Number), u = c.Record({
|
|
|
92
101
|
guards: e.guards,
|
|
93
102
|
publicApi: e.publicApi,
|
|
94
103
|
exposeAsTool: e.exposeAsTool,
|
|
95
|
-
internal: e.internal
|
|
104
|
+
internal: e.internal,
|
|
105
|
+
overridesPlugin: e.overridesPlugin
|
|
96
106
|
}), v = (e) => g({
|
|
97
107
|
kind: "mutation",
|
|
98
108
|
name: e.name,
|
|
@@ -103,7 +113,8 @@ var Fe = c.Union(c.String, c.Number), u = c.Record({
|
|
|
103
113
|
guards: e.guards,
|
|
104
114
|
publicApi: e.publicApi,
|
|
105
115
|
exposeAsTool: e.exposeAsTool,
|
|
106
|
-
internal: e.internal
|
|
116
|
+
internal: e.internal,
|
|
117
|
+
overridesPlugin: e.overridesPlugin
|
|
107
118
|
}), y = (e) => g({
|
|
108
119
|
kind: "action",
|
|
109
120
|
name: e.name,
|
|
@@ -115,14 +126,17 @@ var Fe = c.Union(c.String, c.Number), u = c.Record({
|
|
|
115
126
|
target: e.target,
|
|
116
127
|
publicApi: e.publicApi,
|
|
117
128
|
exposeAsTool: e.exposeAsTool,
|
|
118
|
-
internal: e.internal
|
|
129
|
+
internal: e.internal,
|
|
130
|
+
overridesPlugin: e.overridesPlugin
|
|
119
131
|
}), Ue = (e) => g({
|
|
120
132
|
kind: "stream",
|
|
121
133
|
name: e.name,
|
|
122
134
|
input: e.input,
|
|
123
135
|
element: e.element,
|
|
124
136
|
error: e.error ?? c.Never,
|
|
125
|
-
internal: e.internal
|
|
137
|
+
internal: e.internal,
|
|
138
|
+
overridesPlugin: e.overridesPlugin,
|
|
139
|
+
guards: e.guards
|
|
126
140
|
}), b = (e, t) => t && t.length > 0 ? c.Union(e, ...t) : e, x = (e, t) => t && t.length > 0 ? c.Union(e, s) : e, S = (e, t) => l.make(e.name, {
|
|
127
141
|
payload: e.input,
|
|
128
142
|
success: h(e.output),
|
|
@@ -195,6 +209,15 @@ var Fe = c.Union(c.String, c.Number), u = c.Record({
|
|
|
195
209
|
};
|
|
196
210
|
}, Xe = 7500, Ze = (e) => typeof e == "object" && !!e && e.kind === "event", Qe = (e) => {
|
|
197
211
|
if (e.name.length === 0) throw Error("defineEvent: `name` must not be empty");
|
|
212
|
+
if (/\s/.test(e.name)) throw Error(`defineEvent("${e.name}"): \`name\` must not contain whitespace.\n The name is used as a broker subject segment. NATS refuses a subject with
|
|
213
|
+
whitespace and delivers nothing — silently, and only on that broker, so an
|
|
214
|
+
app that works on Redis stops working when the transport changes.
|
|
215
|
+
Use a dot for namespacing: "orders.paid".`);
|
|
216
|
+
if (e.guards !== void 0 && e.guards.length === 0) throw Error(`defineEvent("${e.name}"): \`guards: []\` is empty, so it enforces nothing —\n but it reads at the call site as if the event were protected. Omit the field
|
|
217
|
+
for an unguarded event, or list the scopes that may subscribe.`);
|
|
218
|
+
if (e.webhook?.rateLimit !== void 0 && e.webhook.rateLimit.perMinute < 1) throw Error(`defineEvent("${e.name}"): \`webhook.rateLimit.perMinute\` is ${e.webhook.rateLimit.perMinute}, which defers every delivery forever.\n There is no "unlimited" spelling here — omit \`rateLimit\` for no ceiling.`);
|
|
219
|
+
if (e.webhook?.version !== void 0 && e.webhook.version < 1) throw Error(`defineEvent("${e.name}"): \`webhook.version\` must be >= 1 (default 1).\n A subscriber pinned to 1 would read this event as BEHIND its own version,
|
|
220
|
+
which is the opposite of what a version bump is for.`);
|
|
198
221
|
if (e.delivery === "latest" && e.webhook !== void 0) throw Error(`defineEvent(${e.name}): \`delivery: 'latest'\` cannot be combined with \`webhook\`.\n \`latest\` means a superseded delivery did not matter — but a webhook delivery is a
|
|
199
222
|
durable side effect at a third party, and one already sent cannot be superseded.
|
|
200
223
|
A high-rate event with an HTTP audience is also 60 deliveries per second per target;
|
package/dist/jwt.d.ts
CHANGED
|
@@ -231,15 +231,28 @@ export declare const _resetJwksCache: () => void;
|
|
|
231
231
|
/** Test-only: clear the OIDC discovery-document cache between runs. */
|
|
232
232
|
export declare const _resetOidcDiscoveryCache: () => void;
|
|
233
233
|
|
|
234
|
-
/** A strategy's verdict on a request.
|
|
235
|
-
* - `matched`: this strategy claims the request; here's the Subject.
|
|
236
|
-
* - `skip`: not my request (e.g. cookie absent); try next strategy.
|
|
237
|
-
* - `failed`: this IS my request BUT validation failed (signature
|
|
238
|
-
* mismatch, expired JWT). Composer bails to anonymous + logs;
|
|
239
|
-
* silently falling through would mask attacks. */
|
|
240
234
|
declare type StrategyResolution = {
|
|
241
235
|
readonly kind: 'matched';
|
|
242
236
|
readonly subject: Subject;
|
|
237
|
+
/**
|
|
238
|
+
* When this credential expires, in unix SECONDS — if the strategy knows.
|
|
239
|
+
*
|
|
240
|
+
* The strategy is the ONLY place in the system that has verified the token
|
|
241
|
+
* and holds its `exp`, and until now it could not say so. The credential
|
|
242
|
+
* bound therefore read one source: the `voltro:session` cookie. For an app
|
|
243
|
+
* authenticating with Bearer JWTs — six of our own catalog strategies do,
|
|
244
|
+
* and every one of them verifies an `exp` — it was silently `undefined`,
|
|
245
|
+
* so "a subscription can no longer outlive the credential that authorized
|
|
246
|
+
* it" was a no-op that read as a guarantee.
|
|
247
|
+
*
|
|
248
|
+
* A reporter found it by expecting black screens an hour after a deploy
|
|
249
|
+
* and getting none. Their conclusion is the one to keep: the guarantee was
|
|
250
|
+
* not false, it was scoped to an auth shape the sentence did not name.
|
|
251
|
+
*
|
|
252
|
+
* Optional, and absent still means no bound — the failure direction is the
|
|
253
|
+
* behaviour that already existed.
|
|
254
|
+
*/
|
|
255
|
+
readonly credentialExpiresAt?: number;
|
|
243
256
|
} | {
|
|
244
257
|
readonly kind: 'skip';
|
|
245
258
|
} | {
|
package/dist/jwt.js
CHANGED
|
@@ -110,22 +110,28 @@ var i = 3600 * 1e3, a = /* @__PURE__ */ new Map(), o = (e, n) => {
|
|
|
110
110
|
reason: `${e.id} token has no tenant claim and no defaultTenantId configured`
|
|
111
111
|
};
|
|
112
112
|
let i = e.subjectIdFromClaims ? e.subjectIdFromClaims(t) : String(t.sub ?? "");
|
|
113
|
-
|
|
114
|
-
kind: "matched",
|
|
115
|
-
subject: {
|
|
116
|
-
type: "user",
|
|
117
|
-
id: i,
|
|
118
|
-
tenantId: n,
|
|
119
|
-
...e.scopesFromClaims === void 0 ? {} : { scopes: e.scopesFromClaims(t) },
|
|
120
|
-
metadata: {
|
|
121
|
-
provider: e.id,
|
|
122
|
-
claims: t
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
} : {
|
|
113
|
+
if (!i) return {
|
|
126
114
|
kind: "failed",
|
|
127
115
|
reason: `${e.id} token has no sub claim`
|
|
128
116
|
};
|
|
117
|
+
let a = {
|
|
118
|
+
type: "user",
|
|
119
|
+
id: i,
|
|
120
|
+
tenantId: n,
|
|
121
|
+
...e.scopesFromClaims === void 0 ? {} : { scopes: e.scopesFromClaims(t) },
|
|
122
|
+
metadata: {
|
|
123
|
+
provider: e.id,
|
|
124
|
+
claims: t
|
|
125
|
+
}
|
|
126
|
+
}, o = typeof t.exp == "number" && Number.isFinite(t.exp) ? t.exp : void 0;
|
|
127
|
+
return o === void 0 ? {
|
|
128
|
+
kind: "matched",
|
|
129
|
+
subject: a
|
|
130
|
+
} : {
|
|
131
|
+
kind: "matched",
|
|
132
|
+
subject: a,
|
|
133
|
+
credentialExpiresAt: o
|
|
134
|
+
};
|
|
129
135
|
} catch (t) {
|
|
130
136
|
return t instanceof c ? {
|
|
131
137
|
kind: "failed",
|
package/dist/rest.d.ts
CHANGED
|
@@ -31,6 +31,28 @@ declare interface ActionProcedureDescriptor<Name extends string, Input extends S
|
|
|
31
31
|
readonly exposeAsTool: ExposeAsTool | undefined;
|
|
32
32
|
/** True when the procedure is kept OFF the wire — no client-group entry and no
|
|
33
33
|
* route in dev or serve. See `internal` on the definer's options. */
|
|
34
|
+
/**
|
|
35
|
+
* Replace a PLUGIN route that answers to this same tag.
|
|
36
|
+
*
|
|
37
|
+
* Without it, a user route and a plugin route sharing a tag is a hard error,
|
|
38
|
+
* and correctly so — two handlers behind one name is not a thing a caller can
|
|
39
|
+
* reason about. But refusing is the wrong answer when the app deliberately
|
|
40
|
+
* wants its own version: the two escapes available otherwise are to rename
|
|
41
|
+
* your procedure (so the split runs along "who built it" rather than along a
|
|
42
|
+
* domain boundary) or to `alias` the whole plugin away (same, one level up).
|
|
43
|
+
* For a frontend developer that is the worst possible partition.
|
|
44
|
+
*
|
|
45
|
+
* A reporter wanted exactly this: adopt `@voltro/plugin-notifications`, whose
|
|
46
|
+
* surface is richer than theirs, add `archive`/`unarchive` beside it — which
|
|
47
|
+
* already composes, since the collision check compares FULL tags and not
|
|
48
|
+
* prefixes — and replace `markRead`, because theirs maintains archive state.
|
|
49
|
+
*
|
|
50
|
+
* Explicit, never inferred. Silently letting the app win would mean a plugin
|
|
51
|
+
* upgrade that adds a route could shadow an app procedure with no diff to
|
|
52
|
+
* read; declaring it makes the intent reviewable and puts the override in the
|
|
53
|
+
* file that performs it.
|
|
54
|
+
*/
|
|
55
|
+
readonly overridesPlugin: boolean | undefined;
|
|
34
56
|
readonly internal: boolean | undefined;
|
|
35
57
|
}
|
|
36
58
|
|
|
@@ -209,6 +231,28 @@ declare interface MutationProcedureDescriptor<Name extends string, Input extends
|
|
|
209
231
|
readonly exposeAsTool: ExposeAsTool | undefined;
|
|
210
232
|
/** True when the procedure is kept OFF the wire — no client-group entry and no
|
|
211
233
|
* route in dev or serve. See `internal` on the definer's options. */
|
|
234
|
+
/**
|
|
235
|
+
* Replace a PLUGIN route that answers to this same tag.
|
|
236
|
+
*
|
|
237
|
+
* Without it, a user route and a plugin route sharing a tag is a hard error,
|
|
238
|
+
* and correctly so — two handlers behind one name is not a thing a caller can
|
|
239
|
+
* reason about. But refusing is the wrong answer when the app deliberately
|
|
240
|
+
* wants its own version: the two escapes available otherwise are to rename
|
|
241
|
+
* your procedure (so the split runs along "who built it" rather than along a
|
|
242
|
+
* domain boundary) or to `alias` the whole plugin away (same, one level up).
|
|
243
|
+
* For a frontend developer that is the worst possible partition.
|
|
244
|
+
*
|
|
245
|
+
* A reporter wanted exactly this: adopt `@voltro/plugin-notifications`, whose
|
|
246
|
+
* surface is richer than theirs, add `archive`/`unarchive` beside it — which
|
|
247
|
+
* already composes, since the collision check compares FULL tags and not
|
|
248
|
+
* prefixes — and replace `markRead`, because theirs maintains archive state.
|
|
249
|
+
*
|
|
250
|
+
* Explicit, never inferred. Silently letting the app win would mean a plugin
|
|
251
|
+
* upgrade that adds a route could shadow an app procedure with no diff to
|
|
252
|
+
* read; declaring it makes the intent reviewable and puts the override in the
|
|
253
|
+
* file that performs it.
|
|
254
|
+
*/
|
|
255
|
+
readonly overridesPlugin: boolean | undefined;
|
|
212
256
|
readonly internal: boolean | undefined;
|
|
213
257
|
}
|
|
214
258
|
|
|
@@ -517,6 +561,28 @@ declare interface QueryProcedureDescriptor<Name extends string, Input extends Sc
|
|
|
517
561
|
readonly exposeAsTool: ExposeAsTool | undefined;
|
|
518
562
|
/** True when the procedure is kept OFF the wire — no client-group entry and no
|
|
519
563
|
* route in dev or serve. See `internal` on the definer's options. */
|
|
564
|
+
/**
|
|
565
|
+
* Replace a PLUGIN route that answers to this same tag.
|
|
566
|
+
*
|
|
567
|
+
* Without it, a user route and a plugin route sharing a tag is a hard error,
|
|
568
|
+
* and correctly so — two handlers behind one name is not a thing a caller can
|
|
569
|
+
* reason about. But refusing is the wrong answer when the app deliberately
|
|
570
|
+
* wants its own version: the two escapes available otherwise are to rename
|
|
571
|
+
* your procedure (so the split runs along "who built it" rather than along a
|
|
572
|
+
* domain boundary) or to `alias` the whole plugin away (same, one level up).
|
|
573
|
+
* For a frontend developer that is the worst possible partition.
|
|
574
|
+
*
|
|
575
|
+
* A reporter wanted exactly this: adopt `@voltro/plugin-notifications`, whose
|
|
576
|
+
* surface is richer than theirs, add `archive`/`unarchive` beside it — which
|
|
577
|
+
* already composes, since the collision check compares FULL tags and not
|
|
578
|
+
* prefixes — and replace `markRead`, because theirs maintains archive state.
|
|
579
|
+
*
|
|
580
|
+
* Explicit, never inferred. Silently letting the app win would mean a plugin
|
|
581
|
+
* upgrade that adds a route could shadow an app procedure with no diff to
|
|
582
|
+
* read; declaring it makes the intent reviewable and puts the override in the
|
|
583
|
+
* file that performs it.
|
|
584
|
+
*/
|
|
585
|
+
readonly overridesPlugin: boolean | undefined;
|
|
520
586
|
readonly internal: boolean | undefined;
|
|
521
587
|
}
|
|
522
588
|
|
package/dist/rest.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as e, i as t, o as n, r, t as i } from "./serverErrorBus-C3JTqgIc.js";
|
|
2
|
-
import { s as a } from "./auth-
|
|
2
|
+
import { s as a } from "./auth-CXMrvPyX.js";
|
|
3
3
|
import { Effect as o, Schema as s } from "effect";
|
|
4
4
|
//#region src/publicApi.ts
|
|
5
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, n) => {
|
package/dist/session.d.ts
CHANGED
|
@@ -108,6 +108,33 @@ export declare const resolveSessionSecret: () => string;
|
|
|
108
108
|
*/
|
|
109
109
|
export declare const resolveSessionSecrets: () => SessionSecrets;
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* The cookie the framework's own session strategy writes and reads.
|
|
113
|
+
*
|
|
114
|
+
* Exported and single-sourced because two readers now need it — the auth
|
|
115
|
+
* strategy in `dev.ts` and `sessionExpiryFromHeaders` below. Two copies of one
|
|
116
|
+
* env expression is the "derived twice" shape: both sites look correct and
|
|
117
|
+
* they disagree the moment someone sets the variable.
|
|
118
|
+
*/
|
|
119
|
+
export declare const SESSION_COOKIE_NAME: string;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The verified expiry of the session cookie in `headers`, in unix SECONDS, or
|
|
123
|
+
* `undefined` when there is no session or it does not verify.
|
|
124
|
+
*
|
|
125
|
+
* It VERIFIES rather than decoding. An unverified read would be worse than
|
|
126
|
+
* nothing here: the value bounds how long a subscription may live, so a client
|
|
127
|
+
* that could forge a far-future `exp` would lift exactly the ceiling this
|
|
128
|
+
* exists to impose. The cost is one HMAC check on a call that already carries
|
|
129
|
+
* the cookie.
|
|
130
|
+
*
|
|
131
|
+
* A SHARED builder on purpose. `voltro dev` and `voltro serve` assemble their
|
|
132
|
+
* middleware independently, and a value derived twice is the shape this repo
|
|
133
|
+
* has been bitten by — dev and serve agreeing on a field while disagreeing on
|
|
134
|
+
* what it contains. Both call this.
|
|
135
|
+
*/
|
|
136
|
+
export declare const sessionExpiryFromHeaders: (headers: Record<string, string | undefined>) => number | undefined;
|
|
137
|
+
|
|
111
138
|
export declare type SessionPayload = typeof SessionPayload_2.Type;
|
|
112
139
|
|
|
113
140
|
declare const SessionPayload_2: Schema.Struct<{
|