@webpieces/core-util 0.4.863 → 0.4.865

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.
Files changed (55) hide show
  1. package/package.json +1 -1
  2. package/src/ContextKey.d.ts +3 -3
  3. package/src/ContextKey.js +3 -3
  4. package/src/ContextKey.js.map +1 -1
  5. package/src/http/ContextMgr.d.ts +3 -3
  6. package/src/http/ContextMgr.js +3 -3
  7. package/src/http/ContextMgr.js.map +1 -1
  8. package/src/http/DeletedAuthCompileAssertions.d.ts +4 -0
  9. package/src/http/DeletedAuthCompileAssertions.js +30 -0
  10. package/src/http/DeletedAuthCompileAssertions.js.map +1 -0
  11. package/src/http/DestinationTrust.d.ts +6 -6
  12. package/src/http/DestinationTrust.js +12 -13
  13. package/src/http/DestinationTrust.js.map +1 -1
  14. package/src/http/HttpEndpointOptions.d.ts +2 -2
  15. package/src/http/HttpEndpointOptions.js.map +1 -1
  16. package/src/http/RouteMetadata.d.ts +6 -1
  17. package/src/http/RouteMetadata.js +14 -1
  18. package/src/http/RouteMetadata.js.map +1 -1
  19. package/src/http/RouteMetadataFactory.js +9 -3
  20. package/src/http/RouteMetadataFactory.js.map +1 -1
  21. package/src/http/RuntimeLocality.d.ts +3 -3
  22. package/src/http/RuntimeLocality.js +1 -1
  23. package/src/http/RuntimeLocality.js.map +1 -1
  24. package/src/http/Secrets.d.ts +2 -2
  25. package/src/http/Secrets.js +2 -2
  26. package/src/http/Secrets.js.map +1 -1
  27. package/src/http/Surface.d.ts +4 -4
  28. package/src/http/Surface.js.map +1 -1
  29. package/src/http/WebpiecesCoreHeaders.d.ts +3 -3
  30. package/src/http/WebpiecesCoreHeaders.js +3 -3
  31. package/src/http/WebpiecesCoreHeaders.js.map +1 -1
  32. package/src/http/WpAuthApiKeyCompileAssertions.d.ts +4 -8
  33. package/src/http/WpAuthApiKeyCompileAssertions.js +8 -13
  34. package/src/http/WpAuthApiKeyCompileAssertions.js.map +1 -1
  35. package/src/http/WpAuthJwtCompileAssertions.d.ts +1 -27
  36. package/src/http/WpAuthJwtCompileAssertions.js +13 -48
  37. package/src/http/WpAuthJwtCompileAssertions.js.map +1 -1
  38. package/src/http/WpAuthorizationCompileAssertions.d.ts +1 -0
  39. package/src/http/WpAuthorizationCompileAssertions.js +26 -0
  40. package/src/http/WpAuthorizationCompileAssertions.js.map +1 -0
  41. package/src/http/auth-mode.d.ts +22 -130
  42. package/src/http/auth-mode.js +100 -26
  43. package/src/http/auth-mode.js.map +1 -1
  44. package/src/http/authorization.d.ts +38 -0
  45. package/src/http/authorization.js +71 -0
  46. package/src/http/authorization.js.map +1 -0
  47. package/src/http/decorators.d.ts +11 -114
  48. package/src/http/decorators.js +47 -157
  49. package/src/http/decorators.js.map +1 -1
  50. package/src/index.d.ts +5 -4
  51. package/src/index.js +21 -19
  52. package/src/index.js.map +1 -1
  53. package/src/mcp/McpAuthMetadata.d.ts +0 -9
  54. package/src/mcp/McpAuthMetadata.js +0 -31
  55. package/src/mcp/McpAuthMetadata.js.map +0 -1
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AuthorizationDeclaration = exports.AUTHORIZATION_METHODS_KEY = exports.AUTHORIZATION_METADATA_KEY = exports.AuthorizationType = void 0;
4
+ exports.WpAuthorization = WpAuthorization;
5
+ exports.getAuthorization = getAuthorization;
6
+ require("reflect-metadata");
7
+ var AuthorizationType;
8
+ (function (AuthorizationType) {
9
+ AuthorizationType["ALL_USERS"] = "all-users";
10
+ AuthorizationType["ROLES"] = "roles";
11
+ AuthorizationType["SERVICE_ONLY"] = "service-only";
12
+ AuthorizationType["USERS_OR_SERVICES"] = "users-or-services";
13
+ AuthorizationType["ANONYMOUS"] = "anonymous";
14
+ AuthorizationType["CUSTOM"] = "custom";
15
+ })(AuthorizationType || (exports.AuthorizationType = AuthorizationType = {}));
16
+ exports.AUTHORIZATION_METADATA_KEY = 'webpieces:authorization';
17
+ exports.AUTHORIZATION_METHODS_KEY = 'webpieces:authorization-methods';
18
+ /** Runtime checks also protect JavaScript and type-erased callers. */
19
+ class AuthorizationDeclaration {
20
+ // webpieces-disable no-any-unknown -- decorator policy is application-owned and validated before storage
21
+ // webpieces-disable no-function-outside-class -- stateless contract validation or scope metadata reader used before DI registration
22
+ static validate(value) {
23
+ if (!value || typeof value !== 'object' || !Object.values(AuthorizationType).includes(value.authType)) {
24
+ throw new Error('@WpAuthorization requires a recognized AuthorizationType.');
25
+ }
26
+ const allowed = new Set(['authType']);
27
+ if (value.authType === AuthorizationType.ROLES)
28
+ allowed.add('roles');
29
+ if (value.authType === AuthorizationType.ANONYMOUS)
30
+ allowed.add('reason');
31
+ if (value.authType === AuthorizationType.CUSTOM)
32
+ allowed.add('appPolicy');
33
+ for (const key of Object.keys(value)) {
34
+ if (!allowed.has(key))
35
+ throw new Error(`@WpAuthorization ${value.authType} rejects '${key}'; application policy belongs in CUSTOM.appPolicy.`);
36
+ }
37
+ if (value.authType === AuthorizationType.ROLES &&
38
+ (!Array.isArray(value.roles) || value.roles.length === 0 || value.roles.some((role) => typeof role !== 'string' || role.trim() === ''))) {
39
+ throw new Error('@WpAuthorization ROLES requires a nonempty tuple of nonempty role names.');
40
+ }
41
+ if (value.authType === AuthorizationType.ANONYMOUS &&
42
+ (typeof value.reason !== 'string' || value.reason.trim() === '')) {
43
+ throw new Error('@WpAuthorization ANONYMOUS requires a nonempty reason.');
44
+ }
45
+ if (value.authType === AuthorizationType.CUSTOM && !Object.hasOwn(value, 'appPolicy')) {
46
+ throw new Error('@WpAuthorization CUSTOM requires appPolicy and a bound AuthorizationHook.');
47
+ }
48
+ }
49
+ }
50
+ exports.AuthorizationDeclaration = AuthorizationDeclaration;
51
+ // webpieces-disable no-function-outside-class -- canonical TypeScript method decorator factory
52
+ function WpAuthorization(policy) {
53
+ AuthorizationDeclaration.validate(policy);
54
+ return (target, propertyKey) => {
55
+ if (propertyKey === undefined)
56
+ throw new Error('@WpAuthorization is method-only.');
57
+ const apiClass = target.constructor;
58
+ if (Reflect.hasMetadata(exports.AUTHORIZATION_METADATA_KEY, apiClass, propertyKey)) {
59
+ throw new Error(`Duplicate @WpAuthorization on ${apiClass.name}.${String(propertyKey)}.`);
60
+ }
61
+ Reflect.defineMetadata(exports.AUTHORIZATION_METADATA_KEY, Object.freeze(policy), apiClass, propertyKey);
62
+ const methods = new Set(Reflect.getMetadata(exports.AUTHORIZATION_METHODS_KEY, apiClass) ?? []);
63
+ methods.add(String(propertyKey));
64
+ Reflect.defineMetadata(exports.AUTHORIZATION_METHODS_KEY, [...methods], apiClass);
65
+ };
66
+ }
67
+ // webpieces-disable no-function-outside-class -- canonical Reflect metadata reader; webpieces-disable no-any-unknown -- erased custom policy is validated by the application hook at startup
68
+ function getAuthorization(apiClass, methodName) {
69
+ return Reflect.getMetadata(exports.AUTHORIZATION_METADATA_KEY, apiClass, methodName);
70
+ }
71
+ //# sourceMappingURL=authorization.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorization.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/authorization.ts"],"names":[],"mappings":"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","sourcesContent":["import 'reflect-metadata';\n\nexport enum AuthorizationType {\n ALL_USERS = 'all-users',\n ROLES = 'roles',\n SERVICE_ONLY = 'service-only',\n USERS_OR_SERVICES = 'users-or-services',\n ANONYMOUS = 'anonymous',\n CUSTOM = 'custom',\n}\n\nexport type AuthorizationRequirement<TPolicy = never> =\n | {\n authType: AuthorizationType.ALL_USERS | AuthorizationType.SERVICE_ONLY | AuthorizationType.USERS_OR_SERVICES;\n roles?: never;\n reason?: never;\n appPolicy?: never;\n }\n | {\n authType: AuthorizationType.ROLES;\n roles: readonly [string, ...string[]];\n reason?: never;\n appPolicy?: never;\n }\n | {\n authType: AuthorizationType.ANONYMOUS;\n reason: string;\n roles?: never;\n appPolicy?: never;\n }\n | {\n authType: AuthorizationType.CUSTOM;\n appPolicy: TPolicy;\n roles?: never;\n reason?: never;\n };\n\nexport const AUTHORIZATION_METADATA_KEY = 'webpieces:authorization';\nexport const AUTHORIZATION_METHODS_KEY = 'webpieces:authorization-methods';\n\n/** Runtime checks also protect JavaScript and type-erased callers. */\nexport class AuthorizationDeclaration {\n // webpieces-disable no-any-unknown -- decorator policy is application-owned and validated before storage\n // webpieces-disable no-function-outside-class -- stateless contract validation or scope metadata reader used before DI registration\n static validate(value: AuthorizationRequirement<unknown>): void {\n if (!value || typeof value !== 'object' || !Object.values(AuthorizationType).includes(value.authType)) {\n throw new Error('@WpAuthorization requires a recognized AuthorizationType.');\n }\n const allowed = new Set(['authType']);\n if (value.authType === AuthorizationType.ROLES) allowed.add('roles');\n if (value.authType === AuthorizationType.ANONYMOUS) allowed.add('reason');\n if (value.authType === AuthorizationType.CUSTOM) allowed.add('appPolicy');\n for (const key of Object.keys(value)) {\n if (!allowed.has(key)) throw new Error(`@WpAuthorization ${value.authType} rejects '${key}'; application policy belongs in CUSTOM.appPolicy.`);\n }\n if (value.authType === AuthorizationType.ROLES &&\n (!Array.isArray(value.roles) || value.roles.length === 0 || value.roles.some((role: string) => typeof role !== 'string' || role.trim() === ''))) {\n throw new Error('@WpAuthorization ROLES requires a nonempty tuple of nonempty role names.');\n }\n if (value.authType === AuthorizationType.ANONYMOUS &&\n (typeof value.reason !== 'string' || value.reason.trim() === '')) {\n throw new Error('@WpAuthorization ANONYMOUS requires a nonempty reason.');\n }\n if (value.authType === AuthorizationType.CUSTOM && !Object.hasOwn(value, 'appPolicy')) {\n throw new Error('@WpAuthorization CUSTOM requires appPolicy and a bound AuthorizationHook.');\n }\n }\n}\n\n// webpieces-disable no-function-outside-class -- canonical TypeScript method decorator factory\nexport function WpAuthorization<TPolicy = never>(policy: AuthorizationRequirement<TPolicy>): MethodDecorator {\n AuthorizationDeclaration.validate(policy);\n return (target: object, propertyKey: string | symbol) => {\n if (propertyKey === undefined) throw new Error('@WpAuthorization is method-only.');\n const apiClass = target.constructor;\n if (Reflect.hasMetadata(AUTHORIZATION_METADATA_KEY, apiClass, propertyKey)) {\n throw new Error(`Duplicate @WpAuthorization on ${apiClass.name}.${String(propertyKey)}.`);\n }\n Reflect.defineMetadata(AUTHORIZATION_METADATA_KEY, Object.freeze(policy), apiClass, propertyKey);\n const methods = new Set<string>(Reflect.getMetadata(AUTHORIZATION_METHODS_KEY, apiClass) ?? []);\n methods.add(String(propertyKey));\n Reflect.defineMetadata(AUTHORIZATION_METHODS_KEY, [...methods], apiClass);\n };\n}\n\n// webpieces-disable no-function-outside-class -- canonical Reflect metadata reader; webpieces-disable no-any-unknown -- erased custom policy is validated by the application hook at startup\nexport function getAuthorization(apiClass: Function, methodName: string): AuthorizationRequirement<unknown> | undefined {\n return Reflect.getMetadata(AUTHORIZATION_METADATA_KEY, apiClass, methodName);\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import 'reflect-metadata';
2
2
  import { MaskSpec, MaskMode } from './LogFieldMask';
3
- import { ApiKeyCredentials, AuthMeta, AuthMode, JwtRequirement } from './auth-mode';
3
+ import { AuthMeta, AuthMethods } from './auth-mode';
4
4
  import { EndpointOperation, EndpointOptions, ExternalEndpointOptions } from './HttpEndpointOptions';
5
5
  import { HttpMethod } from './HttpContract';
6
6
  export type { EndpointOptions, ExternalEndpointOptions } from './HttpEndpointOptions';
@@ -42,7 +42,7 @@ export declare const METADATA_KEYS: {
42
42
  /** Per-method request/response event metadata for a typed streaming endpoint. */
43
43
  STREAM_ENDPOINTS: string;
44
44
  /** Per-method MCP-user authorization, intentionally separate from HTTP hop auth. */
45
- MCP_AUTH_JWT: string;
45
+ LOCAL_ONLY: string;
46
46
  /** Per-method explicit path/query parameter declarations, keyed by parameter index. */
47
47
  HTTP_PARAMETERS: string;
48
48
  };
@@ -81,109 +81,11 @@ export declare function getMaskSpec(apiClass: Function, methodName: string): Mas
81
81
  * The non-empty reason makes anonymous exposure an explicit review decision.
82
82
  */
83
83
  export declare function WpAuthPublic(reason: string): MethodDecorator;
84
- /**
85
- * @WpAuthJwt(requirement) - THE user-facing JWT decorator, covering the whole user-JWT axis: the
86
- * compiler-enforced role decision ({@link JwtRoles}) plus app-defined fields ({@link JwtRequirement}).
87
- *
88
- * ```typescript
89
- * @WpAuthJwt({ roles: ['admin', 'editor'] }) // any-of
90
- * @WpAuthJwt({ allRolesAllowed: true, inOrg: true }) // wide + an app rule enforced by authorizeJwt
91
- * ```
92
- *
93
- * It absorbed the former `@Auth(requirement)` — same argument, same AuthMode, so two spellings of one
94
- * decision. One decorator per credential kind now: `@WpAuthPublic` / `@WpAuthJwt` / `@WpAuthOidc` /
95
- * `@WpAuthSharedSecret` / `@WpAuthLocalOnly`.
96
- */
97
- export declare function WpAuthJwt(requirement: JwtRequirement): MethodDecorator;
98
- /**
99
- * The roles an endpoint accepts, or [] when it accepts every authenticated user. The ONE reader of
100
- * the {@link JwtRoles} union, so no caller has to re-derive "does absent mean wide?" — a question
101
- * whose two plausible answers is how the widest grant kept hiding behind an absent field.
102
- */
103
- export declare function rolesRequired(requirement: JwtRequirement): readonly string[];
104
- /**
105
- * @WpAuthOidc(...callers) - Google OIDC service-to-service auth (Cloud Tasks delivery / cross-service
106
- * RPC). `callers` is an OPTIONAL app-level allow-list of caller service accounts.
107
- *
108
- * NO args = TRUST THE EDGE: accept any genuine Google-signed OIDC caller, because a PRIVATE Cloud
109
- * Run service's edge already gates WHO via `run.invoker` IAM (managed in terraform — one source of
110
- * truth, no hand-synced list in code). If the service is actually PUBLIC, the verifier logs a loud
111
- * warning (it can't be the gate then). Pass explicit SAs (`@WpAuthOidc('svc-a')`) only when you want
112
- * an additional app-level allow-list as defense-in-depth.
113
- */
114
- export declare function WpAuthOidc(...callers: string[]): MethodDecorator;
115
- /**
116
- * @WpAuthSharedSecret(key) - constant-time compare of an inbound header against the secret bound for
117
- * `key`. `key` is a LOOKUP KEY (not an env var): the server looks up its accepted {@link SharedSecrets}
118
- * by this key, and each client looks up the value it sends by the SAME key (see {@link Secrets}).
119
- * For internal callers that cannot mint OIDC tokens.
120
- */
121
- export declare function WpAuthSharedSecret(key: string): MethodDecorator;
122
- /**
123
- * @WpAuthWebhook(name) - an OUTSIDE vendor signed this request in its OWN scheme; the app's bound
124
- * `WebhookAuthCallback` proves it. THE mode for every signed inbound webhook — Sentry, GitHub, Stripe, Slack,
125
- * Twilio — none of which fits the other kinds: no vendor mints Google OIDC tokens, and none sends its
126
- * secret (they all send a DERIVATION over the request), so `@WpAuthPublic` was the only reachable posture
127
- * and `calledBy: 'sentry'` stayed a claim rather than a fact.
128
- *
129
- * ```typescript
130
- * @WpAuthWebhook('sentry')
131
- * @Endpoint(POST, '/hook/sentry/issue', WRITE, EXTERNAL, { calledBy: 'sentry', rawBody: true })
132
- * abstract notify(request: SentryIssueHook): Promise<HookAck>;
133
- * ```
134
- *
135
- * `name` is a bare STRING resolved through DI in the server's container, exactly as
136
- * `@WpAuthOidc('gmail-push')` already is — never a function reference. An api contract is level 0: a
137
- * direct reference to a verifier would invert the dependency graph and drag a vendor SDK into the
138
- * browser bundle that imports the same contract.
139
- *
140
- * THE FRAMEWORK IMPLEMENTS NO VENDOR CRYPTO, deliberately. Every vendor ships an official validator
141
- * (`twilio.validateRequest`, `stripe.webhooks.constructEvent`, `@octokit/webhooks-methods`) and every
142
- * vendor revises its scheme (Twilio added `bodySHA256` for JSON bodies; Stripe versions its header).
143
- * Reimplementing five of those is signing up to track five security changelogs forever and to be
144
- * wrong at the moment being wrong matters. The framework hands the hook enough of the raw request to
145
- * call the vendor's own library — hence the REQUIRED `{ rawBody: true }` (see
146
- * {@link EndpointOptions.rawBody}), which is checked at wiring time.
147
- *
148
- * FAILS CLOSED: with no `WebhookAuthCallback` bound, every `@WpAuthWebhook` endpoint 401s, matching `JwtHook`.
149
- * Silently allowing an unverified webhook is the one default that must not exist.
150
- */
151
- export declare function WpAuthWebhook(name: string): MethodDecorator;
152
- /**
153
- * Declares a customer-held API-key regime and its non-empty ordered credential locations. The app's
154
- * `ApiKeyHook` validates the whole request and supplies trusted context; the declaration only drives
155
- * contract/spec metadata. Unlike shared-secret auth, partner callers cannot assert trusted context.
156
- * Missing hooks fail closed with 401.
157
- */
158
- export declare function WpAuthApiKey(regime: string, credentials: ApiKeyCredentials): MethodDecorator;
159
- /**
160
- * @WpAuthLocalOnly() - this endpoint exists ONLY on a developer's machine. Off-local it is not
161
- * registered as a route at all, and if it is somehow reached it 404s. Method-only, like every
162
- * authorization decorator, so every endpoint's posture is visible at the method.
163
- *
164
- * ```typescript
165
- * @WpAuthLocalOnly()
166
- * @Endpoint(POST, '/logs', WRITE, RPC)
167
- * sendBatch(request: SendLogBatchRequest): Promise<SendLogBatchResponse> { ... }
168
- * ```
169
- *
170
- * WHY IT IS AN AUTH MODE AND NOT A ROUTE-MODULE `if`. Apps hand-rolled this in TWO places kept in
171
- * sync by a comment: a route module that registered the route only locally, PLUS a
172
- * `if (env !== 'local') throw new ApiForbiddenError(...)` at the top of the handler. Neither half
173
- * was visible on the CONTRACT, so nothing reading the api — a human, a generated client, or an
174
- * agent — could tell this endpoint from a `@WpAuthPublic` one. Both halves are the framework's job now,
175
- * driven by this ONE declaration on the contract, which is where every other "who may call this"
176
- * fact already lives.
177
- *
178
- * It is DELIBERATELY a peer of @WpAuthPublic / @WpAuthJwt / @WpAuthOidc / @WpAuthSharedSecret / @WpAuthApiKey rather than an
179
- * option on one of them: one decorator per credential kind, and "local-only" is a different kind of
180
- * gate — it authenticates nobody, it excludes an entire environment.
181
- *
182
- * HOW "local" IS DECIDED: {@link RuntimeLocality}, declared once at startup (a REQUIRED input to
183
- * `RuntimeSetupOptions`). Undeclared means DEPLOYED, so a forgotten wiring call refuses the endpoint
184
- * rather than exposing it.
185
- */
186
- export declare function WpAuthLocalOnly(): MethodDecorator;
84
+ /** Protected network ingress: authenticate one declared credential, then authorize independently. */
85
+ export declare function WpAuth(methods: AuthMethods): MethodDecorator;
86
+ /** Locality is independent of authentication and cannot prove a user or machine identity. */
87
+ export declare function WpLocalOnly(): MethodDecorator;
88
+ export declare function isLocalOnly(apiClass: Function, methodName: string): boolean;
187
89
  /**
188
90
  * Get the base path from @ApiPath decorator.
189
91
  */
@@ -229,20 +131,20 @@ export declare function assertEveryExternalEndpointDeclaresCaller(apiClass: Func
229
131
  export declare function isFormPost(apiClass: Function, methodName: string): boolean;
230
132
  /**
231
133
  * True when the method's @Endpoint declared `{ rawBody: true }` — the transport must retain the
232
- * verbatim bytes + absolute url for an {@link WpAuthWebhook} hook to verify.
134
+ * verbatim bytes + absolute url for an {@link WebhookAuthCallback} hook to verify.
233
135
  */
234
136
  export declare function isRawBody(apiClass: Function, methodName: string): boolean;
235
137
  /**
236
- * Fail-fast at wiring time when an `@WpAuthWebhook` endpoint did not ask the transport to keep the
138
+ * Fail-fast at wiring time when an `webhook(...)` endpoint did not ask the transport to keep the
237
139
  * bytes it is supposed to verify. A hook with nothing to verify is a MISCONFIGURATION, and it must
238
140
  * surface at startup, naming the fix — not as a 401 in production on exactly the traffic the endpoint
239
141
  * exists for.
240
142
  *
241
143
  * This pairing is a runtime assert rather than a type because the two halves live on DIFFERENT
242
- * decorators (`@WpAuthWebhook` and `@Endpoint`), and no union over one decorator's argument can say
144
+ * decorators (`webhook(...)` and `@Endpoint`), and no union over one decorator's argument can say
243
145
  * anything about the other's.
244
146
  *
245
- * @throws Error naming the first `@WpAuthWebhook` endpoint missing `{ rawBody: true }`.
147
+ * @throws Error naming the first `webhook(...)` endpoint missing `{ rawBody: true }`.
246
148
  */
247
149
  export declare function assertEveryWebhookEndpointRetainsRawBody(apiClass: Function): void;
248
150
  /**
@@ -253,11 +155,6 @@ export declare function isApiPath(apiClass: Function): boolean;
253
155
  * Get method-level auth metadata. Class-level authorization is forbidden.
254
156
  */
255
157
  export declare function getAuthMeta(apiClass: Function, methodName: string): AuthMeta | undefined;
256
- /**
257
- * Get the auth mode for a method, or undefined.
258
- * Convenience wrapper over getAuthMeta for callers that only want the mode.
259
- */
260
- export declare function getAuthMode(apiClass: Function, methodName: string): AuthMode | undefined;
261
158
  /**
262
159
  * The ONE prescription for "this endpoint declares no auth", shared by the two places that raise it
263
160
  * (here and http-routing's ApiRoutingFactory) because they had drifted into teaching different menus.
@@ -6,13 +6,9 @@ exports.Endpoint = Endpoint;
6
6
  exports.MaskLog = MaskLog;
7
7
  exports.getMaskSpec = getMaskSpec;
8
8
  exports.WpAuthPublic = WpAuthPublic;
9
- exports.WpAuthJwt = WpAuthJwt;
10
- exports.rolesRequired = rolesRequired;
11
- exports.WpAuthOidc = WpAuthOidc;
12
- exports.WpAuthSharedSecret = WpAuthSharedSecret;
13
- exports.WpAuthWebhook = WpAuthWebhook;
14
- exports.WpAuthApiKey = WpAuthApiKey;
15
- exports.WpAuthLocalOnly = WpAuthLocalOnly;
9
+ exports.WpAuth = WpAuth;
10
+ exports.WpLocalOnly = WpLocalOnly;
11
+ exports.isLocalOnly = isLocalOnly;
16
12
  exports.getApiPath = getApiPath;
17
13
  exports.getEndpoints = getEndpoints;
18
14
  exports.getEndpointKinds = getEndpointKinds;
@@ -26,7 +22,6 @@ exports.isRawBody = isRawBody;
26
22
  exports.assertEveryWebhookEndpointRetainsRawBody = assertEveryWebhookEndpointRetainsRawBody;
27
23
  exports.isApiPath = isApiPath;
28
24
  exports.getAuthMeta = getAuthMeta;
29
- exports.getAuthMode = getAuthMode;
30
25
  exports.assertEveryEndpointHasAuthMode = assertEveryEndpointHasAuthMode;
31
26
  exports.validateNoConflictingDecorators = validateNoConflictingDecorators;
32
27
  require("reflect-metadata");
@@ -36,6 +31,7 @@ const external_caller_1 = require("./external-caller");
36
31
  const auth_mode_1 = require("./auth-mode");
37
32
  const HttpEndpointOptions_1 = require("./HttpEndpointOptions");
38
33
  const HttpContract_1 = require("./HttpContract");
34
+ const authorization_1 = require("./authorization");
39
35
  const http_parameter_decorators_1 = require("./http-parameter-decorators");
40
36
  var HttpEndpointOptions_2 = require("./HttpEndpointOptions");
41
37
  Object.defineProperty(exports, "READ", { enumerable: true, get: function () { return HttpEndpointOptions_2.READ; } });
@@ -81,7 +77,7 @@ exports.METADATA_KEYS = {
81
77
  /** Per-method request/response event metadata for a typed streaming endpoint. */
82
78
  STREAM_ENDPOINTS: 'webpieces:stream-endpoints',
83
79
  /** Per-method MCP-user authorization, intentionally separate from HTTP hop auth. */
84
- MCP_AUTH_JWT: 'webpieces:mcp-auth-jwt',
80
+ LOCAL_ONLY: 'webpieces:local-only',
85
81
  /** Per-method explicit path/query parameter declarations, keyed by parameter index. */
86
82
  HTTP_PARAMETERS: http_parameter_decorators_1.HTTP_PARAMETERS_METADATA_KEY,
87
83
  };
@@ -184,8 +180,7 @@ function getMaskSpec(apiClass, methodName) {
184
180
  * method-only: a class-level default is too easy to miss when reviewing one endpoint.
185
181
  */
186
182
  // webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope
187
- function defineAuthMode(mode) {
188
- const authMeta = new auth_mode_1.AuthMeta(mode);
183
+ function defineAuthMetadata(authMeta) {
189
184
  // webpieces-disable no-any-unknown -- reflect-metadata decorator API requires any
190
185
  return (target, propertyKey, _descriptor) => {
191
186
  if (propertyKey === undefined) {
@@ -211,131 +206,25 @@ function WpAuthPublic(reason) {
211
206
  if (typeof reason !== 'string' || reason.trim() === '') {
212
207
  throw new Error('@WpAuthPublic requires a non-empty reason explaining why anonymous access is required.');
213
208
  }
214
- return defineAuthMode({ kind: 'public' });
215
- }
216
- /**
217
- * @WpAuthJwt(requirement) - THE user-facing JWT decorator, covering the whole user-JWT axis: the
218
- * compiler-enforced role decision ({@link JwtRoles}) plus app-defined fields ({@link JwtRequirement}).
219
- *
220
- * ```typescript
221
- * @WpAuthJwt({ roles: ['admin', 'editor'] }) // any-of
222
- * @WpAuthJwt({ allRolesAllowed: true, inOrg: true }) // wide + an app rule enforced by authorizeJwt
223
- * ```
224
- *
225
- * It absorbed the former `@Auth(requirement)` — same argument, same AuthMode, so two spellings of one
226
- * decision. One decorator per credential kind now: `@WpAuthPublic` / `@WpAuthJwt` / `@WpAuthOidc` /
227
- * `@WpAuthSharedSecret` / `@WpAuthLocalOnly`.
228
- */
229
- // webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope
230
- function WpAuthJwt(requirement) {
231
- return defineAuthMode({ kind: 'jwt', requirement });
232
- }
233
- /**
234
- * The roles an endpoint accepts, or [] when it accepts every authenticated user. The ONE reader of
235
- * the {@link JwtRoles} union, so no caller has to re-derive "does absent mean wide?" — a question
236
- * whose two plausible answers is how the widest grant kept hiding behind an absent field.
237
- */
238
- // webpieces-disable no-function-outside-class -- reflect-metadata reader, sibling of getAuthMode
239
- function rolesRequired(requirement) {
240
- return requirement.allRolesAllowed === true ? [] : requirement.roles;
241
- }
242
- /**
243
- * @WpAuthOidc(...callers) - Google OIDC service-to-service auth (Cloud Tasks delivery / cross-service
244
- * RPC). `callers` is an OPTIONAL app-level allow-list of caller service accounts.
245
- *
246
- * NO args = TRUST THE EDGE: accept any genuine Google-signed OIDC caller, because a PRIVATE Cloud
247
- * Run service's edge already gates WHO via `run.invoker` IAM (managed in terraform — one source of
248
- * truth, no hand-synced list in code). If the service is actually PUBLIC, the verifier logs a loud
249
- * warning (it can't be the gate then). Pass explicit SAs (`@WpAuthOidc('svc-a')`) only when you want
250
- * an additional app-level allow-list as defense-in-depth.
251
- */
252
- // webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope
253
- function WpAuthOidc(...callers) {
254
- return defineAuthMode({ kind: 'oidc', callers });
255
- }
256
- /**
257
- * @WpAuthSharedSecret(key) - constant-time compare of an inbound header against the secret bound for
258
- * `key`. `key` is a LOOKUP KEY (not an env var): the server looks up its accepted {@link SharedSecrets}
259
- * by this key, and each client looks up the value it sends by the SAME key (see {@link Secrets}).
260
- * For internal callers that cannot mint OIDC tokens.
261
- */
262
- // webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope
263
- function WpAuthSharedSecret(key) {
264
- return defineAuthMode({ kind: 'shared-secret', secretKey: key });
265
- }
266
- /**
267
- * @WpAuthWebhook(name) - an OUTSIDE vendor signed this request in its OWN scheme; the app's bound
268
- * `WebhookAuthCallback` proves it. THE mode for every signed inbound webhook — Sentry, GitHub, Stripe, Slack,
269
- * Twilio — none of which fits the other kinds: no vendor mints Google OIDC tokens, and none sends its
270
- * secret (they all send a DERIVATION over the request), so `@WpAuthPublic` was the only reachable posture
271
- * and `calledBy: 'sentry'` stayed a claim rather than a fact.
272
- *
273
- * ```typescript
274
- * @WpAuthWebhook('sentry')
275
- * @Endpoint(POST, '/hook/sentry/issue', WRITE, EXTERNAL, { calledBy: 'sentry', rawBody: true })
276
- * abstract notify(request: SentryIssueHook): Promise<HookAck>;
277
- * ```
278
- *
279
- * `name` is a bare STRING resolved through DI in the server's container, exactly as
280
- * `@WpAuthOidc('gmail-push')` already is — never a function reference. An api contract is level 0: a
281
- * direct reference to a verifier would invert the dependency graph and drag a vendor SDK into the
282
- * browser bundle that imports the same contract.
283
- *
284
- * THE FRAMEWORK IMPLEMENTS NO VENDOR CRYPTO, deliberately. Every vendor ships an official validator
285
- * (`twilio.validateRequest`, `stripe.webhooks.constructEvent`, `@octokit/webhooks-methods`) and every
286
- * vendor revises its scheme (Twilio added `bodySHA256` for JSON bodies; Stripe versions its header).
287
- * Reimplementing five of those is signing up to track five security changelogs forever and to be
288
- * wrong at the moment being wrong matters. The framework hands the hook enough of the raw request to
289
- * call the vendor's own library — hence the REQUIRED `{ rawBody: true }` (see
290
- * {@link EndpointOptions.rawBody}), which is checked at wiring time.
291
- *
292
- * FAILS CLOSED: with no `WebhookAuthCallback` bound, every `@WpAuthWebhook` endpoint 401s, matching `JwtHook`.
293
- * Silently allowing an unverified webhook is the one default that must not exist.
294
- */
295
- // webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope
296
- function WpAuthWebhook(name) {
297
- return defineAuthMode({ kind: 'webhook', name });
209
+ return defineAuthMetadata(new auth_mode_1.AuthMeta([{ kind: 'public' }], reason));
210
+ }
211
+ /** Protected network ingress: authenticate one declared credential, then authorize independently. */
212
+ // webpieces-disable no-function-outside-class -- canonical method decorator factory
213
+ function WpAuth(methods) {
214
+ return defineAuthMetadata(new auth_mode_1.AuthMeta(methods, undefined));
215
+ }
216
+ /** Locality is independent of authentication and cannot prove a user or machine identity. */
217
+ // webpieces-disable no-function-outside-class -- canonical method decorator factory
218
+ function WpLocalOnly() {
219
+ return (target, methodName) => {
220
+ if (methodName === undefined)
221
+ throw new Error('@WpLocalOnly is method-only.');
222
+ Reflect.defineMetadata(exports.METADATA_KEYS.LOCAL_ONLY, true, target.constructor, methodName);
223
+ };
298
224
  }
299
- /**
300
- * Declares a customer-held API-key regime and its non-empty ordered credential locations. The app's
301
- * `ApiKeyHook` validates the whole request and supplies trusted context; the declaration only drives
302
- * contract/spec metadata. Unlike shared-secret auth, partner callers cannot assert trusted context.
303
- * Missing hooks fail closed with 401.
304
- */
305
- // webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope
306
- function WpAuthApiKey(regime, credentials) {
307
- return defineAuthMode({ kind: 'apikey', regime, credentials });
308
- }
309
- /**
310
- * @WpAuthLocalOnly() - this endpoint exists ONLY on a developer's machine. Off-local it is not
311
- * registered as a route at all, and if it is somehow reached it 404s. Method-only, like every
312
- * authorization decorator, so every endpoint's posture is visible at the method.
313
- *
314
- * ```typescript
315
- * @WpAuthLocalOnly()
316
- * @Endpoint(POST, '/logs', WRITE, RPC)
317
- * sendBatch(request: SendLogBatchRequest): Promise<SendLogBatchResponse> { ... }
318
- * ```
319
- *
320
- * WHY IT IS AN AUTH MODE AND NOT A ROUTE-MODULE `if`. Apps hand-rolled this in TWO places kept in
321
- * sync by a comment: a route module that registered the route only locally, PLUS a
322
- * `if (env !== 'local') throw new ApiForbiddenError(...)` at the top of the handler. Neither half
323
- * was visible on the CONTRACT, so nothing reading the api — a human, a generated client, or an
324
- * agent — could tell this endpoint from a `@WpAuthPublic` one. Both halves are the framework's job now,
325
- * driven by this ONE declaration on the contract, which is where every other "who may call this"
326
- * fact already lives.
327
- *
328
- * It is DELIBERATELY a peer of @WpAuthPublic / @WpAuthJwt / @WpAuthOidc / @WpAuthSharedSecret / @WpAuthApiKey rather than an
329
- * option on one of them: one decorator per credential kind, and "local-only" is a different kind of
330
- * gate — it authenticates nobody, it excludes an entire environment.
331
- *
332
- * HOW "local" IS DECIDED: {@link RuntimeLocality}, declared once at startup (a REQUIRED input to
333
- * `RuntimeSetupOptions`). Undeclared means DEPLOYED, so a forgotten wiring call refuses the endpoint
334
- * rather than exposing it.
335
- */
336
- // webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope
337
- function WpAuthLocalOnly() {
338
- return defineAuthMode({ kind: 'local-only' });
225
+ // webpieces-disable no-function-outside-class -- canonical metadata reader
226
+ function isLocalOnly(apiClass, methodName) {
227
+ return Reflect.getMetadata(exports.METADATA_KEYS.LOCAL_ONLY, apiClass, methodName) === true;
339
228
  }
340
229
  // ============================================================
341
230
  // Helper functions
@@ -452,32 +341,32 @@ function isFormPost(apiClass, methodName) {
452
341
  }
453
342
  /**
454
343
  * True when the method's @Endpoint declared `{ rawBody: true }` — the transport must retain the
455
- * verbatim bytes + absolute url for an {@link WpAuthWebhook} hook to verify.
344
+ * verbatim bytes + absolute url for an {@link WebhookAuthCallback} hook to verify.
456
345
  */
457
346
  // webpieces-disable no-function-outside-class -- reflect-metadata reader, sibling of isFormPost
458
347
  function isRawBody(apiClass, methodName) {
459
348
  return getEndpointOptions(apiClass, methodName).rawBody === true;
460
349
  }
461
350
  /**
462
- * Fail-fast at wiring time when an `@WpAuthWebhook` endpoint did not ask the transport to keep the
351
+ * Fail-fast at wiring time when an `webhook(...)` endpoint did not ask the transport to keep the
463
352
  * bytes it is supposed to verify. A hook with nothing to verify is a MISCONFIGURATION, and it must
464
353
  * surface at startup, naming the fix — not as a 401 in production on exactly the traffic the endpoint
465
354
  * exists for.
466
355
  *
467
356
  * This pairing is a runtime assert rather than a type because the two halves live on DIFFERENT
468
- * decorators (`@WpAuthWebhook` and `@Endpoint`), and no union over one decorator's argument can say
357
+ * decorators (`webhook(...)` and `@Endpoint`), and no union over one decorator's argument can say
469
358
  * anything about the other's.
470
359
  *
471
- * @throws Error naming the first `@WpAuthWebhook` endpoint missing `{ rawBody: true }`.
360
+ * @throws Error naming the first `webhook(...)` endpoint missing `{ rawBody: true }`.
472
361
  */
473
362
  // webpieces-disable no-function-outside-class -- wiring-time assert, sibling of assertEveryEndpointHasAuthMode
474
363
  function assertEveryWebhookEndpointRetainsRawBody(apiClass) {
475
364
  const endpoints = getEndpoints(apiClass) || {};
476
365
  for (const methodName of Object.keys(endpoints)) {
477
- if (getAuthMode(apiClass, methodName)?.kind !== 'webhook' ||
366
+ if (!getAuthMeta(apiClass, methodName)?.methods.some((method) => method.kind === 'webhook') ||
478
367
  isRawBody(apiClass, methodName))
479
368
  continue;
480
- throw new Error(`Endpoint '${methodName}' in ${apiClass.name || 'Unknown'} is @WpAuthWebhook but its @Endpoint ` +
369
+ throw new Error(`Endpoint '${methodName}' in ${apiClass.name || 'Unknown'} is webhook(...) but its @Endpoint ` +
481
370
  `does not declare { rawBody: true }. A webhook hook verifies a signature over the bytes and ` +
482
371
  `the url the SENDER transmitted, and without that option the transport parses the body and ` +
483
372
  `throws them away — leaving the hook nothing to check.`);
@@ -496,14 +385,6 @@ function isApiPath(apiClass) {
496
385
  function getAuthMeta(apiClass, methodName) {
497
386
  return Reflect.getMetadata(exports.METADATA_KEYS.AUTH_META, apiClass, methodName);
498
387
  }
499
- /**
500
- * Get the auth mode for a method, or undefined.
501
- * Convenience wrapper over getAuthMeta for callers that only want the mode.
502
- */
503
- // webpieces-disable no-function-outside-class -- typed convenience reader over getAuthMeta
504
- function getAuthMode(apiClass, methodName) {
505
- return getAuthMeta(apiClass, methodName)?.mode;
506
- }
507
388
  /**
508
389
  * The ONE prescription for "this endpoint declares no auth", shared by the two places that raise it
509
390
  * (here and http-routing's ApiRoutingFactory) because they had drifted into teaching different menus.
@@ -511,11 +392,8 @@ function getAuthMode(apiClass, methodName) {
511
392
  * the caller hits becomes the API they believe exists. It leads with the ROLE-GATED member on purpose —
512
393
  * the first thing offered should not be the widest grant.
513
394
  */
514
- exports.MISSING_AUTH_DECORATOR_FIX = "Add one of @WpAuthJwt({roles: ['admin']}) / @WpAuthJwt({allRolesAllowed: true}) / " +
515
- '@WpAuthOidc(...callers) / @WpAuthSharedSecret(key) / ' +
516
- "@WpAuthWebhook('vendor') / @WpAuthApiKey('regime', [{in: 'header', name: 'x-api-key'}]) / " +
517
- "@WpAuthLocalOnly() / @WpAuthPublic('why anonymous access is required') to " +
518
- 'the method.';
395
+ exports.MISSING_AUTH_DECORATOR_FIX = "Declare @WpAuth([jwt(), oidc(...callers), sharedSecret(key), webhook(name), apiKey('regime', [{in: 'header', name: 'x-api-key'}])]) " +
396
+ "or @WpAuthPublic('reason'), and exactly one @WpAuthorization on every endpoint. Add @WpLocalOnly() separately when locality is required.";
519
397
  /**
520
398
  * Fail-fast at wiring time if any endpoint lacks an auth mode. Both the server
521
399
  * (ApiRoutingFactory) and the task/rpc clients call this so a missing auth
@@ -530,6 +408,20 @@ function assertEveryEndpointHasAuthMode(apiClass) {
530
408
  throw new Error(`Endpoint '${methodName}' in ${apiName} has no auth decorator. ` +
531
409
  exports.MISSING_AUTH_DECORATOR_FIX);
532
410
  }
411
+ const policy = (0, authorization_1.getAuthorization)(apiClass, methodName);
412
+ if (!policy)
413
+ throw new Error(`Endpoint '${methodName}' in ${apiName} requires @WpAuthorization.`);
414
+ authorization_1.AuthorizationDeclaration.validate(policy);
415
+ const auth = getAuthMeta(apiClass, methodName);
416
+ const isPublic = auth.methods.some((method) => method.kind === 'public');
417
+ if (isPublic !== (policy.authType === authorization_1.AuthorizationType.ANONYMOUS)) {
418
+ throw new Error(`Endpoint '${methodName}' must pair public authentication with ANONYMOUS authorization, or protected authentication with protected/CUSTOM authorization.`);
419
+ }
420
+ }
421
+ const policyMethods = Reflect.getMetadata(authorization_1.AUTHORIZATION_METHODS_KEY, apiClass) ?? [];
422
+ for (const methodName of policyMethods) {
423
+ if (!(methodName in endpoints))
424
+ throw new Error(`Orphan @WpAuthorization on ${apiName}.${methodName}; declare @Endpoint.`);
533
425
  }
534
426
  }
535
427
  /**
@@ -547,9 +439,7 @@ function validateNoConflictingDecorators(apiClass, methodName) {
547
439
  ? `method '${methodName}' of ${targetName}`
548
440
  : `class ${targetName}`;
549
441
  throw new Error(`Conflicting auth decorator on ${location}. ` +
550
- `Only one of @WpAuthJwt({...}) / @WpAuthOidc(...) / @WpAuthSharedSecret(...) / ` +
551
- `@WpAuthWebhook(...) / @WpAuthApiKey(...) / @WpAuthLocalOnly() / ` +
552
- `@WpAuthPublic('reason') is allowed per target.`);
442
+ `Exactly one @WpAuth([...]) or @WpAuthPublic('reason') is allowed per endpoint.`);
553
443
  }
554
444
  }
555
445
  //# sourceMappingURL=decorators.js.map