@voltro/protocol 0.80.0 → 0.81.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/jwt.d.ts CHANGED
@@ -339,6 +339,14 @@ declare const Subject: Schema.Union<[Schema.Struct<{
339
339
  tenantId: typeof Schema.Null;
340
340
  scopes: Schema.optional<Schema.Array$<typeof Schema.String>>;
341
341
  metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
342
+ }>, Schema.Struct<{
343
+ type: Schema.Literal<["service"]>;
344
+ id: typeof Schema.String;
345
+ /** The calling app's name, as the callee lists it in `serviceCallers`. */
346
+ service: typeof Schema.String;
347
+ tenantId: typeof Schema.Null;
348
+ scopes: Schema.optional<Schema.Array$<typeof Schema.String>>;
349
+ metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
342
350
  }>]>;
343
351
 
344
352
  declare type Subject = typeof Subject.Type;
package/dist/rest.d.ts CHANGED
@@ -30,6 +30,9 @@ declare interface ActionProcedureDescriptor<Name extends string, Input extends S
30
30
  * `openAccess: '<why>'`. Mutually exclusive with `guards`; together they are
31
31
  * the only two shapes `security.defaultDeny` accepts. */
32
32
  readonly openAccess: string | undefined;
33
+ /** Only these apps of the project may call this procedure — see `service` on
34
+ * the definer's options. Absent → the app's own clients call it. */
35
+ readonly service: ServiceSpec | undefined;
33
36
  /** Opt this action into a public REST endpoint (innovation/11). */
34
37
  readonly publicApi: PublicApiSpec | undefined;
35
38
  /** Opt this action into the auto-synthesized agent toolset (innovation/07). */
@@ -138,7 +141,7 @@ export declare const collectPublicApiRoutes: (entries: ReadonlyArray<PublicApiEn
138
141
  * because that is where the normalised decision lands, and because every
139
142
  * enforcement path reads the descriptor's array and nothing else.
140
143
  */
141
- declare type DeclaredAccess<Input = unknown> = ReadonlyArray<AnyGuardSpec<Input> | OpenAccessSpec>;
144
+ declare type DeclaredAccess<Input = unknown> = ReadonlyArray<AnyGuardSpec<Input> | OpenAccessSpec | ServiceAccessSpec>;
142
145
 
143
146
  /** The `_tag` literals a descriptor's `error:` union declares (empty for none). */
144
147
  export declare const declaredErrorTags: (error: Schema.Schema.All | undefined) => ReadonlySet<string>;
@@ -178,15 +181,16 @@ export declare const deriveResource: (tag: string, spec: PublicApiSpec, descript
178
181
 
179
182
  /**
180
183
  * Dispatch a request across REST routes that SHARE a path. Every REST route
181
- * mounts as `*` and gates its own method, returning a precise 405 BEFORE any
182
- * side effect when the method isn't its own — so probing the group in order and
183
- * taking the FIRST non-405 result yields the route that owns the request's
184
- * method. This is what lets the standard GET + POST on one resource path
185
- * coexist: the runtime's rpc server (`rpcServer.ts`, started by both boot
186
- * paths) mounts ONE dispatcher per path (the underlying
187
- * router rejects two mounts on the same `(method, path)`). A single-route group
188
- * returns that route's result directly (its own 405 included); when no route in
189
- * the group owns the method, the last 405 stands.
184
+ * gates its own method, returning a precise 405 BEFORE any side effect when the
185
+ * method isn't its own — so probing the group in order and taking the FIRST
186
+ * non-405 result yields the route that owns the request's method. This is what
187
+ * lets the standard GET + POST on one resource path coexist: `rpcServer.ts` in
188
+ * `@voltro/runtime` mounts ONE dispatcher per
189
+ * path (the underlying router rejects two mounts on the same
190
+ * `(method, path)`). A single-route group returns that route's result directly
191
+ * (its own 405 included); when no route in the group owns the method, the last
192
+ * 405 stands — its `Allow` names every method the path serves, because
193
+ * `restRoutesToHttpRoutes` gives each route its path's full set.
190
194
  *
191
195
  * `group` must be non-empty (the serve pipeline only builds a dispatcher for a
192
196
  * path that has at least one route).
@@ -283,14 +287,24 @@ declare interface IdempotencyStore {
283
287
  /** Current record for (scope, key), or null. Read-side (inspect/dashboard). */
284
288
  readonly get: (scope: string, key: string) => Promise<IdempotencyRecord | null>;
285
289
  /**
286
- * Atomically claim (scope, key) IF absent OR stale (older than `ttlMs`):
290
+ * Atomically claim (scope, key) IF absent OR stale — a COMPLETED record older
291
+ * than `ttlMs` (the replay window), or an IN-FLIGHT one whose lease
292
+ * (`inFlightLeaseMs`, renewed by the live handler) lapsed:
287
293
  * write a fresh `in_flight` row and return `'claimed'`. Otherwise return the
288
294
  * EXISTING (still-fresh) record. The atomicity here is the whole game — two
289
295
  * concurrent same-key requests both hit this, exactly one gets `'claimed'`,
290
296
  * the loser reads the winner's record. In SQL this is one
291
297
  * `INSERT … ON CONFLICT DO UPDATE … WHERE existing.createdAt < now - ttl`.
292
298
  */
293
- readonly claim: (scope: string, key: string, now: number, ttlMs: number) => Promise<'claimed' | IdempotencyRecord>;
299
+ readonly claim: (scope: string, key: string, now: number, ttlMs: number, inFlightLeaseMs: number) => Promise<'claimed' | IdempotencyRecord>;
300
+ /**
301
+ * Extend a RUNNING claim's lease: bump its timestamp while it is still
302
+ * `in_flight` (a completed or released claim is left alone). The server calls
303
+ * it while the handler runs; a process that dies stops calling it, and its
304
+ * claim goes stale after `inFlightLeaseMs` instead of after the whole replay
305
+ * window.
306
+ */
307
+ readonly renew: (scope: string, key: string, now: number) => Promise<void>;
294
308
  /** Flip the claim to `completed` and store the response. */
295
309
  readonly complete: (scope: string, key: string, response: IdempotencyResponse, now: number) => Promise<void>;
296
310
  /** Drop the claim (handler errored → a retry may re-process). */
@@ -370,6 +384,9 @@ declare interface MutationProcedureDescriptor<Name extends string, Input extends
370
384
  * `openAccess: '<why>'`. Mutually exclusive with `guards`; together they are
371
385
  * the only two shapes `security.defaultDeny` accepts. */
372
386
  readonly openAccess: string | undefined;
387
+ /** Only these apps of the project may call this procedure — see `service` on
388
+ * the definer's options. Absent → the app's own clients call it. */
389
+ readonly service: ServiceSpec | undefined;
373
390
  /** Opt this mutation into a public REST endpoint (innovation/11). */
374
391
  readonly publicApi: PublicApiSpec | undefined;
375
392
  /** Opt this mutation into the auto-synthesized agent toolset (innovation/07). */
@@ -504,11 +521,17 @@ declare interface PluginErrorSchema {
504
521
 
505
522
  /** A public raw-HTTP route a plugin serves on the framework listener. */
506
523
  declare interface PluginHttpRoute {
507
- /** HTTP method, or `'*'` for any (the handler decides). PATCH/HEAD/OPTIONS
508
- * are first-class — the REST desugar used to mount `'*'` partly BECAUSE
509
- * this union lacked PATCH; that reason is gone (the `'*'` mount remains
510
- * for its other job: one dispatcher per shared path + a precise 405). */
524
+ /** HTTP method, or `'*'` for any (the handler decides). The server mounts the
525
+ * path for exactly the methods its routes declare; `'*'` mounts it for every
526
+ * method the router knows (34), so keep that for a route that really serves
527
+ * them — see `answersOtherMethods` for one that only wants to refuse them. */
511
528
  readonly method: '*' | 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
529
+ /** `true`: the handler gates its own method, so a request with a method the
530
+ * path does not declare is still handed to it — to answer with a precise
531
+ * `405` and `Allow` instead of the router's `404`. The path is still mounted
532
+ * only for `method`. A REST route sets it. Without it, a route that declares
533
+ * one method never sees another. */
534
+ readonly answersOtherMethods?: boolean | undefined;
512
535
  /** Absolute path, e.g. `/_voltro/storage`. Matched byte for byte: the
513
536
  * router folds case and a trailing slash to FIND the route, the dispatcher
514
537
  * then asks whether the request's path is one the route declared — so
@@ -1052,6 +1075,9 @@ declare interface QueryProcedureDescriptor<Name extends string, Input extends Sc
1052
1075
  * `openAccess: '<why>'`. Mutually exclusive with `guards`; together they are
1053
1076
  * the only two shapes `security.defaultDeny` accepts. */
1054
1077
  readonly openAccess: string | undefined;
1078
+ /** Only these apps of the project may call this procedure — see `service` on
1079
+ * the definer's options. Absent → the app's own clients call it. */
1080
+ readonly service: ServiceSpec | undefined;
1055
1081
  /** Opt this query into a public REST endpoint (innovation/11). */
1056
1082
  readonly publicApi: PublicApiSpec | undefined;
1057
1083
  /** Opt this query into the auto-synthesized agent toolset (innovation/07). */
@@ -1490,6 +1516,37 @@ export declare const routeAcceptsPath: (route: {
1490
1516
  readonly match?: "prefix" | "exact" | undefined;
1491
1517
  }, path: string) => boolean;
1492
1518
 
1519
+ /**
1520
+ * The runtime-erased form of a procedure's `service: { callers }` — "only these
1521
+ * apps of the project may call this, and nobody else".
1522
+ *
1523
+ * A guard entry for the reason `OpenAccessSpec` is one: every enforcement path
1524
+ * is handed the `guards` array and nothing else, so a decision that lived on a
1525
+ * separate descriptor field would be enforced by the paths somebody remembered
1526
+ * and skipped by the rest.
1527
+ *
1528
+ * It admits exactly a `service` subject whose name is listed. `admin:full` does
1529
+ * NOT bypass it: a service procedure is a machine contract between two apps,
1530
+ * and a browser session holding the admin scope is not one of the parties.
1531
+ */
1532
+ declare interface ServiceAccessSpec {
1533
+ /** The calling apps' names, as the callee declares them in `serviceCallers`.
1534
+ * Non-empty by construction — the definers refuse an empty list. */
1535
+ readonly service: ReadonlyArray<string>;
1536
+ }
1537
+
1538
+ /**
1539
+ * A service procedure's declaration: which apps of the project may call it.
1540
+ *
1541
+ * service: { callers: ['worker'] }
1542
+ *
1543
+ * The names are the CALLING apps' names as this app lists them in
1544
+ * `serviceCallers` — the same name the caller's credential resolves to.
1545
+ */
1546
+ declare interface ServiceSpec {
1547
+ readonly callers: ReadonlyArray<string>;
1548
+ }
1549
+
1493
1550
  /**
1494
1551
  * Build a streaming (Server-Sent Events) handler result. `subscribe` gets an
1495
1552
  * `emit` for one already-framed SSE chunk and MUST return an unsubscribe, which
@@ -1558,6 +1615,14 @@ declare const Subject: Schema.Union<[Schema.Struct<{
1558
1615
  tenantId: typeof Schema.Null;
1559
1616
  scopes: Schema.optional<Schema.Array$<typeof Schema.String>>;
1560
1617
  metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
1618
+ }>, Schema.Struct<{
1619
+ type: Schema.Literal<["service"]>;
1620
+ id: typeof Schema.String;
1621
+ /** The calling app's name, as the callee lists it in `serviceCallers`. */
1622
+ service: typeof Schema.String;
1623
+ tenantId: typeof Schema.Null;
1624
+ scopes: Schema.optional<Schema.Array$<typeof Schema.String>>;
1625
+ metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
1561
1626
  }>]>;
1562
1627
 
1563
1628
  declare type Subject = typeof Subject.Type;