@procedures/client 0.52.2 → 0.52.3

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.
@@ -59,11 +59,22 @@ export function exposePatterns(patterns: readonly string[]): (path: ProcedurePat
59
59
  };
60
60
  }
61
61
 
62
+ /**
63
+ * This function tells if a list of excluded keys names a path. An entry can be the key of the path
64
+ * (`collections.e2e%2Etabs.delete`) or its segments joined with plain dots
65
+ * (`collections.e2e.tabs.delete`), the form before CLI-75. So an exclusion of the old form still
66
+ * hides its procedure (audit 2026-11-08). The plain form can hide one more path, but not one less.
67
+ */
68
+ function isExcluded(excluded: ReadonlySet<string>, path: ProcedurePath): boolean {
69
+ // path-check: the raw form of an exclude entry before CLI-75, compared as text, not decoded.
70
+ return excluded.has(pathToKey(path)) || excluded.has(path.join("."));
71
+ }
72
+
62
73
  /** The rule of a tool surface: the namespaces, less the excluded keys. */
63
74
  export function surfaceRule(surface: ToolSurface): (path: ProcedurePath) => boolean {
64
75
  const namespaces = new Set(surface.namespaces);
65
76
  const excluded = new Set(surface.exclude ?? []);
66
- return (path) => namespaces.has(path[0] ?? "") && !excluded.has(pathToKey(path));
77
+ return (path) => namespaces.has(path[0] ?? "") && !isExcluded(excluded, path);
67
78
  }
68
79
 
69
80
  /** The options of `hostPolicy()`. */
@@ -163,7 +174,7 @@ function exposeOf(options: HostPolicyOptions): ((path: ProcedurePath) => boolean
163
174
  : surfaceRule(expose as ToolSurface);
164
175
  const excluded = new Set(options.exclude ?? []);
165
176
  if (excluded.size === 0) return rule;
166
- return (path) => !excluded.has(pathToKey(path)) && (rule ? rule(path) : true);
177
+ return (path) => !isExcluded(excluded, path) && (rule ? rule(path) : true);
167
178
  }
168
179
 
169
180
  /** This function builds the policy of a host from its options. */
@@ -460,6 +460,11 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
460
460
  * that `admission()` gave. Only this server adds an entry, so a request cannot claim one.
461
461
  */
462
462
  private readonly admissions = new WeakMap<Credentials, { readonly principal: Principal | undefined; readonly narrowing: ServerRequest<unknown>["narrowing"] }>();
463
+ /**
464
+ * The admission of each request object. The late lookup of a lazy registry admits a request
465
+ * before it loads packages, and the call of the same request uses that result again.
466
+ */
467
+ private readonly admitted = new WeakMap<ServerRequest<unknown>, Promise<ServerResponse<unknown> | undefined>>();
463
468
  /** The functions that `drain()` waits on: each one runs when no call is in flight */
464
469
  private idleWaiters: Array<() => void> = [];
465
470
  /** The publisher of the remote events, from the option `events`, or undefined. */
@@ -847,7 +852,20 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
847
852
  */
848
853
  protected override findLateHandler(method: Method, request: ServerRequest<unknown>): Promise<ServerHandler | null> | null {
849
854
  if (!this.servesRegistry || !this.procedureRegistry.lazy) return null;
850
- return this.procedureRegistry.resolve(methodToPath(method)).then(() => this.findHandler(method, request));
855
+ const path = methodToPath(method);
856
+ // A path outside the expose rule of the face loads nothing, as findHandler() gives it no handler
857
+ if (request.expose !== undefined) {
858
+ const rule: unknown = request.expose;
859
+ if (typeof rule !== "function" || rule(path) !== true) return null;
860
+ }
861
+ return (async (): Promise<ServerHandler | null> => {
862
+ // Only an admitted request loads packages (audit 2026-11-08). Before, a caller with no
863
+ // credentials loaded packages, and got the load errors of the packages.
864
+ const refusal = await this.admit(request);
865
+ if (refusal) return () => Promise.resolve(refusal);
866
+ await this.procedureRegistry.resolve(path);
867
+ return this.findHandler(method, request);
868
+ })();
851
869
  }
852
870
 
853
871
  /**
@@ -896,9 +914,20 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
896
914
  * request hook refuses it, and the hook does not run (CLI-1). The authenticate function gives
897
915
  * the principal. The credentials of an admission of this server (`admission()`) skip the
898
916
  * checks: the request gets the principal and the narrowing of that admission (CLI-2b). The
899
- * function gives an UNAUTHORIZED response, or undefined when the request passes.
900
- */
901
- private async admit(request: ServerRequest<unknown>): Promise<ServerResponse<unknown> | undefined> {
917
+ * function gives an UNAUTHORIZED response, or undefined when the request passes. The checks
918
+ * run one time for each request object.
919
+ */
920
+ private admit(request: ServerRequest<unknown>): Promise<ServerResponse<unknown> | undefined> {
921
+ let result = this.admitted.get(request);
922
+ if (result === undefined) {
923
+ result = this.checkRequest(request);
924
+ this.admitted.set(request, result);
925
+ }
926
+ return result;
927
+ }
928
+
929
+ /** The checks of `admit()` for one request object. */
930
+ private async checkRequest(request: ServerRequest<unknown>): Promise<ServerResponse<unknown> | undefined> {
902
931
  const unauthorized = (message: string): ServerResponse<unknown> => ({
903
932
  id: request.id,
904
933
  status: { type: "error", code: "UNAUTHORIZED", message, retryable: false },