@procedures/client 0.12.1 → 0.13.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.
@@ -89,10 +89,12 @@ export interface ProcedureServerOptions extends ServerOptions {
89
89
  clock?: Clock | undefined;
90
90
 
91
91
  /**
92
- * The procedures that the callers of this server can reach. `registerFromRegistry()` registers
93
- * only these procedures. A data-driven procedure (control flow, `eval`, a procedure that
94
- * `procedure.define` made) can call only these procedures too, because its caller chooses
95
- * what it calls. A procedure of code calls the procedures of its `metadata.calls`
92
+ * The procedures that the callers of this server can reach. The rule applies to each procedure
93
+ * that the server serves: the procedures of the registry and the procedures that
94
+ * `registerProcedure()` pins, also the collection procedures (CLI-47). Only a procedure that the
95
+ * server pins with `alwaysOn` is outside the rule. A data-driven procedure (control flow, `eval`,
96
+ * a procedure that `procedure.define` made) can call only these procedures too, because its
97
+ * caller chooses what it calls. A procedure of code calls the procedures of its `metadata.calls`
96
98
  * (`docker.run` declares `shell.run`) and the exposed ones. Without this rule, the server
97
99
  * exposes every procedure. See `procedures/policy.ts` (deep dive MCP-3).
98
100
  */
@@ -180,6 +182,18 @@ export interface ProcedureServerOptions extends ServerOptions {
180
182
  debug?: boolean | ((report: ServerDebugReport) => void) | undefined;
181
183
  }
182
184
 
185
+ /** The options of `registerProcedure()`, `registerProcedures()` and `registerModule()` (CLI-47). */
186
+ export interface RegisterProcedureOptions {
187
+ /**
188
+ * True: the server serves the procedure, also when its expose rule does not match the path. A
189
+ * host sets it only for its own procedures: `host.health`, `host.info` and `host.metrics`. The
190
+ * server sets it for `events.subscribe`, because the topic rule is the expose rule of that
191
+ * procedure. False: the expose rule applies, as for a procedure of the registry.
192
+ * @default false
193
+ */
194
+ alwaysOn?: boolean | undefined;
195
+ }
196
+
183
197
  /**
184
198
  * The metadata key of a stream response whose procedure declares `stream: { unbounded: true }`.
185
199
  * Its value is "unbounded".
@@ -372,8 +386,11 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
372
386
  private readonly storageUses = new Map<CollectionStorage<unknown>, { calls: number; waiters: Array<() => void> }>();
373
387
  /** The clock of the wait of `useStorage` */
374
388
  private readonly clock: Clock;
375
- /** The procedures that `registerProcedure` pinned, by key */
376
- private readonly pinnedProcedures = new Map<string, AnyProcedure>();
389
+ /**
390
+ * The procedures that `registerProcedure` pinned, by key. The expose rule applies to each one,
391
+ * unless the pin is `alwaysOn` (CLI-47).
392
+ */
393
+ private readonly pinnedProcedures = new Map<string, { readonly procedure: AnyProcedure; readonly alwaysOn: boolean }>();
377
394
  /**
378
395
  * True after `registerFromRegistry()`: the server serves each procedure of the registry that
379
396
  * has a handler and passes the expose rule, looked up when the request arrives (deep dive
@@ -442,9 +459,10 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
442
459
  : options.debug === true
443
460
  ? (report) => console.debug(`[ProcedureServer] ${report.topic} of ${report.requestId}: ${report.message}`)
444
461
  : undefined;
445
- // The remote events: the server pins events.subscribe, as it pins host.health (EVENTS.md)
462
+ // The remote events: the server pins events.subscribe, as a host pins host.health (EVENTS.md).
463
+ // The topic rule is its expose rule, so the pin is always on (CLI-47).
446
464
  this.publisher = options.events instanceof EventPublisher ? options.events : options.events ? new EventPublisher(options.events) : undefined;
447
- if (this.publisher) this.registerProcedure(this.publisher.procedure);
465
+ if (this.publisher) this.registerProcedure(this.publisher.procedure, { alwaysOn: true });
448
466
 
449
467
  // Configure collections
450
468
  if (options.collections) {
@@ -672,39 +690,46 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
672
690
  // ===========================================================================
673
691
 
674
692
  /**
675
- * Register a procedure as a server handler. The server keeps this procedure object. A
693
+ * Register a procedure as a server handler (a pin). The server keeps this procedure object. A
676
694
  * procedure of the registry at the same path comes first when the server serves the registry.
677
695
  *
696
+ * The expose rule of the server applies to the pin, as to a procedure of the registry (CLI-47).
697
+ * With `alwaysOn`, the server serves the pin also when the rule does not match its path. Only
698
+ * the procedures of a host (`host.*`) and `events.subscribe` set it.
699
+ *
678
700
  * @param procedure - Procedure definition
701
+ * @param options - `alwaysOn`: the expose rule does not apply to this pin
679
702
  */
680
- registerProcedure(procedure: AnyProcedure): void {
703
+ registerProcedure(procedure: AnyProcedure, options: RegisterProcedureOptions = {}): void {
681
704
  // Must have a handler
682
705
  if (!procedure.handler) {
683
706
  throw new Error(`Procedure at ${pathToKey(procedure.path)} has no handler`);
684
707
  }
685
708
  // The path must make a method: this throws for an empty path. One segment or more is valid (CLI-28)
686
709
  pathToMethod(procedure.path);
687
- this.pinnedProcedures.set(pathToKey(procedure.path), procedure);
710
+ this.pinnedProcedures.set(pathToKey(procedure.path), { procedure, alwaysOn: options.alwaysOn === true });
688
711
  }
689
712
 
690
713
  /**
691
- * Register multiple procedures.
714
+ * Register multiple procedures. The options apply to each one.
692
715
  *
693
716
  * @param procedures - Array of procedures
717
+ * @param options - `alwaysOn`: the expose rule does not apply to these pins
694
718
  */
695
- registerProcedures(procedures: AnyProcedure[]): void {
719
+ registerProcedures(procedures: AnyProcedure[], options: RegisterProcedureOptions = {}): void {
696
720
  for (const procedure of procedures) {
697
- this.registerProcedure(procedure);
721
+ this.registerProcedure(procedure, options);
698
722
  }
699
723
  }
700
724
 
701
725
  /**
702
- * Register a procedure module.
726
+ * Register a procedure module. The options apply to each procedure of the module.
703
727
  *
704
728
  * @param module - Module with procedures array
729
+ * @param options - `alwaysOn`: the expose rule does not apply to these pins
705
730
  */
706
- registerModule(module: ProcedureModule): void {
707
- this.registerProcedures(module.procedures);
731
+ registerModule(module: ProcedureModule, options: RegisterProcedureOptions = {}): void {
732
+ this.registerProcedures(module.procedures, options);
708
733
  }
709
734
 
710
735
  /**
@@ -717,19 +742,27 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
717
742
  this.servesRegistry = true;
718
743
  }
719
744
 
745
+ /** True when the expose rule of the server matches the path, or when the server has no rule. */
746
+ private exposes(path: ProcedurePath): boolean {
747
+ return !this.expose || this.expose(path);
748
+ }
749
+
720
750
  /**
721
751
  * The procedure that answers a method, or undefined. The registry comes first (when the server
722
- * serves it), then the pinned procedures.
752
+ * serves it), then the pinned procedures. The expose rule applies to both, and only an
753
+ * `alwaysOn` pin is outside it (CLI-47). Before, each pin was outside the rule, so an `exclude`
754
+ * of the host policy did not hide a collection procedure.
723
755
  */
724
756
  private resolveProcedure(method: Method): AnyProcedure | undefined {
725
757
  const path = methodToPath(method);
726
758
  if (this.servesRegistry) {
727
759
  const procedure = this.procedureRegistry.get(path);
728
- if (procedure?.handler && (!this.expose || this.expose(procedure.path))) {
760
+ if (procedure?.handler && this.exposes(procedure.path)) {
729
761
  return procedure;
730
762
  }
731
763
  }
732
- return this.pinnedProcedures.get(pathToKey(path));
764
+ const pin = this.pinnedProcedures.get(pathToKey(path));
765
+ return pin && (pin.alwaysOn || this.exposes(pin.procedure.path)) ? pin.procedure : undefined;
733
766
  }
734
767
 
735
768
  /**
@@ -999,6 +1032,9 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
999
1032
  * Register collection procedures for a specific collection.
1000
1033
  * Creates standard CRUD procedures using the configured storage.
1001
1034
  *
1035
+ * The expose rule of the server applies to these procedures (CLI-47). A rule that does not
1036
+ * match `collections.<name>.*` hides them, and an `exclude` of the host policy hides one of them.
1037
+ *
1002
1038
  * @param collectionName - Collection name
1003
1039
  * @param options - The query policy of `find`. `$regex` is off unless the policy permits it.
1004
1040
  */
@@ -1018,12 +1054,15 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
1018
1054
  // Utility Methods
1019
1055
  // ===========================================================================
1020
1056
 
1021
- /** The paths that the server serves now. */
1057
+ /** The paths that the server serves now: the same rule as `resolveProcedure()`. */
1022
1058
  private servedKeys(): Set<string> {
1023
- const keys = new Set(this.pinnedProcedures.keys());
1059
+ const keys = new Set<string>();
1060
+ for (const [key, pin] of this.pinnedProcedures) {
1061
+ if (pin.alwaysOn || this.exposes(pin.procedure.path)) keys.add(key);
1062
+ }
1024
1063
  if (this.servesRegistry) {
1025
1064
  for (const procedure of this.procedureRegistry.getAll()) {
1026
- if (procedure.handler && (!this.expose || this.expose(procedure.path))) {
1065
+ if (procedure.handler && this.exposes(procedure.path)) {
1027
1066
  keys.add(pathToKey(procedure.path));
1028
1067
  }
1029
1068
  }