@webpieces/core-util 0.4.863 → 0.4.864
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/package.json +1 -1
- package/src/ContextKey.d.ts +3 -3
- package/src/ContextKey.js +3 -3
- package/src/ContextKey.js.map +1 -1
- package/src/http/ContextMgr.d.ts +3 -3
- package/src/http/ContextMgr.js +3 -3
- package/src/http/ContextMgr.js.map +1 -1
- package/src/http/DeletedAuthCompileAssertions.d.ts +4 -0
- package/src/http/DeletedAuthCompileAssertions.js +30 -0
- package/src/http/DeletedAuthCompileAssertions.js.map +1 -0
- package/src/http/DestinationTrust.d.ts +6 -6
- package/src/http/DestinationTrust.js +12 -13
- package/src/http/DestinationTrust.js.map +1 -1
- package/src/http/HttpEndpointOptions.d.ts +2 -2
- package/src/http/HttpEndpointOptions.js.map +1 -1
- package/src/http/RouteMetadata.d.ts +6 -1
- package/src/http/RouteMetadata.js +14 -1
- package/src/http/RouteMetadata.js.map +1 -1
- package/src/http/RouteMetadataFactory.js +9 -3
- package/src/http/RouteMetadataFactory.js.map +1 -1
- package/src/http/RuntimeLocality.d.ts +3 -3
- package/src/http/RuntimeLocality.js +1 -1
- package/src/http/RuntimeLocality.js.map +1 -1
- package/src/http/Secrets.d.ts +2 -2
- package/src/http/Secrets.js +2 -2
- package/src/http/Secrets.js.map +1 -1
- package/src/http/Surface.d.ts +4 -4
- package/src/http/Surface.js.map +1 -1
- package/src/http/WebpiecesCoreHeaders.d.ts +3 -3
- package/src/http/WebpiecesCoreHeaders.js +3 -3
- package/src/http/WebpiecesCoreHeaders.js.map +1 -1
- package/src/http/WpAuthApiKeyCompileAssertions.d.ts +4 -8
- package/src/http/WpAuthApiKeyCompileAssertions.js +8 -13
- package/src/http/WpAuthApiKeyCompileAssertions.js.map +1 -1
- package/src/http/WpAuthJwtCompileAssertions.d.ts +1 -27
- package/src/http/WpAuthJwtCompileAssertions.js +13 -48
- package/src/http/WpAuthJwtCompileAssertions.js.map +1 -1
- package/src/http/WpAuthorizationCompileAssertions.d.ts +1 -0
- package/src/http/WpAuthorizationCompileAssertions.js +26 -0
- package/src/http/WpAuthorizationCompileAssertions.js.map +1 -0
- package/src/http/auth-mode.d.ts +22 -130
- package/src/http/auth-mode.js +100 -26
- package/src/http/auth-mode.js.map +1 -1
- package/src/http/authorization.d.ts +38 -0
- package/src/http/authorization.js +71 -0
- package/src/http/authorization.js.map +1 -0
- package/src/http/decorators.d.ts +11 -114
- package/src/http/decorators.js +47 -157
- package/src/http/decorators.js.map +1 -1
- package/src/index.d.ts +5 -4
- package/src/index.js +21 -19
- package/src/index.js.map +1 -1
- package/src/mcp/McpAuthMetadata.d.ts +0 -9
- package/src/mcp/McpAuthMetadata.js +0 -31
- 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"]}
|
package/src/http/decorators.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
2
|
import { MaskSpec, MaskMode } from './LogFieldMask';
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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.
|
package/src/http/decorators.js
CHANGED
|
@@ -6,13 +6,9 @@ exports.Endpoint = Endpoint;
|
|
|
6
6
|
exports.MaskLog = MaskLog;
|
|
7
7
|
exports.getMaskSpec = getMaskSpec;
|
|
8
8
|
exports.WpAuthPublic = WpAuthPublic;
|
|
9
|
-
exports.
|
|
10
|
-
exports.
|
|
11
|
-
exports.
|
|
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
|
-
|
|
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
|
|
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
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
301
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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 (
|
|
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
|
|
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 = "
|
|
515
|
-
|
|
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
|
-
`
|
|
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
|