@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.
- 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
|
@@ -1,31 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* COMPILE-TIME assertions for `JwtRoles` — the guarantee that every broken role decision is a compile
|
|
3
|
-
* error rather than a silent over-permissioning. Each `@ts-expect-error` below FAILS THE BUILD (TS2578,
|
|
4
|
-
* "unused '@ts-expect-error' directive") if the line it guards ever starts compiling. Verified by
|
|
5
|
-
* deliberately making a guarded line valid and watching `tsc -p tsconfig.lib.json` go red.
|
|
6
|
-
*
|
|
7
|
-
* WHY THIS IS NOT A `.spec.ts` FILE. It was, and it proved nothing: tsconfig.lib.json EXCLUDES spec
|
|
8
|
-
* files, and vitest transpiles with esbuild, which strips types without checking them. So a
|
|
9
|
-
* `@ts-expect-error` in a spec is inert — the suite passes whether or not the guarded line really
|
|
10
|
-
* errors. Verified the hard way: with one guarded line deliberately made VALID, both `vitest run` and
|
|
11
|
-
* `nx run core-util:ci` stayed green. A type-level guarantee must be asserted in a file the
|
|
12
|
-
* type-checker actually compiles, which is any non-spec file under `src/`.
|
|
13
|
-
*
|
|
14
|
-
* WHY IT CALLS THE FACTORIES INSTEAD OF USING DECORATOR SYNTAX. A decorator factory is a plain
|
|
15
|
-
* function, so a direct call type-checks the argument identically — and the first cut of this file,
|
|
16
|
-
* which used `@ApiPath` + `@Endpoint` on an abstract class, made the architecture scanner classify
|
|
17
|
-
* core-util as exporting an API contract and demand a `role:api-lib` tag. A test fixture must not
|
|
18
|
-
* change what the repo believes about its own architecture.
|
|
19
|
-
*
|
|
20
|
-
* The counterpart RUNTIME behaviour (that `allRolesAllowed: true` reads back as `[]` through
|
|
21
|
-
* `rolesRequired`) is asserted in `__tests__/pubsub-and-auth-decorators.spec.ts`, where runtime
|
|
22
|
-
* assertions belong.
|
|
23
|
-
*/
|
|
1
|
+
/** Type assertions run in the library build because vitest erases types. */
|
|
24
2
|
export declare class AuthJwtCompileAssertions {
|
|
25
|
-
/** The two legitimate spellings must keep compiling; this half is asserted by the ABSENCE of error. */
|
|
26
3
|
legitimate(): void;
|
|
27
|
-
/** Every one of these must be UNWRITABLE. A directive going unused here fails the build. */
|
|
28
4
|
rejected(): void;
|
|
29
|
-
/** Class-level auth readers were deleted; every metadata lookup must name the audited method. */
|
|
30
|
-
readersRequireMethodName(): void;
|
|
31
5
|
}
|
|
@@ -2,59 +2,24 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.AuthJwtCompileAssertions = void 0;
|
|
4
4
|
const decorators_1 = require("./decorators");
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* deliberately making a guarded line valid and watching `tsc -p tsconfig.lib.json` go red.
|
|
10
|
-
*
|
|
11
|
-
* WHY THIS IS NOT A `.spec.ts` FILE. It was, and it proved nothing: tsconfig.lib.json EXCLUDES spec
|
|
12
|
-
* files, and vitest transpiles with esbuild, which strips types without checking them. So a
|
|
13
|
-
* `@ts-expect-error` in a spec is inert — the suite passes whether or not the guarded line really
|
|
14
|
-
* errors. Verified the hard way: with one guarded line deliberately made VALID, both `vitest run` and
|
|
15
|
-
* `nx run core-util:ci` stayed green. A type-level guarantee must be asserted in a file the
|
|
16
|
-
* type-checker actually compiles, which is any non-spec file under `src/`.
|
|
17
|
-
*
|
|
18
|
-
* WHY IT CALLS THE FACTORIES INSTEAD OF USING DECORATOR SYNTAX. A decorator factory is a plain
|
|
19
|
-
* function, so a direct call type-checks the argument identically — and the first cut of this file,
|
|
20
|
-
* which used `@ApiPath` + `@Endpoint` on an abstract class, made the architecture scanner classify
|
|
21
|
-
* core-util as exporting an API contract and demand a `role:api-lib` tag. A test fixture must not
|
|
22
|
-
* change what the repo believes about its own architecture.
|
|
23
|
-
*
|
|
24
|
-
* The counterpart RUNTIME behaviour (that `allRolesAllowed: true` reads back as `[]` through
|
|
25
|
-
* `rolesRequired`) is asserted in `__tests__/pubsub-and-auth-decorators.spec.ts`, where runtime
|
|
26
|
-
* assertions belong.
|
|
27
|
-
*/
|
|
5
|
+
const auth_mode_1 = require("./auth-mode");
|
|
6
|
+
// @ts-expect-error standalone JWT declarations are removed; use WpAuth([jwt()]) plus WpAuthorization
|
|
7
|
+
const decorators_2 = require("./decorators");
|
|
8
|
+
/** Type assertions run in the library build because vitest erases types. */
|
|
28
9
|
class AuthJwtCompileAssertions {
|
|
29
|
-
/** The two legitimate spellings must keep compiling; this half is asserted by the ABSENCE of error. */
|
|
30
10
|
legitimate() {
|
|
31
|
-
void (0, decorators_1.
|
|
32
|
-
void (0, decorators_1.WpAuthJwt)({ roles: ['admin', 'editor'] });
|
|
33
|
-
void (0, decorators_1.WpAuthJwt)({ allRolesAllowed: true });
|
|
34
|
-
void (0, decorators_1.WpAuthJwt)({ allRolesAllowed: true, inOrg: true });
|
|
35
|
-
void (0, decorators_1.WpAuthJwt)({ roles: ['admin'], tenantScoped: true });
|
|
11
|
+
void (0, decorators_1.WpAuth)([(0, auth_mode_1.jwt)()]);
|
|
36
12
|
}
|
|
37
|
-
/** Every one of these must be UNWRITABLE. A directive going unused here fails the build. */
|
|
38
13
|
rejected() {
|
|
39
|
-
// @ts-expect-error
|
|
40
|
-
void (0, decorators_1.
|
|
41
|
-
// @ts-expect-error
|
|
42
|
-
void (0,
|
|
43
|
-
// @ts-expect-error
|
|
44
|
-
void (0, decorators_1.WpAuthJwt)({ allRolesAllowed: true, roles: ['admin'] });
|
|
45
|
-
// @ts-expect-error allRolesAllowed:false is a redundant SECOND spelling of role-gating
|
|
46
|
-
void (0, decorators_1.WpAuthJwt)({ roles: ['admin'], allRolesAllowed: false });
|
|
47
|
-
// @ts-expect-error `false` is never how you say "wide" — the other branch is for that
|
|
48
|
-
void (0, decorators_1.WpAuthJwt)({ allRolesAllowed: false });
|
|
49
|
-
// @ts-expect-error app-defined fields alone authorize nothing; a role decision is mandatory
|
|
50
|
-
void (0, decorators_1.WpAuthJwt)({ inOrg: true });
|
|
51
|
-
}
|
|
52
|
-
/** Class-level auth readers were deleted; every metadata lookup must name the audited method. */
|
|
53
|
-
readersRequireMethodName() {
|
|
54
|
-
// @ts-expect-error methodName is mandatory so removed class-level auth cannot silently compile
|
|
14
|
+
// @ts-expect-error a protected declaration requires at least one credential descriptor
|
|
15
|
+
void (0, decorators_1.WpAuth)([]);
|
|
16
|
+
// @ts-expect-error policy belongs on WpAuthorization
|
|
17
|
+
void (0, auth_mode_1.jwt)({ roles: ['admin'] });
|
|
18
|
+
// @ts-expect-error method metadata cannot be read without the method name
|
|
55
19
|
void (0, decorators_1.getAuthMeta)(AuthJwtCompileAssertions);
|
|
56
|
-
|
|
57
|
-
void
|
|
20
|
+
void decorators_2.WpAuthJwt;
|
|
21
|
+
void decorators_2.getAuthMode;
|
|
22
|
+
void decorators_2.rolesRequired;
|
|
58
23
|
}
|
|
59
24
|
}
|
|
60
25
|
exports.AuthJwtCompileAssertions = AuthJwtCompileAssertions;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WpAuthJwtCompileAssertions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/WpAuthJwtCompileAssertions.ts"],"names":[],"mappings":";;;AAAA,
|
|
1
|
+
{"version":3,"file":"WpAuthJwtCompileAssertions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/WpAuthJwtCompileAssertions.ts"],"names":[],"mappings":";;;AAAA,6CAAmD;AACnD,2CAAkC;AAClC,qGAAqG;AACrG,6CAAqE;AAErE,4EAA4E;AAC5E,MAAa,wBAAwB;IACjC,UAAU;QACN,KAAK,IAAA,mBAAM,EAAC,CAAC,IAAA,eAAG,GAAE,CAAC,CAAC,CAAC;IACzB,CAAC;IAED,QAAQ;QACJ,uFAAuF;QACvF,KAAK,IAAA,mBAAM,EAAC,EAAE,CAAC,CAAC;QAChB,qDAAqD;QACrD,KAAK,IAAA,eAAG,EAAC,EAAE,KAAK,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC/B,0EAA0E;QAC1E,KAAK,IAAA,wBAAW,EAAC,wBAAwB,CAAC,CAAC;QAC3C,KAAK,sBAAS,CAAC;QACf,KAAK,wBAAW,CAAC;QACjB,KAAK,0BAAa,CAAC;IACvB,CAAC;CACJ;AAhBD,4DAgBC","sourcesContent":["import { getAuthMeta, WpAuth } from './decorators';\nimport { jwt } from './auth-mode';\n// @ts-expect-error standalone JWT declarations are removed; use WpAuth([jwt()]) plus WpAuthorization\nimport { WpAuthJwt, getAuthMode, rolesRequired } from './decorators';\n\n/** Type assertions run in the library build because vitest erases types. */\nexport class AuthJwtCompileAssertions {\n legitimate(): void {\n void WpAuth([jwt()]);\n }\n\n rejected(): void {\n // @ts-expect-error a protected declaration requires at least one credential descriptor\n void WpAuth([]);\n // @ts-expect-error policy belongs on WpAuthorization\n void jwt({ roles: ['admin'] });\n // @ts-expect-error method metadata cannot be read without the method name\n void getAuthMeta(AuthJwtCompileAssertions);\n void WpAuthJwt;\n void getAuthMode;\n void rolesRequired;\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const authorization_1 = require("./authorization");
|
|
4
|
+
class OrderPolicy {
|
|
5
|
+
permission;
|
|
6
|
+
}
|
|
7
|
+
(0, authorization_1.WpAuthorization)({ authType: authorization_1.AuthorizationType.ALL_USERS });
|
|
8
|
+
(0, authorization_1.WpAuthorization)({ authType: authorization_1.AuthorizationType.ROLES, roles: ['editor'] });
|
|
9
|
+
(0, authorization_1.WpAuthorization)({ authType: authorization_1.AuthorizationType.CUSTOM, appPolicy: { permission: 'orders:edit' } });
|
|
10
|
+
// @ts-expect-error ROLES requires a nonempty tuple
|
|
11
|
+
(0, authorization_1.WpAuthorization)({ authType: authorization_1.AuthorizationType.ROLES, roles: [] });
|
|
12
|
+
// @ts-expect-error ROLES requires explicit roles
|
|
13
|
+
(0, authorization_1.WpAuthorization)({ authType: authorization_1.AuthorizationType.ROLES });
|
|
14
|
+
// @ts-expect-error CUSTOM requires typed application policy
|
|
15
|
+
(0, authorization_1.WpAuthorization)({ authType: authorization_1.AuthorizationType.CUSTOM });
|
|
16
|
+
// @ts-expect-error standard policies reject application policy
|
|
17
|
+
(0, authorization_1.WpAuthorization)({ authType: authorization_1.AuthorizationType.ALL_USERS, appPolicy: { permission: 'orders:edit' } });
|
|
18
|
+
// @ts-expect-error roles are accepted only by ROLES
|
|
19
|
+
(0, authorization_1.WpAuthorization)({ authType: authorization_1.AuthorizationType.SERVICE_ONLY, roles: ['editor'] });
|
|
20
|
+
// @ts-expect-error arbitrary top-level permissions belong in CUSTOM.appPolicy
|
|
21
|
+
(0, authorization_1.WpAuthorization)({ authType: authorization_1.AuthorizationType.ALL_USERS, permission: 'orders:read' });
|
|
22
|
+
// @ts-expect-error the application payload is checked by its declared type
|
|
23
|
+
(0, authorization_1.WpAuthorization)({ authType: authorization_1.AuthorizationType.CUSTOM, appPolicy: { permission: 'users:edit' } });
|
|
24
|
+
// @ts-expect-error anonymous access requires a reason
|
|
25
|
+
(0, authorization_1.WpAuthorization)({ authType: authorization_1.AuthorizationType.ANONYMOUS });
|
|
26
|
+
//# sourceMappingURL=WpAuthorizationCompileAssertions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WpAuthorizationCompileAssertions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/WpAuthorizationCompileAssertions.ts"],"names":[],"mappings":";;AAAA,mDAAqE;AAErE,MAAM,WAAW;IACb,UAAU,CAAiC;CAC9C;AAED,IAAA,+BAAe,EAAC,EAAE,QAAQ,EAAE,iCAAiB,CAAC,SAAS,EAAE,CAAC,CAAC;AAC3D,IAAA,+BAAe,EAAC,EAAE,QAAQ,EAAE,iCAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AAC1E,IAAA,+BAAe,EAAc,EAAE,QAAQ,EAAE,iCAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,UAAU,EAAE,aAAa,EAAE,EAAE,CAAC,CAAC;AAC/G,mDAAmD;AACnD,IAAA,+BAAe,EAAC,EAAE,QAAQ,EAAE,iCAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;AAClE,iDAAiD;AACjD,IAAA,+BAAe,EAAC,EAAE,QAAQ,EAAE,iCAAiB,CAAC,KAAK,EAAE,CAAC,CAAC;AACvD,4DAA4D;AAC5D,IAAA,+BAAe,EAAc,EAAE,QAAQ,EAAE,iCAAiB,CAAC,MAAM,EAAE,CAAC,CAAC;AACrE,+DAA+D;AAC/D,IAAA,+BAAe,EAAc,EAAE,QAAQ,EAAE,iCAAiB,CAAC,SAAS,EAAE,SAAS,EAAE,EAAE,UAAU,EAAE,aAAa,EAAE,EAAE,CAAC,CAAC;AAClH,oDAAoD;AACpD,IAAA,+BAAe,EAAC,EAAE,QAAQ,EAAE,iCAAiB,CAAC,YAAY,EAAE,KAAK,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AACjF,8EAA8E;AAC9E,IAAA,+BAAe,EAAC,EAAE,QAAQ,EAAE,iCAAiB,CAAC,SAAS,EAAE,UAAU,EAAE,aAAa,EAAE,CAAC,CAAC;AACtF,2EAA2E;AAC3E,IAAA,+BAAe,EAAc,EAAE,QAAQ,EAAE,iCAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,UAAU,EAAE,YAAY,EAAE,EAAE,CAAC,CAAC;AAC9G,sDAAsD;AACtD,IAAA,+BAAe,EAAC,EAAE,QAAQ,EAAE,iCAAiB,CAAC,SAAS,EAAE,CAAC,CAAC","sourcesContent":["import { AuthorizationType, WpAuthorization } from './authorization';\n\nclass OrderPolicy {\n permission!: 'orders:read' | 'orders:edit';\n}\n\nWpAuthorization({ authType: AuthorizationType.ALL_USERS });\nWpAuthorization({ authType: AuthorizationType.ROLES, roles: ['editor'] });\nWpAuthorization<OrderPolicy>({ authType: AuthorizationType.CUSTOM, appPolicy: { permission: 'orders:edit' } });\n// @ts-expect-error ROLES requires a nonempty tuple\nWpAuthorization({ authType: AuthorizationType.ROLES, roles: [] });\n// @ts-expect-error ROLES requires explicit roles\nWpAuthorization({ authType: AuthorizationType.ROLES });\n// @ts-expect-error CUSTOM requires typed application policy\nWpAuthorization<OrderPolicy>({ authType: AuthorizationType.CUSTOM });\n// @ts-expect-error standard policies reject application policy\nWpAuthorization<OrderPolicy>({ authType: AuthorizationType.ALL_USERS, appPolicy: { permission: 'orders:edit' } });\n// @ts-expect-error roles are accepted only by ROLES\nWpAuthorization({ authType: AuthorizationType.SERVICE_ONLY, roles: ['editor'] });\n// @ts-expect-error arbitrary top-level permissions belong in CUSTOM.appPolicy\nWpAuthorization({ authType: AuthorizationType.ALL_USERS, permission: 'orders:read' });\n// @ts-expect-error the application payload is checked by its declared type\nWpAuthorization<OrderPolicy>({ authType: AuthorizationType.CUSTOM, appPolicy: { permission: 'users:edit' } });\n// @ts-expect-error anonymous access requires a reason\nWpAuthorization({ authType: AuthorizationType.ANONYMOUS });\n"]}
|
package/src/http/auth-mode.d.ts
CHANGED
|
@@ -1,79 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The TYPE layer of the auth surface: what an endpoint's credential posture IS, with no decorator and
|
|
3
|
-
* no reflect-metadata. `decorators.ts` (which attaches these) imports FROM here, never the other way,
|
|
4
|
-
* so a reader that only needs to switch on a mode — `DestinationTrust`, `RouteMetadata`, a spec
|
|
5
|
-
* generator — does not drag the whole decorator surface in with it.
|
|
6
|
-
*
|
|
7
|
-
* Split out of `decorators.ts` purely for file size, exactly as `api-kind.ts`, `external-caller.ts` and
|
|
8
|
-
* `RouteMetadata.ts` were before it. Nothing about these types' role changed in the move, and the
|
|
9
|
-
* barrel keeps the package surface identical.
|
|
10
|
-
*/
|
|
11
|
-
/**
|
|
12
|
-
* The role decision for a JWT endpoint, as a union the COMPILER enforces — one spelling per decision,
|
|
13
|
-
* every broken combination a compile error:
|
|
14
|
-
*
|
|
15
|
-
* ```typescript
|
|
16
|
-
* @WpAuthJwt({ roles: ['admin'] }) // ✅ role-gated (any-of)
|
|
17
|
-
* @WpAuthJwt({ allRolesAllowed: true }) // ✅ every authenticated user, said out loud
|
|
18
|
-
* @WpAuthJwt({}) // ❌ pick a branch
|
|
19
|
-
* @WpAuthJwt({ roles: [] }) // ❌ needs at least one role
|
|
20
|
-
* ```
|
|
21
|
-
*
|
|
22
|
-
* `allRolesAllowed` exists ONLY on the wide branch (the dangerous half must be a greppable token, and
|
|
23
|
-
* the narrow branch rejects it as a redundant second spelling); `roles` is a NON-EMPTY tuple so
|
|
24
|
-
* "declared roles, passed none" — the old optional `string[]`'s silent widest grant — cannot be written.
|
|
25
|
-
* All six bad cases are pinned in `AuthJwtCompileAssertions.ts` — a COMPILED file, not a spec: tsc
|
|
26
|
-
* fails the build (TS2578) if any starts compiling. A spec cannot do this (see that file's header).
|
|
27
|
-
*
|
|
28
|
-
* WHY a type rather than the runtime `throw` this replaced: `.claude/review/backwards-compatibility.md`
|
|
29
|
-
* shim shapes #4 and #5. Not restated here — three copies of one rationale is three things to drift.
|
|
30
|
-
*/
|
|
31
|
-
export type JwtRoles = {
|
|
32
|
-
allRolesAllowed: true;
|
|
33
|
-
roles?: never;
|
|
34
|
-
} | {
|
|
35
|
-
roles: readonly [string, ...string[]];
|
|
36
|
-
allRolesAllowed?: never;
|
|
37
|
-
};
|
|
38
|
-
/**
|
|
39
|
-
* JwtRequirement - the {@link JwtRoles} decision PLUS any app-defined authorization fields, e.g.
|
|
40
|
-
* `@WpAuthJwt({ allRolesAllowed: true, inOrg: true })`. The framework authenticates (JwtHook.parseJwt)
|
|
41
|
-
* and enforces the roles any-of; the app overrides JwtHook.authorizeJwt to enforce its own fields.
|
|
42
|
-
* Both hook methods are ASYNC, so an app field like `inOrg` may be answered from a datastore.
|
|
43
|
-
*
|
|
44
|
-
* This was a SECOND decorator (`@Auth`) whose `roles` was optional — so `@Auth({})` reached the exact
|
|
45
|
-
* widest grant that {@link JwtRoles} exists to make un-typeable. Folding it in leaves one decorator per
|
|
46
|
-
* credential kind and closes that route by construction.
|
|
47
|
-
*/
|
|
48
|
-
export type JwtRequirement = JwtRoles & {
|
|
49
|
-
[field: string]: unknown;
|
|
50
|
-
};
|
|
51
|
-
/**
|
|
52
|
-
* WHERE ONE api-key credential rides on the wire, as a union the COMPILER enforces — the DECLARATION
|
|
53
|
-
* a spec generator turns into an OpenAPI `securityScheme`:
|
|
54
|
-
*
|
|
55
|
-
* ```typescript
|
|
56
|
-
* { in: 'header', name: 'x-api-key' } // ✅ → {type: apiKey, in: header, name: x-api-key}
|
|
57
|
-
* { in: 'bearer' } // ✅ → {type: http, scheme: bearer}
|
|
58
|
-
* { in: 'bearer', name: 'x-api-key' } // ❌ bearer's location IS `Authorization`; a name is a lie
|
|
59
|
-
* { in: 'header' } // ❌ a header credential with no header name is unusable
|
|
60
|
-
* ```
|
|
61
|
-
*
|
|
62
|
-
* WHY A UNION AND NOT ONE OPTIONAL `headerName`. The two OpenAPI schemes reachable from "an api key"
|
|
63
|
-
* are structurally DIFFERENT documents (`type: apiKey` + `in` + `name` vs `type: http` + `scheme`).
|
|
64
|
-
* A single optional field makes the contradictory combination REPRESENTABLE, and a generator handed
|
|
65
|
-
* `{in: 'bearer', name: 'x-api-key'}` has to either guess or emit a silently-wrong published contract.
|
|
66
|
-
* `name?: never` on the bearer branch is what deletes that state — the same device {@link JwtRoles}
|
|
67
|
-
* uses, pinned the same way in `AuthApiKeyCompileAssertions.ts` (a COMPILED file, not a spec).
|
|
68
|
-
*
|
|
69
|
-
* THIS IS DESCRIPTION, NOT HANDLING. The framework reads no header from this; `ApiKeyHook` still gets
|
|
70
|
-
* the whole request and still owns the cross-check. Declaring the location cannot regress running auth
|
|
71
|
-
* — it exists so the location stops living ONLY in a hand-written spec fragment that drifts silently
|
|
72
|
-
* from the hook's own header constants.
|
|
73
|
-
*
|
|
74
|
-
* `description` is carried because it is what a docs site renders on its authorization card, and that
|
|
75
|
-
* prose belongs beside the declaration rather than in a hand-maintained JSON file.
|
|
76
|
-
*/
|
|
77
1
|
export type ApiKeyCredential = {
|
|
78
2
|
in: 'header';
|
|
79
3
|
name: string;
|
|
@@ -83,48 +7,13 @@ export type ApiKeyCredential = {
|
|
|
83
7
|
name?: never;
|
|
84
8
|
description?: string;
|
|
85
9
|
};
|
|
86
|
-
/**
|
|
87
|
-
* EVERY credential one api-key regime requires, in the order they appear in the published document.
|
|
88
|
-
*
|
|
89
|
-
* A NON-EMPTY tuple, for the same reason {@link JwtRoles}'s `roles` is one: a regime that declares no
|
|
90
|
-
* credential would generate a document with no security block — the exact silent failure the
|
|
91
|
-
* declaration exists to remove — so `[]` is a COMPILE error rather than a runtime throw (shim shape
|
|
92
|
-
* #4). More than one entry is the normal case, not an exotic one: a real regime authenticates a PAIR,
|
|
93
|
-
* and every entry here must be presented TOGETHER (an AND, i.e. ONE OpenAPI security-requirement
|
|
94
|
-
* object holding every scheme, never a LIST of one-key objects, which would mean "either suffices").
|
|
95
|
-
*/
|
|
96
10
|
export type ApiKeyCredentials = readonly [ApiKeyCredential, ...ApiKeyCredential[]];
|
|
97
|
-
/**
|
|
98
|
-
|
|
99
|
-
* `switch (mode.kind)` and get the data it needs, exhaustively.
|
|
100
|
-
*
|
|
101
|
-
* - `public` → no auth check
|
|
102
|
-
* - `jwt` → user JWT; `requirement` carries the compiler-enforced role decision
|
|
103
|
-
* ({@link JwtRoles}) plus any app-defined authorization fields
|
|
104
|
-
* - `oidc` → Google OIDC service-to-service (Cloud Tasks delivery / cross-service RPC);
|
|
105
|
-
* `callers` is the allow-list of caller SAs ('self' = this service's SA)
|
|
106
|
-
* - `shared-secret` → constant-time compare of a header against the secret bound for `secretKey`
|
|
107
|
-
* - `webhook` → an OUTSIDE vendor signed this request its own way; the app's bound `WebhookAuthCallback`
|
|
108
|
-
* verifies it, selected by `name`. The framework ships NO vendor crypto (see
|
|
109
|
-
* {@link WpAuthWebhook}).
|
|
110
|
-
* - `apikey` → a CUSTOMER holds the credential; the app's bound `ApiKeyHook` looks it up
|
|
111
|
-
* (async, over the whole header set) and returns the context to seed, selected
|
|
112
|
-
* by `regime`. `credentials` DECLARES where the credential rides — an ORDERED,
|
|
113
|
-
* non-empty list, because a real regime authenticates a PAIR (a key AND the
|
|
114
|
-
* organization id it acts for) that must be presented TOGETHER. NOT a peer
|
|
115
|
-
* service — see {@link WpAuthApiKey}.
|
|
116
|
-
* - `local-only` → exists ONLY on a developer's machine; not registered and never served when
|
|
117
|
-
* {@link RuntimeLocality} says this process is deployed. Authenticates NOBODY —
|
|
118
|
-
* it is a deployment gate, not a credential.
|
|
119
|
-
*/
|
|
120
|
-
export type AuthMode = {
|
|
121
|
-
kind: 'public';
|
|
122
|
-
} | {
|
|
11
|
+
/** One concrete mechanism; user policy belongs exclusively to WpAuthorization. */
|
|
12
|
+
export type AuthMethod = {
|
|
123
13
|
kind: 'jwt';
|
|
124
|
-
requirement: JwtRequirement;
|
|
125
14
|
} | {
|
|
126
15
|
kind: 'oidc';
|
|
127
|
-
callers: string[];
|
|
16
|
+
callers: readonly string[];
|
|
128
17
|
} | {
|
|
129
18
|
kind: 'shared-secret';
|
|
130
19
|
secretKey: string;
|
|
@@ -135,22 +24,25 @@ export type AuthMode = {
|
|
|
135
24
|
kind: 'apikey';
|
|
136
25
|
regime: string;
|
|
137
26
|
credentials: ApiKeyCredentials;
|
|
138
|
-
} | {
|
|
139
|
-
kind: 'local-only';
|
|
140
27
|
};
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
* Carries a discriminated {@link AuthMode} and nothing else. It USED to also expose
|
|
147
|
-
* `authenticated`/`roles` getters "for back-compat with readers that only understand the user-JWT
|
|
148
|
-
* model" — deleted, because nothing read them: every reader (AuthFilter, BrowserProxyClient,
|
|
149
|
-
* ProxyClient) switches on `mode.kind`, which is the whole point of the discriminated union. A
|
|
150
|
-
* flattened view of a union is a second spelling of it, and the flattened one silently answers
|
|
151
|
-
* `authenticated: true` for oidc and shared-secret too.
|
|
152
|
-
*/
|
|
28
|
+
export type AuthMethods = readonly [AuthMethod, ...AuthMethod[]];
|
|
29
|
+
export type AuthMode = AuthMethod | {
|
|
30
|
+
kind: 'public';
|
|
31
|
+
};
|
|
32
|
+
/** Alternatives remain unresolved until a concrete mechanism is chosen for this hop. */
|
|
153
33
|
export declare class AuthMeta {
|
|
154
|
-
|
|
155
|
-
|
|
34
|
+
readonly publicReason: string | undefined;
|
|
35
|
+
readonly methods: readonly [AuthMode, ...AuthMode[]];
|
|
36
|
+
constructor(methods: readonly [AuthMode, ...AuthMode[]], publicReason: string | undefined);
|
|
37
|
+
private static snapshot;
|
|
38
|
+
}
|
|
39
|
+
export declare class AuthDeclaration {
|
|
40
|
+
static validate(meta: AuthMeta): void;
|
|
41
|
+
private static validateMethod;
|
|
42
|
+
private static requireText;
|
|
156
43
|
}
|
|
44
|
+
export declare function jwt(): AuthMethod;
|
|
45
|
+
export declare function oidc(...callers: string[]): AuthMethod;
|
|
46
|
+
export declare function sharedSecret(secretKey: string): AuthMethod;
|
|
47
|
+
export declare function webhook(name: string): AuthMethod;
|
|
48
|
+
export declare function apiKey(regime: string, credentials: ApiKeyCredentials): AuthMethod;
|
package/src/http/auth-mode.js
CHANGED
|
@@ -1,33 +1,107 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* The TYPE layer of the auth surface: what an endpoint's credential posture IS, with no decorator and
|
|
4
|
-
* no reflect-metadata. `decorators.ts` (which attaches these) imports FROM here, never the other way,
|
|
5
|
-
* so a reader that only needs to switch on a mode — `DestinationTrust`, `RouteMetadata`, a spec
|
|
6
|
-
* generator — does not drag the whole decorator surface in with it.
|
|
7
|
-
*
|
|
8
|
-
* Split out of `decorators.ts` purely for file size, exactly as `api-kind.ts`, `external-caller.ts` and
|
|
9
|
-
* `RouteMetadata.ts` were before it. Nothing about these types' role changed in the move, and the
|
|
10
|
-
* barrel keeps the package surface identical.
|
|
11
|
-
*/
|
|
12
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
-
exports.AuthMeta = void 0;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* `authenticated`/`roles` getters "for back-compat with readers that only understand the user-JWT
|
|
21
|
-
* model" — deleted, because nothing read them: every reader (AuthFilter, BrowserProxyClient,
|
|
22
|
-
* ProxyClient) switches on `mode.kind`, which is the whole point of the discriminated union. A
|
|
23
|
-
* flattened view of a union is a second spelling of it, and the flattened one silently answers
|
|
24
|
-
* `authenticated: true` for oidc and shared-secret too.
|
|
25
|
-
*/
|
|
3
|
+
exports.AuthDeclaration = exports.AuthMeta = void 0;
|
|
4
|
+
exports.jwt = jwt;
|
|
5
|
+
exports.oidc = oidc;
|
|
6
|
+
exports.sharedSecret = sharedSecret;
|
|
7
|
+
exports.webhook = webhook;
|
|
8
|
+
exports.apiKey = apiKey;
|
|
9
|
+
/** Alternatives remain unresolved until a concrete mechanism is chosen for this hop. */
|
|
26
10
|
class AuthMeta {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
11
|
+
publicReason;
|
|
12
|
+
methods;
|
|
13
|
+
constructor(methods, publicReason) {
|
|
14
|
+
this.publicReason = publicReason;
|
|
15
|
+
this.methods = methods;
|
|
16
|
+
AuthDeclaration.validate(this);
|
|
17
|
+
this.methods = Object.freeze(methods.map((method) => AuthMeta.snapshot(method)));
|
|
18
|
+
Object.freeze(this);
|
|
19
|
+
}
|
|
20
|
+
// webpieces-disable no-function-outside-class -- stateless contract validation or scope metadata reader used before DI registration
|
|
21
|
+
static snapshot(method) {
|
|
22
|
+
switch (method.kind) {
|
|
23
|
+
case 'oidc': return Object.freeze({ ...method, callers: Object.freeze([...method.callers]) });
|
|
24
|
+
case 'apikey': return Object.freeze({ ...method, credentials: Object.freeze(method.credentials.map((credential) => Object.freeze({ ...credential }))) });
|
|
25
|
+
default: return Object.freeze({ ...method });
|
|
26
|
+
}
|
|
30
27
|
}
|
|
31
28
|
}
|
|
32
29
|
exports.AuthMeta = AuthMeta;
|
|
30
|
+
class AuthDeclaration {
|
|
31
|
+
// webpieces-disable no-function-outside-class -- stateless contract validation or scope metadata reader used before DI registration
|
|
32
|
+
static validate(meta) {
|
|
33
|
+
if (!Array.isArray(meta.methods) || meta.methods.length === 0)
|
|
34
|
+
throw new Error('@WpAuth requires a nonempty methods tuple.');
|
|
35
|
+
const kinds = new Set();
|
|
36
|
+
for (const method of meta.methods) {
|
|
37
|
+
if (!method || typeof method !== 'object' || kinds.has(method.kind))
|
|
38
|
+
throw new Error('@WpAuth rejects duplicate or invalid credential methods.');
|
|
39
|
+
kinds.add(method.kind);
|
|
40
|
+
this.validateMethod(method);
|
|
41
|
+
}
|
|
42
|
+
if (kinds.has('public')) {
|
|
43
|
+
if (meta.methods.length !== 1 || typeof meta.publicReason !== 'string' || !meta.publicReason.trim()) {
|
|
44
|
+
throw new Error('Public auth must be declared separately with @WpAuthPublic(reason).');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else if (meta.publicReason !== undefined)
|
|
48
|
+
throw new Error('Protected @WpAuth rejects public reasons.');
|
|
49
|
+
}
|
|
50
|
+
// webpieces-disable no-function-outside-class -- stateless contract validation or scope metadata reader used before DI registration
|
|
51
|
+
static validateMethod(method) {
|
|
52
|
+
let keys;
|
|
53
|
+
switch (method.kind) {
|
|
54
|
+
case 'jwt':
|
|
55
|
+
case 'public':
|
|
56
|
+
keys = ['kind'];
|
|
57
|
+
break;
|
|
58
|
+
case 'oidc':
|
|
59
|
+
keys = ['kind', 'callers'];
|
|
60
|
+
if (!Array.isArray(method.callers) || !method.callers.every((caller) => typeof caller === 'string' && caller.trim().length > 0))
|
|
61
|
+
throw new Error('oidc(...) requires valid caller names.');
|
|
62
|
+
break;
|
|
63
|
+
case 'shared-secret':
|
|
64
|
+
keys = ['kind', 'secretKey'];
|
|
65
|
+
this.requireText(method.secretKey, 'sharedSecret');
|
|
66
|
+
break;
|
|
67
|
+
case 'webhook':
|
|
68
|
+
keys = ['kind', 'name'];
|
|
69
|
+
this.requireText(method.name, 'webhook');
|
|
70
|
+
break;
|
|
71
|
+
case 'apikey':
|
|
72
|
+
keys = ['kind', 'regime', 'credentials'];
|
|
73
|
+
this.requireText(method.regime, 'apiKey');
|
|
74
|
+
if (!Array.isArray(method.credentials) || !method.credentials.length)
|
|
75
|
+
throw new Error('apiKey(...) requires a nonempty credentials tuple.');
|
|
76
|
+
for (const credential of method.credentials) {
|
|
77
|
+
if (credential.in === 'header')
|
|
78
|
+
this.requireText(credential.name, 'apiKey header');
|
|
79
|
+
else if (credential.in !== 'bearer' || Object.hasOwn(credential, 'name'))
|
|
80
|
+
throw new Error('Invalid API-key credential location.');
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
default: throw new Error('Unknown @WpAuth method; use jwt/oidc/sharedSecret/webhook/apiKey descriptors.');
|
|
84
|
+
}
|
|
85
|
+
for (const key of Object.keys(method)) {
|
|
86
|
+
if (!keys.includes(key))
|
|
87
|
+
throw new Error(`Auth method rejects '${key}'; operation policy belongs in @WpAuthorization.`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// webpieces-disable no-function-outside-class -- stateless contract validation or scope metadata reader used before DI registration
|
|
91
|
+
static requireText(value, label) {
|
|
92
|
+
if (typeof value !== 'string' || !value.trim())
|
|
93
|
+
throw new Error(`${label} requires a nonempty name.`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
exports.AuthDeclaration = AuthDeclaration;
|
|
97
|
+
// webpieces-disable no-function-outside-class -- canonical credential descriptor factory
|
|
98
|
+
function jwt() { return { kind: 'jwt' }; }
|
|
99
|
+
// webpieces-disable no-function-outside-class -- canonical credential descriptor factory
|
|
100
|
+
function oidc(...callers) { return { kind: 'oidc', callers }; }
|
|
101
|
+
// webpieces-disable no-function-outside-class -- canonical credential descriptor factory
|
|
102
|
+
function sharedSecret(secretKey) { return { kind: 'shared-secret', secretKey }; }
|
|
103
|
+
// webpieces-disable no-function-outside-class -- canonical credential descriptor factory
|
|
104
|
+
function webhook(name) { return { kind: 'webhook', name }; }
|
|
105
|
+
// webpieces-disable no-function-outside-class -- canonical credential descriptor factory
|
|
106
|
+
function apiKey(regime, credentials) { return { kind: 'apikey', regime, credentials }; }
|
|
33
107
|
//# sourceMappingURL=auth-mode.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-mode.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/auth-mode.ts"],"names":[],"mappings":";AAAA;;;;;;;;;GASG;;;AAiHH;;;;;;;;;;;GAWG;AACH,MAAa,QAAQ;IACjB,IAAI,CAAW;IAEf,YAAY,IAAc;QACtB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACrB,CAAC;CACJ;AAND,4BAMC","sourcesContent":["/**\n * The TYPE layer of the auth surface: what an endpoint's credential posture IS, with no decorator and\n * no reflect-metadata. `decorators.ts` (which attaches these) imports FROM here, never the other way,\n * so a reader that only needs to switch on a mode — `DestinationTrust`, `RouteMetadata`, a spec\n * generator — does not drag the whole decorator surface in with it.\n *\n * Split out of `decorators.ts` purely for file size, exactly as `api-kind.ts`, `external-caller.ts` and\n * `RouteMetadata.ts` were before it. Nothing about these types' role changed in the move, and the\n * barrel keeps the package surface identical.\n */\n\n/**\n * The role decision for a JWT endpoint, as a union the COMPILER enforces — one spelling per decision,\n * every broken combination a compile error:\n *\n * ```typescript\n * @WpAuthJwt({ roles: ['admin'] }) // ✅ role-gated (any-of)\n * @WpAuthJwt({ allRolesAllowed: true }) // ✅ every authenticated user, said out loud\n * @WpAuthJwt({}) // ❌ pick a branch\n * @WpAuthJwt({ roles: [] }) // ❌ needs at least one role\n * ```\n *\n * `allRolesAllowed` exists ONLY on the wide branch (the dangerous half must be a greppable token, and\n * the narrow branch rejects it as a redundant second spelling); `roles` is a NON-EMPTY tuple so\n * \"declared roles, passed none\" — the old optional `string[]`'s silent widest grant — cannot be written.\n * All six bad cases are pinned in `AuthJwtCompileAssertions.ts` — a COMPILED file, not a spec: tsc\n * fails the build (TS2578) if any starts compiling. A spec cannot do this (see that file's header).\n *\n * WHY a type rather than the runtime `throw` this replaced: `.claude/review/backwards-compatibility.md`\n * shim shapes #4 and #5. Not restated here — three copies of one rationale is three things to drift.\n */\nexport type JwtRoles =\n | { allRolesAllowed: true; roles?: never }\n | { roles: readonly [string, ...string[]]; allRolesAllowed?: never };\n\n/**\n * JwtRequirement - the {@link JwtRoles} decision PLUS any app-defined authorization fields, e.g.\n * `@WpAuthJwt({ allRolesAllowed: true, inOrg: true })`. The framework authenticates (JwtHook.parseJwt)\n * and enforces the roles any-of; the app overrides JwtHook.authorizeJwt to enforce its own fields.\n * Both hook methods are ASYNC, so an app field like `inOrg` may be answered from a datastore.\n *\n * This was a SECOND decorator (`@Auth`) whose `roles` was optional — so `@Auth({})` reached the exact\n * widest grant that {@link JwtRoles} exists to make un-typeable. Folding it in leaves one decorator per\n * credential kind and closes that route by construction.\n */\n// webpieces-disable no-any-unknown -- app-defined authorization fields (inOrg, tenant, ...)\nexport type JwtRequirement = JwtRoles & { [field: string]: unknown };\n\n/**\n * WHERE ONE api-key credential rides on the wire, as a union the COMPILER enforces — the DECLARATION\n * a spec generator turns into an OpenAPI `securityScheme`:\n *\n * ```typescript\n * { in: 'header', name: 'x-api-key' } // ✅ → {type: apiKey, in: header, name: x-api-key}\n * { in: 'bearer' } // ✅ → {type: http, scheme: bearer}\n * { in: 'bearer', name: 'x-api-key' } // ❌ bearer's location IS `Authorization`; a name is a lie\n * { in: 'header' } // ❌ a header credential with no header name is unusable\n * ```\n *\n * WHY A UNION AND NOT ONE OPTIONAL `headerName`. The two OpenAPI schemes reachable from \"an api key\"\n * are structurally DIFFERENT documents (`type: apiKey` + `in` + `name` vs `type: http` + `scheme`).\n * A single optional field makes the contradictory combination REPRESENTABLE, and a generator handed\n * `{in: 'bearer', name: 'x-api-key'}` has to either guess or emit a silently-wrong published contract.\n * `name?: never` on the bearer branch is what deletes that state — the same device {@link JwtRoles}\n * uses, pinned the same way in `AuthApiKeyCompileAssertions.ts` (a COMPILED file, not a spec).\n *\n * THIS IS DESCRIPTION, NOT HANDLING. The framework reads no header from this; `ApiKeyHook` still gets\n * the whole request and still owns the cross-check. Declaring the location cannot regress running auth\n * — it exists so the location stops living ONLY in a hand-written spec fragment that drifts silently\n * from the hook's own header constants.\n *\n * `description` is carried because it is what a docs site renders on its authorization card, and that\n * prose belongs beside the declaration rather than in a hand-maintained JSON file.\n */\nexport type ApiKeyCredential =\n | { in: 'header'; name: string; description?: string }\n | { in: 'bearer'; name?: never; description?: string };\n\n/**\n * EVERY credential one api-key regime requires, in the order they appear in the published document.\n *\n * A NON-EMPTY tuple, for the same reason {@link JwtRoles}'s `roles` is one: a regime that declares no\n * credential would generate a document with no security block — the exact silent failure the\n * declaration exists to remove — so `[]` is a COMPILE error rather than a runtime throw (shim shape\n * #4). More than one entry is the normal case, not an exotic one: a real regime authenticates a PAIR,\n * and every entry here must be presented TOGETHER (an AND, i.e. ONE OpenAPI security-requirement\n * object holding every scheme, never a LIST of one-key objects, which would mean \"either suffices\").\n */\nexport type ApiKeyCredentials = readonly [ApiKeyCredential, ...ApiKeyCredential[]];\n\n/**\n * The service-to-service / user auth mode of an endpoint. Discriminated union so a filter can\n * `switch (mode.kind)` and get the data it needs, exhaustively.\n *\n * - `public` → no auth check\n * - `jwt` → user JWT; `requirement` carries the compiler-enforced role decision\n * ({@link JwtRoles}) plus any app-defined authorization fields\n * - `oidc` → Google OIDC service-to-service (Cloud Tasks delivery / cross-service RPC);\n * `callers` is the allow-list of caller SAs ('self' = this service's SA)\n * - `shared-secret` → constant-time compare of a header against the secret bound for `secretKey`\n * - `webhook` → an OUTSIDE vendor signed this request its own way; the app's bound `WebhookAuthCallback`\n * verifies it, selected by `name`. The framework ships NO vendor crypto (see\n * {@link WpAuthWebhook}).\n * - `apikey` → a CUSTOMER holds the credential; the app's bound `ApiKeyHook` looks it up\n * (async, over the whole header set) and returns the context to seed, selected\n * by `regime`. `credentials` DECLARES where the credential rides — an ORDERED,\n * non-empty list, because a real regime authenticates a PAIR (a key AND the\n * organization id it acts for) that must be presented TOGETHER. NOT a peer\n * service — see {@link WpAuthApiKey}.\n * - `local-only` → exists ONLY on a developer's machine; not registered and never served when\n * {@link RuntimeLocality} says this process is deployed. Authenticates NOBODY —\n * it is a deployment gate, not a credential.\n */\nexport type AuthMode =\n | { kind: 'public' }\n | { kind: 'jwt'; requirement: JwtRequirement }\n | { kind: 'oidc'; callers: string[] }\n | { kind: 'shared-secret'; secretKey: string }\n | { kind: 'webhook'; name: string }\n | { kind: 'apikey'; regime: string; credentials: ApiKeyCredentials }\n | { kind: 'local-only' };\n\n/**\n * Auth metadata attached to a class or method via one of the auth decorators\n * (@WpAuthPublic / @WpAuthJwt / @WpAuthOidc / @WpAuthSharedSecret / @WpAuthWebhook / @WpAuthApiKey / @WpAuthLocalOnly) —\n * one per credential kind.\n *\n * Carries a discriminated {@link AuthMode} and nothing else. It USED to also expose\n * `authenticated`/`roles` getters \"for back-compat with readers that only understand the user-JWT\n * model\" — deleted, because nothing read them: every reader (AuthFilter, BrowserProxyClient,\n * ProxyClient) switches on `mode.kind`, which is the whole point of the discriminated union. A\n * flattened view of a union is a second spelling of it, and the flattened one silently answers\n * `authenticated: true` for oidc and shared-secret too.\n */\nexport class AuthMeta {\n mode: AuthMode;\n\n constructor(mode: AuthMode) {\n this.mode = mode;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"auth-mode.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/auth-mode.ts"],"names":[],"mappings":"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","sourcesContent":["export type ApiKeyCredential =\n | { in: 'header'; name: string; description?: string }\n | { in: 'bearer'; name?: never; description?: string };\n\nexport type ApiKeyCredentials = readonly [ApiKeyCredential, ...ApiKeyCredential[]];\n\n/** One concrete mechanism; user policy belongs exclusively to WpAuthorization. */\nexport type AuthMethod =\n | { kind: 'jwt' }\n | { kind: 'oidc'; callers: readonly string[] }\n | { kind: 'shared-secret'; secretKey: string }\n | { kind: 'webhook'; name: string }\n | { kind: 'apikey'; regime: string; credentials: ApiKeyCredentials };\n\nexport type AuthMethods = readonly [AuthMethod, ...AuthMethod[]];\nexport type AuthMode = AuthMethod | { kind: 'public' };\n\n/** Alternatives remain unresolved until a concrete mechanism is chosen for this hop. */\nexport class AuthMeta {\n readonly methods: readonly [AuthMode, ...AuthMode[]];\n\n constructor(\n methods: readonly [AuthMode, ...AuthMode[]],\n public readonly publicReason: string | undefined,\n ) {\n this.methods = methods;\n AuthDeclaration.validate(this);\n this.methods = Object.freeze(methods.map((method: AuthMode) => AuthMeta.snapshot(method))) as readonly [AuthMode, ...AuthMode[]];\n Object.freeze(this);\n }\n\n // webpieces-disable no-function-outside-class -- stateless contract validation or scope metadata reader used before DI registration\n private static snapshot(method: AuthMode): AuthMode {\n switch (method.kind) {\n case 'oidc': return Object.freeze({ ...method, callers: Object.freeze([...method.callers]) });\n case 'apikey': return Object.freeze({ ...method, credentials: Object.freeze(method.credentials.map((credential: ApiKeyCredential) => Object.freeze({ ...credential }))) as ApiKeyCredentials });\n default: return Object.freeze({ ...method });\n }\n }\n}\n\nexport class AuthDeclaration {\n // webpieces-disable no-function-outside-class -- stateless contract validation or scope metadata reader used before DI registration\n static validate(meta: AuthMeta): void {\n if (!Array.isArray(meta.methods) || meta.methods.length === 0) throw new Error('@WpAuth requires a nonempty methods tuple.');\n const kinds = new Set<string>();\n for (const method of meta.methods) {\n if (!method || typeof method !== 'object' || kinds.has(method.kind)) throw new Error('@WpAuth rejects duplicate or invalid credential methods.');\n kinds.add(method.kind);\n this.validateMethod(method);\n }\n if (kinds.has('public')) {\n if (meta.methods.length !== 1 || typeof meta.publicReason !== 'string' || !meta.publicReason.trim()) {\n throw new Error('Public auth must be declared separately with @WpAuthPublic(reason).');\n }\n } else if (meta.publicReason !== undefined) throw new Error('Protected @WpAuth rejects public reasons.');\n }\n\n // webpieces-disable no-function-outside-class -- stateless contract validation or scope metadata reader used before DI registration\n private static validateMethod(method: AuthMode): void {\n let keys: readonly string[];\n switch (method.kind) {\n case 'jwt':\n case 'public': keys = ['kind']; break;\n case 'oidc':\n keys = ['kind', 'callers'];\n if (!Array.isArray(method.callers) || !method.callers.every((caller: string) => typeof caller === 'string' && caller.trim().length > 0)) throw new Error('oidc(...) requires valid caller names.');\n break;\n case 'shared-secret':\n keys = ['kind', 'secretKey'];\n this.requireText(method.secretKey, 'sharedSecret'); break;\n case 'webhook':\n keys = ['kind', 'name'];\n this.requireText(method.name, 'webhook'); break;\n case 'apikey':\n keys = ['kind', 'regime', 'credentials'];\n this.requireText(method.regime, 'apiKey');\n if (!Array.isArray(method.credentials) || !method.credentials.length) throw new Error('apiKey(...) requires a nonempty credentials tuple.');\n for (const credential of method.credentials) {\n if (credential.in === 'header') this.requireText(credential.name, 'apiKey header');\n else if (credential.in !== 'bearer' || Object.hasOwn(credential, 'name')) throw new Error('Invalid API-key credential location.');\n }\n break;\n default: throw new Error('Unknown @WpAuth method; use jwt/oidc/sharedSecret/webhook/apiKey descriptors.');\n }\n for (const key of Object.keys(method)) {\n if (!keys.includes(key)) throw new Error(`Auth method rejects '${key}'; operation policy belongs in @WpAuthorization.`);\n }\n }\n\n // webpieces-disable no-function-outside-class -- stateless contract validation or scope metadata reader used before DI registration\n private static requireText(value: string, label: string): void {\n if (typeof value !== 'string' || !value.trim()) throw new Error(`${label} requires a nonempty name.`);\n }\n}\n\n// webpieces-disable no-function-outside-class -- canonical credential descriptor factory\nexport function jwt(): AuthMethod { return { kind: 'jwt' }; }\n// webpieces-disable no-function-outside-class -- canonical credential descriptor factory\nexport function oidc(...callers: string[]): AuthMethod { return { kind: 'oidc', callers }; }\n// webpieces-disable no-function-outside-class -- canonical credential descriptor factory\nexport function sharedSecret(secretKey: string): AuthMethod { return { kind: 'shared-secret', secretKey }; }\n// webpieces-disable no-function-outside-class -- canonical credential descriptor factory\nexport function webhook(name: string): AuthMethod { return { kind: 'webhook', name }; }\n// webpieces-disable no-function-outside-class -- canonical credential descriptor factory\nexport function apiKey(regime: string, credentials: ApiKeyCredentials): AuthMethod { return { kind: 'apikey', regime, credentials }; }\n"]}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
export declare enum AuthorizationType {
|
|
3
|
+
ALL_USERS = "all-users",
|
|
4
|
+
ROLES = "roles",
|
|
5
|
+
SERVICE_ONLY = "service-only",
|
|
6
|
+
USERS_OR_SERVICES = "users-or-services",
|
|
7
|
+
ANONYMOUS = "anonymous",
|
|
8
|
+
CUSTOM = "custom"
|
|
9
|
+
}
|
|
10
|
+
export type AuthorizationRequirement<TPolicy = never> = {
|
|
11
|
+
authType: AuthorizationType.ALL_USERS | AuthorizationType.SERVICE_ONLY | AuthorizationType.USERS_OR_SERVICES;
|
|
12
|
+
roles?: never;
|
|
13
|
+
reason?: never;
|
|
14
|
+
appPolicy?: never;
|
|
15
|
+
} | {
|
|
16
|
+
authType: AuthorizationType.ROLES;
|
|
17
|
+
roles: readonly [string, ...string[]];
|
|
18
|
+
reason?: never;
|
|
19
|
+
appPolicy?: never;
|
|
20
|
+
} | {
|
|
21
|
+
authType: AuthorizationType.ANONYMOUS;
|
|
22
|
+
reason: string;
|
|
23
|
+
roles?: never;
|
|
24
|
+
appPolicy?: never;
|
|
25
|
+
} | {
|
|
26
|
+
authType: AuthorizationType.CUSTOM;
|
|
27
|
+
appPolicy: TPolicy;
|
|
28
|
+
roles?: never;
|
|
29
|
+
reason?: never;
|
|
30
|
+
};
|
|
31
|
+
export declare const AUTHORIZATION_METADATA_KEY = "webpieces:authorization";
|
|
32
|
+
export declare const AUTHORIZATION_METHODS_KEY = "webpieces:authorization-methods";
|
|
33
|
+
/** Runtime checks also protect JavaScript and type-erased callers. */
|
|
34
|
+
export declare class AuthorizationDeclaration {
|
|
35
|
+
static validate(value: AuthorizationRequirement<unknown>): void;
|
|
36
|
+
}
|
|
37
|
+
export declare function WpAuthorization<TPolicy = never>(policy: AuthorizationRequirement<TPolicy>): MethodDecorator;
|
|
38
|
+
export declare function getAuthorization(apiClass: Function, methodName: string): AuthorizationRequirement<unknown> | undefined;
|