@webpieces/core-util 0.4.607 → 0.4.608
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/http/DestinationTrust.d.ts +2 -2
- package/src/http/DestinationTrust.js +7 -2
- package/src/http/DestinationTrust.js.map +1 -1
- package/src/http/RuntimeLocality.d.ts +70 -0
- package/src/http/RuntimeLocality.js +75 -0
- package/src/http/RuntimeLocality.js.map +1 -0
- package/src/http/api-kind.d.ts +65 -0
- package/src/http/api-kind.js +120 -0
- package/src/http/api-kind.js.map +1 -0
- package/src/http/decorators.d.ts +34 -56
- package/src/http/decorators.js +37 -113
- package/src/http/decorators.js.map +1 -1
- package/src/index.d.ts +6 -2
- package/src/index.js +19 -12
- package/src/index.js.map +1 -1
package/package.json
CHANGED
|
@@ -38,8 +38,8 @@ export declare class DestinationTrust {
|
|
|
38
38
|
*/
|
|
39
39
|
private static readonly VERIFIES_CALLER;
|
|
40
40
|
/**
|
|
41
|
-
* The destination cannot tell us from a browser with curl (@AuthJwt / @Public /
|
|
42
|
-
* no declared mode), so trusted keys are omitted. Untrusted keys still travel.
|
|
41
|
+
* The destination cannot tell us from a browser with curl (@AuthJwt / @Public / @AuthLocalOnly /
|
|
42
|
+
* an endpoint with no declared mode), so trusted keys are omitted. Untrusted keys still travel.
|
|
43
43
|
*/
|
|
44
44
|
private static readonly CANNOT_VERIFY_CALLER;
|
|
45
45
|
private constructor();
|
|
@@ -39,8 +39,8 @@ class DestinationTrust {
|
|
|
39
39
|
*/
|
|
40
40
|
static VERIFIES_CALLER = new DestinationTrust(true);
|
|
41
41
|
/**
|
|
42
|
-
* The destination cannot tell us from a browser with curl (@AuthJwt / @Public /
|
|
43
|
-
* no declared mode), so trusted keys are omitted. Untrusted keys still travel.
|
|
42
|
+
* The destination cannot tell us from a browser with curl (@AuthJwt / @Public / @AuthLocalOnly /
|
|
43
|
+
* an endpoint with no declared mode), so trusted keys are omitted. Untrusted keys still travel.
|
|
44
44
|
*/
|
|
45
45
|
static CANNOT_VERIFY_CALLER = new DestinationTrust(false);
|
|
46
46
|
constructor(verifiesCaller) {
|
|
@@ -62,6 +62,11 @@ class DestinationTrust {
|
|
|
62
62
|
return DestinationTrust.VERIFIES_CALLER;
|
|
63
63
|
case 'jwt':
|
|
64
64
|
case 'public':
|
|
65
|
+
// @AuthLocalOnly authenticates NOBODY — it gates on the environment, not on a
|
|
66
|
+
// credential — so a browser with curl on the same laptop is indistinguishable from us.
|
|
67
|
+
// Same bucket as public/jwt. (This switch has NO `default` on purpose: adding a kind to
|
|
68
|
+
// AuthMode is a compile error here rather than a silent permissive fallthrough.)
|
|
69
|
+
case 'local-only':
|
|
65
70
|
return DestinationTrust.CANNOT_VERIFY_CALLER;
|
|
66
71
|
}
|
|
67
72
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DestinationTrust.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/DestinationTrust.ts"],"names":[],"mappings":";;;AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAa,gBAAgB;IAcY;IAbrC;;;;OAIG;IACK,MAAM,CAAU,eAAe,GAAG,IAAI,gBAAgB,CAAC,IAAI,CAAC,CAAC;IAErE;;;OAGG;IACK,MAAM,CAAU,oBAAoB,GAAG,IAAI,gBAAgB,CAAC,KAAK,CAAC,CAAC;IAE3E,YAAqC,cAAuB;QAAvB,mBAAc,GAAd,cAAc,CAAS;IAAG,CAAC;IAEhE;;;;OAIG;IACH,qSAAqS;IACrS,MAAM,CAAC,WAAW,CAAC,IAA0B;QACzC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACrB,OAAO,gBAAgB,CAAC,oBAAoB,CAAC;QACjD,CAAC;QACD,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;YAChB,KAAK,MAAM,CAAC;YACZ,KAAK,eAAe;gBAChB,OAAO,gBAAgB,CAAC,eAAe,CAAC;YAC5C,KAAK,KAAK,CAAC;YACX,KAAK,QAAQ;
|
|
1
|
+
{"version":3,"file":"DestinationTrust.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/DestinationTrust.ts"],"names":[],"mappings":";;;AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAa,gBAAgB;IAcY;IAbrC;;;;OAIG;IACK,MAAM,CAAU,eAAe,GAAG,IAAI,gBAAgB,CAAC,IAAI,CAAC,CAAC;IAErE;;;OAGG;IACK,MAAM,CAAU,oBAAoB,GAAG,IAAI,gBAAgB,CAAC,KAAK,CAAC,CAAC;IAE3E,YAAqC,cAAuB;QAAvB,mBAAc,GAAd,cAAc,CAAS;IAAG,CAAC;IAEhE;;;;OAIG;IACH,qSAAqS;IACrS,MAAM,CAAC,WAAW,CAAC,IAA0B;QACzC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACrB,OAAO,gBAAgB,CAAC,oBAAoB,CAAC;QACjD,CAAC;QACD,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;YAChB,KAAK,MAAM,CAAC;YACZ,KAAK,eAAe;gBAChB,OAAO,gBAAgB,CAAC,eAAe,CAAC;YAC5C,KAAK,KAAK,CAAC;YACX,KAAK,QAAQ,CAAC;YACd,8EAA8E;YAC9E,uFAAuF;YACvF,wFAAwF;YACxF,iFAAiF;YACjF,KAAK,YAAY;gBACb,OAAO,gBAAgB,CAAC,oBAAoB,CAAC;QACrD,CAAC;IACL,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAkB;QACrB,OAAO,IAAI,CAAC,cAAc,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC;IACnD,CAAC;;AAhDL,4CAiDC","sourcesContent":["import { AnyContextKey } from '../ContextKey';\nimport { AuthMode } from './decorators';\n\n/**\n * DestinationTrust - the OUTBOUND half of the trust model: may a TRUSTED context key\n * ({@link ContextKey.trusted}) ride to the endpoint we are about to call?\n *\n * ## Why the client has to answer this at all\n *\n * The server already decided (see `PendingWireTrust`): an inbound `x-user-id` is admitted only on a\n * route that verified WHO called it — `@AuthOidc` / `@AuthSharedSecret`. On a `@AuthJwt` or `@Public`\n * route the same header must match what the authenticator independently derived, or the request is\n * REJECTED with a 401.\n *\n * That rule is correct, and it means a client that ships `x-user-id` to a `@Public` endpoint is\n * building a request the callee is obliged to reject. Before this class the outbound builders\n * forwarded EVERY transferred key with no idea what the destination was, so an internal service\n * calling another service's public or JWT endpoint 401'd itself. The fix belongs on the producing\n * side: don't send what cannot possibly be believed.\n *\n * ## Why it is a class with a private constructor and no boolean parameter\n *\n * `buildOutboundHeaders(sendTrusted = true)` would have been three characters of work and exactly the\n * \"widening that is an ABSENCE rather than a token\" CLAUDE.md rejects — the permissive answer would be\n * what you get by not typing anything. There is no way to build a DestinationTrust except from the\n * destination endpoint's own {@link AuthMode}, so the caller cannot assert a posture the route does\n * not actually have, and `grep -rn DestinationTrust.forAuthMode` lists every place the question is\n * asked. The two instances are PRIVATE for the same reason: exposing them would be a second spelling\n * that skips the derivation.\n *\n * Per CLAUDE.md: data-only structure, so a class rather than an interface or a bare boolean.\n */\nexport class DestinationTrust {\n /**\n * The destination authenticates its CALLER (@AuthOidc / @AuthSharedSecret), so it is entitled to\n * believe context WE vouch for — this is the service-to-service identity propagation that trusted\n * keys keep an `httpHeader` for.\n */\n private static readonly VERIFIES_CALLER = new DestinationTrust(true);\n\n /**\n * The destination cannot tell us from a browser with curl (@AuthJwt / @Public / @AuthLocalOnly /\n * an endpoint with no declared mode), so trusted keys are omitted. Untrusted keys still travel.\n */\n private static readonly CANNOT_VERIFY_CALLER = new DestinationTrust(false);\n\n private constructor(private readonly verifiesCaller: boolean) {}\n\n /**\n * The ONLY way to obtain one: state the destination endpoint's auth mode. `undefined` (an\n * endpoint that declared no mode) is treated as un-verifying, i.e. the SAFE answer — an absent\n * declaration must never be the widest one.\n */\n // webpieces-disable no-function-outside-class -- static factory replacing the (now private) constructor, exactly as ContextKey.trusted/untrusted do: the destination's auth mode must be part of the CALL, and a DI-injected instance method would let a caller hold one without ever naming a route\n static forAuthMode(mode: AuthMode | undefined): DestinationTrust {\n if (mode === undefined) {\n return DestinationTrust.CANNOT_VERIFY_CALLER;\n }\n switch (mode.kind) {\n case 'oidc':\n case 'shared-secret':\n return DestinationTrust.VERIFIES_CALLER;\n case 'jwt':\n case 'public':\n // @AuthLocalOnly authenticates NOBODY — it gates on the environment, not on a\n // credential — so a browser with curl on the same laptop is indistinguishable from us.\n // Same bucket as public/jwt. (This switch has NO `default` on purpose: adding a kind to\n // AuthMode is a compile error here rather than a silent permissive fallthrough.)\n case 'local-only':\n return DestinationTrust.CANNOT_VERIFY_CALLER;\n }\n }\n\n /**\n * May this key go on the wire to this destination? Untrusted keys always may — nobody was ever\n * going to make a security decision on them. A trusted key may only when the destination will\n * authenticate US, because that is the only case its `AuthFilter` will admit it.\n */\n allows(key: AnyContextKey): boolean {\n return this.verifiesCaller || !key.isTrusted();\n }\n}\n"]}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where this process is running, as a NAMED token rather than a boolean:
|
|
3
|
+
*
|
|
4
|
+
* - `'local'` — a developer's machine. `@AuthLocalOnly` endpoints exist and serve.
|
|
5
|
+
* - `'deployed'` — anywhere else (staging, prod, CI, a container). `@AuthLocalOnly` endpoints are
|
|
6
|
+
* not registered and, if reached anyway, 404.
|
|
7
|
+
*
|
|
8
|
+
* A `boolean` would have made the DANGEROUS half (`true`) unnameable and ungreppable — see CLAUDE.md
|
|
9
|
+
* shim shape #5. `grep -rn "'local'" ` over a repo's startup now lists every place that claims to be a
|
|
10
|
+
* developer's machine.
|
|
11
|
+
*/
|
|
12
|
+
export type Locality = 'local' | 'deployed';
|
|
13
|
+
/**
|
|
14
|
+
* RuntimeLocality - the ONE answer to "am I running on a developer's machine?", for the one part of
|
|
15
|
+
* webpieces that needs it: {@link AuthLocalOnly}.
|
|
16
|
+
*
|
|
17
|
+
* ## Why this is a seam and not a `process.env` read
|
|
18
|
+
*
|
|
19
|
+
* The framework cannot compute this itself and must not try. "Local" is a fact about the DEPLOYMENT
|
|
20
|
+
* PLATFORM: Cloud Run derives it from `K_SERVICE`, ECS from `ECS_CONTAINER_METADATA_URI`, a laptop
|
|
21
|
+
* from the absence of both. Baking any one of those into core-util would hardcode a cloud vendor into
|
|
22
|
+
* the framework core, and core-util is browser-safe (it may not read `process.env` at all). So the
|
|
23
|
+
* ENVIRONMENT tells the framework, exactly as it tells it the logging backend
|
|
24
|
+
* ({@link LogManager.setFactory}), the header set ({@link HeaderRegistry.configure}), the context seam
|
|
25
|
+
* ({@link ApiCallContextHolder.install}) and its own identity ({@link ServiceInfo.setInfo}).
|
|
26
|
+
*
|
|
27
|
+
* It is a VALUE holder rather than an interface-plus-impl (the `ApiCallContext` shape) because there
|
|
28
|
+
* is no behavior to plug in — the answer is one token fixed at startup. Per CLAUDE.md, data is a
|
|
29
|
+
* class; only behavior is an interface.
|
|
30
|
+
*
|
|
31
|
+
* ## Where it is declared
|
|
32
|
+
*
|
|
33
|
+
* `RuntimeSetupOptions` takes it as a REQUIRED, positional constructor argument, so `setupRuntime`
|
|
34
|
+
* declares it on every server and no server can boot without having stated it. That is the same
|
|
35
|
+
* forcing function `@Endpoint(path, kind)` uses: a required positional argument turns "we forgot" into
|
|
36
|
+
* a compile error instead of a runtime guess.
|
|
37
|
+
*
|
|
38
|
+
* ## FAIL SAFE when nothing declared it
|
|
39
|
+
*
|
|
40
|
+
* {@link isLocalDevelopment} returns `false` until {@link declare} is called. An undeclared process is
|
|
41
|
+
* treated as DEPLOYED, so the failure mode of a forgotten wiring call is "my local-only endpoint 404s
|
|
42
|
+
* on my laptop" — annoying and instantly visible — never "my local-only endpoint is live in
|
|
43
|
+
* production". The permissive answer is never the one you get by not typing anything.
|
|
44
|
+
*/
|
|
45
|
+
export declare class RuntimeLocality {
|
|
46
|
+
/** Process-global; set once at startup. `undefined` = never declared = treated as deployed. */
|
|
47
|
+
private static locality;
|
|
48
|
+
/**
|
|
49
|
+
* State where this process is running. Call it at startup — `setupRuntime` does it for you from
|
|
50
|
+
* `RuntimeSetupOptions.locality`.
|
|
51
|
+
*
|
|
52
|
+
* LAST CALL WINS, mirroring {@link ServiceInfo.setInfo}: an in-process test can legitimately boot
|
|
53
|
+
* two servers back-to-back.
|
|
54
|
+
*/
|
|
55
|
+
static declare(locality: Locality): void;
|
|
56
|
+
/**
|
|
57
|
+
* True ONLY when a startup explicitly declared `'local'`. Undeclared reads as deployed — see the
|
|
58
|
+
* fail-safe note on the class. Does not throw: a wrong answer here must refuse an endpoint, never
|
|
59
|
+
* 500 unrelated traffic.
|
|
60
|
+
*/
|
|
61
|
+
static isLocalDevelopment(): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Whether anything declared a locality at all. Used ONLY to make the refusal log say which of the
|
|
64
|
+
* two reasons applies — "you are deployed" vs "nobody ever told me" — because those have very
|
|
65
|
+
* different fixes and a developer staring at a 404 on their own laptop needs to know which.
|
|
66
|
+
*/
|
|
67
|
+
static isDeclared(): boolean;
|
|
68
|
+
/** Reset — for tests, mirroring {@link ServiceInfo.clear}. */
|
|
69
|
+
static clear(): void;
|
|
70
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RuntimeLocality = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* RuntimeLocality - the ONE answer to "am I running on a developer's machine?", for the one part of
|
|
6
|
+
* webpieces that needs it: {@link AuthLocalOnly}.
|
|
7
|
+
*
|
|
8
|
+
* ## Why this is a seam and not a `process.env` read
|
|
9
|
+
*
|
|
10
|
+
* The framework cannot compute this itself and must not try. "Local" is a fact about the DEPLOYMENT
|
|
11
|
+
* PLATFORM: Cloud Run derives it from `K_SERVICE`, ECS from `ECS_CONTAINER_METADATA_URI`, a laptop
|
|
12
|
+
* from the absence of both. Baking any one of those into core-util would hardcode a cloud vendor into
|
|
13
|
+
* the framework core, and core-util is browser-safe (it may not read `process.env` at all). So the
|
|
14
|
+
* ENVIRONMENT tells the framework, exactly as it tells it the logging backend
|
|
15
|
+
* ({@link LogManager.setFactory}), the header set ({@link HeaderRegistry.configure}), the context seam
|
|
16
|
+
* ({@link ApiCallContextHolder.install}) and its own identity ({@link ServiceInfo.setInfo}).
|
|
17
|
+
*
|
|
18
|
+
* It is a VALUE holder rather than an interface-plus-impl (the `ApiCallContext` shape) because there
|
|
19
|
+
* is no behavior to plug in — the answer is one token fixed at startup. Per CLAUDE.md, data is a
|
|
20
|
+
* class; only behavior is an interface.
|
|
21
|
+
*
|
|
22
|
+
* ## Where it is declared
|
|
23
|
+
*
|
|
24
|
+
* `RuntimeSetupOptions` takes it as a REQUIRED, positional constructor argument, so `setupRuntime`
|
|
25
|
+
* declares it on every server and no server can boot without having stated it. That is the same
|
|
26
|
+
* forcing function `@Endpoint(path, kind)` uses: a required positional argument turns "we forgot" into
|
|
27
|
+
* a compile error instead of a runtime guess.
|
|
28
|
+
*
|
|
29
|
+
* ## FAIL SAFE when nothing declared it
|
|
30
|
+
*
|
|
31
|
+
* {@link isLocalDevelopment} returns `false` until {@link declare} is called. An undeclared process is
|
|
32
|
+
* treated as DEPLOYED, so the failure mode of a forgotten wiring call is "my local-only endpoint 404s
|
|
33
|
+
* on my laptop" — annoying and instantly visible — never "my local-only endpoint is live in
|
|
34
|
+
* production". The permissive answer is never the one you get by not typing anything.
|
|
35
|
+
*/
|
|
36
|
+
class RuntimeLocality {
|
|
37
|
+
/** Process-global; set once at startup. `undefined` = never declared = treated as deployed. */
|
|
38
|
+
static locality;
|
|
39
|
+
/**
|
|
40
|
+
* State where this process is running. Call it at startup — `setupRuntime` does it for you from
|
|
41
|
+
* `RuntimeSetupOptions.locality`.
|
|
42
|
+
*
|
|
43
|
+
* LAST CALL WINS, mirroring {@link ServiceInfo.setInfo}: an in-process test can legitimately boot
|
|
44
|
+
* two servers back-to-back.
|
|
45
|
+
*/
|
|
46
|
+
// webpieces-disable no-function-outside-class -- static global singleton (like ServiceInfo/HeaderRegistry); populated once at startup, never DI-injected
|
|
47
|
+
static declare(locality) {
|
|
48
|
+
RuntimeLocality.locality = locality;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* True ONLY when a startup explicitly declared `'local'`. Undeclared reads as deployed — see the
|
|
52
|
+
* fail-safe note on the class. Does not throw: a wrong answer here must refuse an endpoint, never
|
|
53
|
+
* 500 unrelated traffic.
|
|
54
|
+
*/
|
|
55
|
+
// webpieces-disable no-function-outside-class -- static global singleton (like ServiceInfo/HeaderRegistry); populated once at startup, never DI-injected
|
|
56
|
+
static isLocalDevelopment() {
|
|
57
|
+
return RuntimeLocality.locality === 'local';
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Whether anything declared a locality at all. Used ONLY to make the refusal log say which of the
|
|
61
|
+
* two reasons applies — "you are deployed" vs "nobody ever told me" — because those have very
|
|
62
|
+
* different fixes and a developer staring at a 404 on their own laptop needs to know which.
|
|
63
|
+
*/
|
|
64
|
+
// webpieces-disable no-function-outside-class -- static global singleton (like ServiceInfo/HeaderRegistry); populated once at startup, never DI-injected
|
|
65
|
+
static isDeclared() {
|
|
66
|
+
return RuntimeLocality.locality !== undefined;
|
|
67
|
+
}
|
|
68
|
+
/** Reset — for tests, mirroring {@link ServiceInfo.clear}. */
|
|
69
|
+
// webpieces-disable no-function-outside-class -- static global singleton (like ServiceInfo/HeaderRegistry); populated once at startup, never DI-injected
|
|
70
|
+
static clear() {
|
|
71
|
+
RuntimeLocality.locality = undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.RuntimeLocality = RuntimeLocality;
|
|
75
|
+
//# sourceMappingURL=RuntimeLocality.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RuntimeLocality.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/RuntimeLocality.ts"],"names":[],"mappings":";;;AAaA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAa,eAAe;IACxB,+FAA+F;IACvF,MAAM,CAAC,QAAQ,CAAuB;IAE9C;;;;;;OAMG;IACH,yJAAyJ;IACzJ,MAAM,CAAC,OAAO,CAAC,QAAkB;QAC7B,eAAe,CAAC,QAAQ,GAAG,QAAQ,CAAC;IACxC,CAAC;IAED;;;;OAIG;IACH,yJAAyJ;IACzJ,MAAM,CAAC,kBAAkB;QACrB,OAAO,eAAe,CAAC,QAAQ,KAAK,OAAO,CAAC;IAChD,CAAC;IAED;;;;OAIG;IACH,yJAAyJ;IACzJ,MAAM,CAAC,UAAU;QACb,OAAO,eAAe,CAAC,QAAQ,KAAK,SAAS,CAAC;IAClD,CAAC;IAED,8DAA8D;IAC9D,yJAAyJ;IACzJ,MAAM,CAAC,KAAK;QACR,eAAe,CAAC,QAAQ,GAAG,SAAS,CAAC;IACzC,CAAC;CACJ;AAzCD,0CAyCC","sourcesContent":["/**\n * Where this process is running, as a NAMED token rather than a boolean:\n *\n * - `'local'` — a developer's machine. `@AuthLocalOnly` endpoints exist and serve.\n * - `'deployed'` — anywhere else (staging, prod, CI, a container). `@AuthLocalOnly` endpoints are\n * not registered and, if reached anyway, 404.\n *\n * A `boolean` would have made the DANGEROUS half (`true`) unnameable and ungreppable — see CLAUDE.md\n * shim shape #5. `grep -rn \"'local'\" ` over a repo's startup now lists every place that claims to be a\n * developer's machine.\n */\nexport type Locality = 'local' | 'deployed';\n\n/**\n * RuntimeLocality - the ONE answer to \"am I running on a developer's machine?\", for the one part of\n * webpieces that needs it: {@link AuthLocalOnly}.\n *\n * ## Why this is a seam and not a `process.env` read\n *\n * The framework cannot compute this itself and must not try. \"Local\" is a fact about the DEPLOYMENT\n * PLATFORM: Cloud Run derives it from `K_SERVICE`, ECS from `ECS_CONTAINER_METADATA_URI`, a laptop\n * from the absence of both. Baking any one of those into core-util would hardcode a cloud vendor into\n * the framework core, and core-util is browser-safe (it may not read `process.env` at all). So the\n * ENVIRONMENT tells the framework, exactly as it tells it the logging backend\n * ({@link LogManager.setFactory}), the header set ({@link HeaderRegistry.configure}), the context seam\n * ({@link ApiCallContextHolder.install}) and its own identity ({@link ServiceInfo.setInfo}).\n *\n * It is a VALUE holder rather than an interface-plus-impl (the `ApiCallContext` shape) because there\n * is no behavior to plug in — the answer is one token fixed at startup. Per CLAUDE.md, data is a\n * class; only behavior is an interface.\n *\n * ## Where it is declared\n *\n * `RuntimeSetupOptions` takes it as a REQUIRED, positional constructor argument, so `setupRuntime`\n * declares it on every server and no server can boot without having stated it. That is the same\n * forcing function `@Endpoint(path, kind)` uses: a required positional argument turns \"we forgot\" into\n * a compile error instead of a runtime guess.\n *\n * ## FAIL SAFE when nothing declared it\n *\n * {@link isLocalDevelopment} returns `false` until {@link declare} is called. An undeclared process is\n * treated as DEPLOYED, so the failure mode of a forgotten wiring call is \"my local-only endpoint 404s\n * on my laptop\" — annoying and instantly visible — never \"my local-only endpoint is live in\n * production\". The permissive answer is never the one you get by not typing anything.\n */\nexport class RuntimeLocality {\n /** Process-global; set once at startup. `undefined` = never declared = treated as deployed. */\n private static locality: Locality | undefined;\n\n /**\n * State where this process is running. Call it at startup — `setupRuntime` does it for you from\n * `RuntimeSetupOptions.locality`.\n *\n * LAST CALL WINS, mirroring {@link ServiceInfo.setInfo}: an in-process test can legitimately boot\n * two servers back-to-back.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like ServiceInfo/HeaderRegistry); populated once at startup, never DI-injected\n static declare(locality: Locality): void {\n RuntimeLocality.locality = locality;\n }\n\n /**\n * True ONLY when a startup explicitly declared `'local'`. Undeclared reads as deployed — see the\n * fail-safe note on the class. Does not throw: a wrong answer here must refuse an endpoint, never\n * 500 unrelated traffic.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like ServiceInfo/HeaderRegistry); populated once at startup, never DI-injected\n static isLocalDevelopment(): boolean {\n return RuntimeLocality.locality === 'local';\n }\n\n /**\n * Whether anything declared a locality at all. Used ONLY to make the refusal log say which of the\n * two reasons applies — \"you are deployed\" vs \"nobody ever told me\" — because those have very\n * different fixes and a developer staring at a 404 on their own laptop needs to know which.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like ServiceInfo/HeaderRegistry); populated once at startup, never DI-injected\n static isDeclared(): boolean {\n return RuntimeLocality.locality !== undefined;\n }\n\n /** Reset — for tests, mirroring {@link ServiceInfo.clear}. */\n // webpieces-disable no-function-outside-class -- static global singleton (like ServiceInfo/HeaderRegistry); populated once at startup, never DI-injected\n static clear(): void {\n RuntimeLocality.locality = undefined;\n }\n}\n"]}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { EndpointKind } from './decorators';
|
|
3
|
+
/**
|
|
4
|
+
* API KIND — whether a contract is synchronous RPC or fire-and-forget over a queue — plus the
|
|
5
|
+
* queue-naming rules that only a @PubSub contract has.
|
|
6
|
+
*
|
|
7
|
+
* Split out of `decorators.ts` purely for size (max-file-lines); the dependency runs ONE way,
|
|
8
|
+
* api-kind -> decorators, so there is no cycle. Auth modes and endpoint shape stay in
|
|
9
|
+
* `decorators.ts`; everything here is re-exported from the package barrel, so no consumer import
|
|
10
|
+
* changes and there is no second spelling of anything.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* API kind. 'rpc' = synchronous request/response (http-client ↔ ApiRoutingFactory).
|
|
14
|
+
* 'pubsub' = fire-and-forget cloud task; the enqueue client (cloudtasks-client)
|
|
15
|
+
* schedules a Cloud Task that is later delivered to the SAME controller endpoint.
|
|
16
|
+
*/
|
|
17
|
+
export type ApiKind = 'rpc' | 'pubsub';
|
|
18
|
+
/**
|
|
19
|
+
* @Rpc() - marks an API class as synchronous request/response (the default kind).
|
|
20
|
+
* Present mostly for symmetry/readability; an undecorated API is treated as 'rpc'.
|
|
21
|
+
*/
|
|
22
|
+
export declare function Rpc(): ClassDecorator;
|
|
23
|
+
/**
|
|
24
|
+
* @PubSub() - marks an API class as fire-and-forget over Cloud Tasks. Every method
|
|
25
|
+
* MUST return Promise<void> (a compile-time contract on the abstract API). The
|
|
26
|
+
* enqueue client and the controller share this one class, exactly like RPC.
|
|
27
|
+
*/
|
|
28
|
+
export declare function PubSub(): ClassDecorator;
|
|
29
|
+
/**
|
|
30
|
+
* @Queue(name) - override the Cloud Tasks queue name for a @PubSub method. Default
|
|
31
|
+
* (no decorator) is `${ApiClassName}-${methodName}`, matched 1:1 by Terraform.
|
|
32
|
+
*/
|
|
33
|
+
export declare function Queue(name: string): MethodDecorator;
|
|
34
|
+
/**
|
|
35
|
+
* Get the API kind. Defaults to 'rpc' when neither @Rpc nor @PubSub is present.
|
|
36
|
+
*/
|
|
37
|
+
export declare function getApiKind(apiClass: Function): ApiKind;
|
|
38
|
+
/**
|
|
39
|
+
* Assert the API class is of the expected kind (used by the clients: the RPC
|
|
40
|
+
* client rejects a @PubSub api and vice-versa).
|
|
41
|
+
* @throws Error if the kind doesn't match.
|
|
42
|
+
*/
|
|
43
|
+
export declare function assertApiKind(apiClass: Function, expected: ApiKind): void;
|
|
44
|
+
/**
|
|
45
|
+
* Which {@link EndpointKind}s each {@link ApiKind} may declare. A @PubSub contract is delivered
|
|
46
|
+
* asynchronously by definition, so `rpc` is meaningless on it; an @Rpc contract has no queue, so
|
|
47
|
+
* `cloudtasks`/`cron` on it would name a queue/schedule nothing could ever deliver to. `external`
|
|
48
|
+
* is legal on both — a webhook posts synchronously, a push subscription does not.
|
|
49
|
+
*
|
|
50
|
+
* Shared so the wiring-time assert below and the build-time architecture scan enforce ONE rule.
|
|
51
|
+
*/
|
|
52
|
+
export declare const ENDPOINT_KINDS_BY_API_KIND: Record<ApiKind, readonly EndpointKind[]>;
|
|
53
|
+
/**
|
|
54
|
+
* Validate @PubSub conventions at wiring time: the class must be @ApiPath + @PubSub, declare at
|
|
55
|
+
* least one endpoint, and every endpoint must declare a kind this api kind can actually deliver.
|
|
56
|
+
* (Return-type is Promise<void>, a compile-time contract — TS erases types at runtime so it cannot
|
|
57
|
+
* be re-checked here.)
|
|
58
|
+
* @throws Error if conventions are violated.
|
|
59
|
+
*/
|
|
60
|
+
export declare function assertPubSubConventions(apiClass: Function): void;
|
|
61
|
+
/**
|
|
62
|
+
* Resolve the Cloud Tasks queue name for a @PubSub method: the @Queue override if
|
|
63
|
+
* present, else `${ApiClassName}-${methodName}`.
|
|
64
|
+
*/
|
|
65
|
+
export declare function getQueueName(apiClass: Function, methodName: string): string;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ENDPOINT_KINDS_BY_API_KIND = void 0;
|
|
4
|
+
exports.Rpc = Rpc;
|
|
5
|
+
exports.PubSub = PubSub;
|
|
6
|
+
exports.Queue = Queue;
|
|
7
|
+
exports.getApiKind = getApiKind;
|
|
8
|
+
exports.assertApiKind = assertApiKind;
|
|
9
|
+
exports.assertPubSubConventions = assertPubSubConventions;
|
|
10
|
+
exports.getQueueName = getQueueName;
|
|
11
|
+
require("reflect-metadata");
|
|
12
|
+
const decorators_1 = require("./decorators");
|
|
13
|
+
/**
|
|
14
|
+
* @Rpc() - marks an API class as synchronous request/response (the default kind).
|
|
15
|
+
* Present mostly for symmetry/readability; an undecorated API is treated as 'rpc'.
|
|
16
|
+
*/
|
|
17
|
+
// webpieces-disable no-function-outside-class -- decorator factory / reflect-metadata reader; moved verbatim from decorators.ts for file size, same module-scope shape as its siblings there
|
|
18
|
+
function Rpc() {
|
|
19
|
+
// webpieces-disable no-any-unknown -- reflect-metadata decorator API requires any
|
|
20
|
+
return (target) => {
|
|
21
|
+
Reflect.defineMetadata(decorators_1.METADATA_KEYS.API_KIND, 'rpc', target);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* @PubSub() - marks an API class as fire-and-forget over Cloud Tasks. Every method
|
|
26
|
+
* MUST return Promise<void> (a compile-time contract on the abstract API). The
|
|
27
|
+
* enqueue client and the controller share this one class, exactly like RPC.
|
|
28
|
+
*/
|
|
29
|
+
// webpieces-disable no-function-outside-class -- decorator factory / reflect-metadata reader; moved verbatim from decorators.ts for file size, same module-scope shape as its siblings there
|
|
30
|
+
function PubSub() {
|
|
31
|
+
// webpieces-disable no-any-unknown -- reflect-metadata decorator API requires any
|
|
32
|
+
return (target) => {
|
|
33
|
+
Reflect.defineMetadata(decorators_1.METADATA_KEYS.API_KIND, 'pubsub', target);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* @Queue(name) - override the Cloud Tasks queue name for a @PubSub method. Default
|
|
38
|
+
* (no decorator) is `${ApiClassName}-${methodName}`, matched 1:1 by Terraform.
|
|
39
|
+
*/
|
|
40
|
+
// webpieces-disable no-function-outside-class -- decorator factory / reflect-metadata reader; moved verbatim from decorators.ts for file size, same module-scope shape as its siblings there
|
|
41
|
+
function Queue(name) {
|
|
42
|
+
// webpieces-disable no-any-unknown -- reflect-metadata decorator API requires any
|
|
43
|
+
return (target, propertyKey, _descriptor) => {
|
|
44
|
+
const metadataTarget = typeof target === 'function' ? target : target.constructor;
|
|
45
|
+
const overrides = Reflect.getMetadata(decorators_1.METADATA_KEYS.QUEUE_OVERRIDE, metadataTarget) || {};
|
|
46
|
+
overrides[propertyKey] = name;
|
|
47
|
+
Reflect.defineMetadata(decorators_1.METADATA_KEYS.QUEUE_OVERRIDE, overrides, metadataTarget);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Get the API kind. Defaults to 'rpc' when neither @Rpc nor @PubSub is present.
|
|
52
|
+
*/
|
|
53
|
+
// webpieces-disable no-function-outside-class -- decorator factory / reflect-metadata reader; moved verbatim from decorators.ts for file size, same module-scope shape as its siblings there
|
|
54
|
+
function getApiKind(apiClass) {
|
|
55
|
+
return Reflect.getMetadata(decorators_1.METADATA_KEYS.API_KIND, apiClass) ?? 'rpc';
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Assert the API class is of the expected kind (used by the clients: the RPC
|
|
59
|
+
* client rejects a @PubSub api and vice-versa).
|
|
60
|
+
* @throws Error if the kind doesn't match.
|
|
61
|
+
*/
|
|
62
|
+
// webpieces-disable no-function-outside-class -- decorator factory / reflect-metadata reader; moved verbatim from decorators.ts for file size, same module-scope shape as its siblings there
|
|
63
|
+
function assertApiKind(apiClass, expected) {
|
|
64
|
+
const actual = getApiKind(apiClass);
|
|
65
|
+
if (actual !== expected) {
|
|
66
|
+
const apiName = apiClass.name || 'Unknown';
|
|
67
|
+
throw new Error(`API ${apiName} is @${actual === 'pubsub' ? 'PubSub' : 'Rpc'} but a ` +
|
|
68
|
+
`${expected === 'pubsub' ? '@PubSub (cloud task)' : '@Rpc'} API was required here.`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Which {@link EndpointKind}s each {@link ApiKind} may declare. A @PubSub contract is delivered
|
|
73
|
+
* asynchronously by definition, so `rpc` is meaningless on it; an @Rpc contract has no queue, so
|
|
74
|
+
* `cloudtasks`/`cron` on it would name a queue/schedule nothing could ever deliver to. `external`
|
|
75
|
+
* is legal on both — a webhook posts synchronously, a push subscription does not.
|
|
76
|
+
*
|
|
77
|
+
* Shared so the wiring-time assert below and the build-time architecture scan enforce ONE rule.
|
|
78
|
+
*/
|
|
79
|
+
exports.ENDPOINT_KINDS_BY_API_KIND = {
|
|
80
|
+
rpc: ['rpc', 'external'],
|
|
81
|
+
pubsub: ['cloudtasks', 'cron', 'external'],
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Validate @PubSub conventions at wiring time: the class must be @ApiPath + @PubSub, declare at
|
|
85
|
+
* least one endpoint, and every endpoint must declare a kind this api kind can actually deliver.
|
|
86
|
+
* (Return-type is Promise<void>, a compile-time contract — TS erases types at runtime so it cannot
|
|
87
|
+
* be re-checked here.)
|
|
88
|
+
* @throws Error if conventions are violated.
|
|
89
|
+
*/
|
|
90
|
+
// webpieces-disable no-function-outside-class -- decorator factory / reflect-metadata reader; moved verbatim from decorators.ts for file size, same module-scope shape as its siblings there
|
|
91
|
+
function assertPubSubConventions(apiClass) {
|
|
92
|
+
assertApiKind(apiClass, 'pubsub');
|
|
93
|
+
const apiName = apiClass.name || 'Unknown';
|
|
94
|
+
if (!(0, decorators_1.isApiPath)(apiClass)) {
|
|
95
|
+
throw new Error(`@PubSub API ${apiName} must also be decorated with @ApiPath()`);
|
|
96
|
+
}
|
|
97
|
+
const endpoints = (0, decorators_1.getEndpoints)(apiClass) || {};
|
|
98
|
+
if (Object.keys(endpoints).length === 0) {
|
|
99
|
+
throw new Error(`@PubSub API ${apiName} declares no @Endpoint methods`);
|
|
100
|
+
}
|
|
101
|
+
const allowed = exports.ENDPOINT_KINDS_BY_API_KIND.pubsub;
|
|
102
|
+
const kinds = (0, decorators_1.getEndpointKinds)(apiClass);
|
|
103
|
+
for (const methodName of Object.keys(endpoints)) {
|
|
104
|
+
const kind = kinds[methodName];
|
|
105
|
+
if (kind !== undefined && allowed.includes(kind))
|
|
106
|
+
continue;
|
|
107
|
+
throw new Error(`@PubSub API ${apiName}.${methodName} declares @Endpoint(..., '${kind ?? 'missing'}') — a ` +
|
|
108
|
+
`@PubSub contract is delivered through a queue, so it must be one of: ${allowed.join(' | ')}.`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Resolve the Cloud Tasks queue name for a @PubSub method: the @Queue override if
|
|
113
|
+
* present, else `${ApiClassName}-${methodName}`.
|
|
114
|
+
*/
|
|
115
|
+
// webpieces-disable no-function-outside-class -- decorator factory / reflect-metadata reader; moved verbatim from decorators.ts for file size, same module-scope shape as its siblings there
|
|
116
|
+
function getQueueName(apiClass, methodName) {
|
|
117
|
+
const overrides = Reflect.getMetadata(decorators_1.METADATA_KEYS.QUEUE_OVERRIDE, apiClass) || {};
|
|
118
|
+
return overrides[methodName] ?? `${apiClass.name || 'Unknown'}-${methodName}`;
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=api-kind.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-kind.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/api-kind.ts"],"names":[],"mappings":"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","sourcesContent":["import 'reflect-metadata';\nimport { EndpointKind, METADATA_KEYS, getEndpoints, getEndpointKinds, isApiPath } from './decorators';\n\n/**\n * API KIND — whether a contract is synchronous RPC or fire-and-forget over a queue — plus the\n * queue-naming rules that only a @PubSub contract has.\n *\n * Split out of `decorators.ts` purely for size (max-file-lines); the dependency runs ONE way,\n * api-kind -> decorators, so there is no cycle. Auth modes and endpoint shape stay in\n * `decorators.ts`; everything here is re-exported from the package barrel, so no consumer import\n * changes and there is no second spelling of anything.\n */\n\n// ============================================================\n// API kind (RPC vs PubSub/Cloud Tasks) + queue naming\n// ============================================================\n\n/**\n * API kind. 'rpc' = synchronous request/response (http-client ↔ ApiRoutingFactory).\n * 'pubsub' = fire-and-forget cloud task; the enqueue client (cloudtasks-client)\n * schedules a Cloud Task that is later delivered to the SAME controller endpoint.\n */\nexport type ApiKind = 'rpc' | 'pubsub';\n\n/**\n * @Rpc() - marks an API class as synchronous request/response (the default kind).\n * Present mostly for symmetry/readability; an undecorated API is treated as 'rpc'.\n */\n// webpieces-disable no-function-outside-class -- decorator factory / reflect-metadata reader; moved verbatim from decorators.ts for file size, same module-scope shape as its siblings there\nexport function Rpc(): ClassDecorator {\n // webpieces-disable no-any-unknown -- reflect-metadata decorator API requires any\n return (target: any) => {\n Reflect.defineMetadata(METADATA_KEYS.API_KIND, 'rpc' as ApiKind, target);\n };\n}\n\n/**\n * @PubSub() - marks an API class as fire-and-forget over Cloud Tasks. Every method\n * MUST return Promise<void> (a compile-time contract on the abstract API). The\n * enqueue client and the controller share this one class, exactly like RPC.\n */\n// webpieces-disable no-function-outside-class -- decorator factory / reflect-metadata reader; moved verbatim from decorators.ts for file size, same module-scope shape as its siblings there\nexport function PubSub(): ClassDecorator {\n // webpieces-disable no-any-unknown -- reflect-metadata decorator API requires any\n return (target: any) => {\n Reflect.defineMetadata(METADATA_KEYS.API_KIND, 'pubsub' as ApiKind, target);\n };\n}\n\n/**\n * @Queue(name) - override the Cloud Tasks queue name for a @PubSub method. Default\n * (no decorator) is `${ApiClassName}-${methodName}`, matched 1:1 by Terraform.\n */\n// webpieces-disable no-function-outside-class -- decorator factory / reflect-metadata reader; moved verbatim from decorators.ts for file size, same module-scope shape as its siblings there\nexport function Queue(name: string): MethodDecorator {\n // webpieces-disable no-any-unknown -- reflect-metadata decorator API requires any\n return (target: any, propertyKey: string | symbol, _descriptor: PropertyDescriptor) => {\n const metadataTarget = typeof target === 'function' ? target : target.constructor;\n const overrides: Record<string, string> =\n Reflect.getMetadata(METADATA_KEYS.QUEUE_OVERRIDE, metadataTarget) || {};\n overrides[propertyKey as string] = name;\n Reflect.defineMetadata(METADATA_KEYS.QUEUE_OVERRIDE, overrides, metadataTarget);\n };\n}\n\n/**\n * Get the API kind. Defaults to 'rpc' when neither @Rpc nor @PubSub is present.\n */\n// webpieces-disable no-function-outside-class -- decorator factory / reflect-metadata reader; moved verbatim from decorators.ts for file size, same module-scope shape as its siblings there\nexport function getApiKind(apiClass: Function): ApiKind {\n return (Reflect.getMetadata(METADATA_KEYS.API_KIND, apiClass) as ApiKind) ?? 'rpc';\n}\n\n/**\n * Assert the API class is of the expected kind (used by the clients: the RPC\n * client rejects a @PubSub api and vice-versa).\n * @throws Error if the kind doesn't match.\n */\n// webpieces-disable no-function-outside-class -- decorator factory / reflect-metadata reader; moved verbatim from decorators.ts for file size, same module-scope shape as its siblings there\nexport function assertApiKind(apiClass: Function, expected: ApiKind): void {\n const actual = getApiKind(apiClass);\n if (actual !== expected) {\n const apiName = apiClass.name || 'Unknown';\n throw new Error(\n `API ${apiName} is @${actual === 'pubsub' ? 'PubSub' : 'Rpc'} but a ` +\n `${expected === 'pubsub' ? '@PubSub (cloud task)' : '@Rpc'} API was required here.`,\n );\n }\n}\n\n/**\n * Which {@link EndpointKind}s each {@link ApiKind} may declare. A @PubSub contract is delivered\n * asynchronously by definition, so `rpc` is meaningless on it; an @Rpc contract has no queue, so\n * `cloudtasks`/`cron` on it would name a queue/schedule nothing could ever deliver to. `external`\n * is legal on both — a webhook posts synchronously, a push subscription does not.\n *\n * Shared so the wiring-time assert below and the build-time architecture scan enforce ONE rule.\n */\nexport const ENDPOINT_KINDS_BY_API_KIND: Record<ApiKind, readonly EndpointKind[]> = {\n rpc: ['rpc', 'external'],\n pubsub: ['cloudtasks', 'cron', 'external'],\n};\n\n/**\n * Validate @PubSub conventions at wiring time: the class must be @ApiPath + @PubSub, declare at\n * least one endpoint, and every endpoint must declare a kind this api kind can actually deliver.\n * (Return-type is Promise<void>, a compile-time contract — TS erases types at runtime so it cannot\n * be re-checked here.)\n * @throws Error if conventions are violated.\n */\n// webpieces-disable no-function-outside-class -- decorator factory / reflect-metadata reader; moved verbatim from decorators.ts for file size, same module-scope shape as its siblings there\nexport function assertPubSubConventions(apiClass: Function): void {\n assertApiKind(apiClass, 'pubsub');\n const apiName = apiClass.name || 'Unknown';\n if (!isApiPath(apiClass)) {\n throw new Error(`@PubSub API ${apiName} must also be decorated with @ApiPath()`);\n }\n const endpoints = getEndpoints(apiClass) || {};\n if (Object.keys(endpoints).length === 0) {\n throw new Error(`@PubSub API ${apiName} declares no @Endpoint methods`);\n }\n const allowed = ENDPOINT_KINDS_BY_API_KIND.pubsub;\n const kinds = getEndpointKinds(apiClass);\n for (const methodName of Object.keys(endpoints)) {\n const kind = kinds[methodName];\n if (kind !== undefined && allowed.includes(kind)) continue;\n throw new Error(\n `@PubSub API ${apiName}.${methodName} declares @Endpoint(..., '${kind ?? 'missing'}') — a ` +\n `@PubSub contract is delivered through a queue, so it must be one of: ${allowed.join(' | ')}.`,\n );\n }\n}\n\n/**\n * Resolve the Cloud Tasks queue name for a @PubSub method: the @Queue override if\n * present, else `${ApiClassName}-${methodName}`.\n */\n// webpieces-disable no-function-outside-class -- decorator factory / reflect-metadata reader; moved verbatim from decorators.ts for file size, same module-scope shape as its siblings there\nexport function getQueueName(apiClass: Function, methodName: string): string {\n const overrides: Record<string, string> =\n Reflect.getMetadata(METADATA_KEYS.QUEUE_OVERRIDE, apiClass) || {};\n return overrides[methodName] ?? `${apiClass.name || 'Unknown'}-${methodName}`;\n}\n"]}
|
package/src/http/decorators.d.ts
CHANGED
|
@@ -139,6 +139,9 @@ export type JwtRequirement = JwtRoles & {
|
|
|
139
139
|
* - `oidc` → Google OIDC service-to-service (Cloud Tasks delivery / cross-service RPC);
|
|
140
140
|
* `callers` is the allow-list of caller SAs ('self' = this service's SA)
|
|
141
141
|
* - `shared-secret` → constant-time compare of a header against the secret bound for `secretKey`
|
|
142
|
+
* - `local-only` → exists ONLY on a developer's machine; not registered and never served when
|
|
143
|
+
* {@link RuntimeLocality} says this process is deployed. Authenticates NOBODY —
|
|
144
|
+
* it is a deployment gate, not a credential.
|
|
142
145
|
*/
|
|
143
146
|
export type AuthMode = {
|
|
144
147
|
kind: 'public';
|
|
@@ -151,10 +154,12 @@ export type AuthMode = {
|
|
|
151
154
|
} | {
|
|
152
155
|
kind: 'shared-secret';
|
|
153
156
|
secretKey: string;
|
|
157
|
+
} | {
|
|
158
|
+
kind: 'local-only';
|
|
154
159
|
};
|
|
155
160
|
/**
|
|
156
161
|
* Auth metadata attached to a class or method via one of the auth decorators
|
|
157
|
-
* (@Public / @AuthJwt / @AuthOidc / @AuthSharedSecret) — one per credential kind.
|
|
162
|
+
* (@Public / @AuthJwt / @AuthOidc / @AuthSharedSecret / @AuthLocalOnly) — one per credential kind.
|
|
158
163
|
*
|
|
159
164
|
* Carries a discriminated {@link AuthMode} and nothing else. It USED to also expose
|
|
160
165
|
* `authenticated`/`roles` getters "for back-compat with readers that only understand the user-JWT
|
|
@@ -261,7 +266,7 @@ export declare function Public(): ClassDecorator & MethodDecorator;
|
|
|
261
266
|
*
|
|
262
267
|
* It absorbed the former `@Auth(requirement)` — same argument, same AuthMode, so two spellings of one
|
|
263
268
|
* decision. One decorator per credential kind now: `@Public` / `@AuthJwt` / `@AuthOidc` /
|
|
264
|
-
* `@AuthSharedSecret`.
|
|
269
|
+
* `@AuthSharedSecret` / `@AuthLocalOnly`.
|
|
265
270
|
*/
|
|
266
271
|
export declare function AuthJwt(requirement: JwtRequirement): ClassDecorator & MethodDecorator;
|
|
267
272
|
/**
|
|
@@ -288,6 +293,33 @@ export declare function AuthOidc(...callers: string[]): ClassDecorator & MethodD
|
|
|
288
293
|
* For internal callers that cannot mint OIDC tokens.
|
|
289
294
|
*/
|
|
290
295
|
export declare function AuthSharedSecret(key: string): ClassDecorator & MethodDecorator;
|
|
296
|
+
/**
|
|
297
|
+
* @AuthLocalOnly() - this endpoint exists ONLY on a developer's machine. Off-local it is not
|
|
298
|
+
* registered as a route at all, and if it is somehow reached it 404s. Class- or method-level.
|
|
299
|
+
*
|
|
300
|
+
* ```typescript
|
|
301
|
+
* @AuthLocalOnly()
|
|
302
|
+
* @Endpoint('/logs', 'rpc')
|
|
303
|
+
* sendBatch(request: SendLogBatchRequest): Promise<SendLogBatchResponse> { ... }
|
|
304
|
+
* ```
|
|
305
|
+
*
|
|
306
|
+
* WHY IT IS AN AUTH MODE AND NOT A ROUTE-MODULE `if`. Apps hand-rolled this in TWO places kept in
|
|
307
|
+
* sync by a comment: a route module that registered the route only locally, PLUS a
|
|
308
|
+
* `if (env !== 'local') throw new HttpForbiddenError(...)` at the top of the handler. Neither half
|
|
309
|
+
* was visible on the CONTRACT, so nothing reading the api — a human, a generated client, or an
|
|
310
|
+
* agent — could tell this endpoint from a `@Public` one. Both halves are the framework's job now,
|
|
311
|
+
* driven by this ONE declaration on the contract, which is where every other "who may call this"
|
|
312
|
+
* fact already lives.
|
|
313
|
+
*
|
|
314
|
+
* It is DELIBERATELY a peer of @Public / @AuthJwt / @AuthOidc / @AuthSharedSecret rather than an
|
|
315
|
+
* option on one of them: one decorator per credential kind, and "local-only" is a different kind of
|
|
316
|
+
* gate — it authenticates nobody, it excludes an entire environment.
|
|
317
|
+
*
|
|
318
|
+
* HOW "local" IS DECIDED: {@link RuntimeLocality}, declared once at startup (a REQUIRED input to
|
|
319
|
+
* `RuntimeSetupOptions`). Undeclared means DEPLOYED, so a forgotten wiring call refuses the endpoint
|
|
320
|
+
* rather than exposing it.
|
|
321
|
+
*/
|
|
322
|
+
export declare function AuthLocalOnly(): ClassDecorator & MethodDecorator;
|
|
291
323
|
/**
|
|
292
324
|
* Get the base path from @ApiPath decorator.
|
|
293
325
|
*/
|
|
@@ -356,60 +388,6 @@ export declare const MISSING_AUTH_DECORATOR_FIX: string;
|
|
|
356
388
|
* @throws Error naming the first endpoint with no auth decorator, via {@link MISSING_AUTH_DECORATOR_FIX}.
|
|
357
389
|
*/
|
|
358
390
|
export declare function assertEveryEndpointHasAuthMode(apiClass: Function): void;
|
|
359
|
-
/**
|
|
360
|
-
* API kind. 'rpc' = synchronous request/response (http-client ↔ ApiRoutingFactory).
|
|
361
|
-
* 'pubsub' = fire-and-forget cloud task; the enqueue client (cloudtasks-client)
|
|
362
|
-
* schedules a Cloud Task that is later delivered to the SAME controller endpoint.
|
|
363
|
-
*/
|
|
364
|
-
export type ApiKind = 'rpc' | 'pubsub';
|
|
365
|
-
/**
|
|
366
|
-
* @Rpc() - marks an API class as synchronous request/response (the default kind).
|
|
367
|
-
* Present mostly for symmetry/readability; an undecorated API is treated as 'rpc'.
|
|
368
|
-
*/
|
|
369
|
-
export declare function Rpc(): ClassDecorator;
|
|
370
|
-
/**
|
|
371
|
-
* @PubSub() - marks an API class as fire-and-forget over Cloud Tasks. Every method
|
|
372
|
-
* MUST return Promise<void> (a compile-time contract on the abstract API). The
|
|
373
|
-
* enqueue client and the controller share this one class, exactly like RPC.
|
|
374
|
-
*/
|
|
375
|
-
export declare function PubSub(): ClassDecorator;
|
|
376
|
-
/**
|
|
377
|
-
* @Queue(name) - override the Cloud Tasks queue name for a @PubSub method. Default
|
|
378
|
-
* (no decorator) is `${ApiClassName}-${methodName}`, matched 1:1 by Terraform.
|
|
379
|
-
*/
|
|
380
|
-
export declare function Queue(name: string): MethodDecorator;
|
|
381
|
-
/**
|
|
382
|
-
* Get the API kind. Defaults to 'rpc' when neither @Rpc nor @PubSub is present.
|
|
383
|
-
*/
|
|
384
|
-
export declare function getApiKind(apiClass: Function): ApiKind;
|
|
385
|
-
/**
|
|
386
|
-
* Assert the API class is of the expected kind (used by the clients: the RPC
|
|
387
|
-
* client rejects a @PubSub api and vice-versa).
|
|
388
|
-
* @throws Error if the kind doesn't match.
|
|
389
|
-
*/
|
|
390
|
-
export declare function assertApiKind(apiClass: Function, expected: ApiKind): void;
|
|
391
|
-
/**
|
|
392
|
-
* Which {@link EndpointKind}s each {@link ApiKind} may declare. A @PubSub contract is delivered
|
|
393
|
-
* asynchronously by definition, so `rpc` is meaningless on it; an @Rpc contract has no queue, so
|
|
394
|
-
* `cloudtasks`/`cron` on it would name a queue/schedule nothing could ever deliver to. `external`
|
|
395
|
-
* is legal on both — a webhook posts synchronously, a push subscription does not.
|
|
396
|
-
*
|
|
397
|
-
* Shared so the wiring-time assert below and the build-time architecture scan enforce ONE rule.
|
|
398
|
-
*/
|
|
399
|
-
export declare const ENDPOINT_KINDS_BY_API_KIND: Record<ApiKind, readonly EndpointKind[]>;
|
|
400
|
-
/**
|
|
401
|
-
* Validate @PubSub conventions at wiring time: the class must be @ApiPath + @PubSub, declare at
|
|
402
|
-
* least one endpoint, and every endpoint must declare a kind this api kind can actually deliver.
|
|
403
|
-
* (Return-type is Promise<void>, a compile-time contract — TS erases types at runtime so it cannot
|
|
404
|
-
* be re-checked here.)
|
|
405
|
-
* @throws Error if conventions are violated.
|
|
406
|
-
*/
|
|
407
|
-
export declare function assertPubSubConventions(apiClass: Function): void;
|
|
408
|
-
/**
|
|
409
|
-
* Resolve the Cloud Tasks queue name for a @PubSub method: the @Queue override if
|
|
410
|
-
* present, else `${ApiClassName}-${methodName}`.
|
|
411
|
-
*/
|
|
412
|
-
export declare function getQueueName(apiClass: Function, methodName: string): string;
|
|
413
391
|
/**
|
|
414
392
|
* Validate that a class/method doesn't have conflicting auth decorators.
|
|
415
393
|
* @throws Error if multiple auth decorators are found on the same target.
|
package/src/http/decorators.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.MISSING_AUTH_DECORATOR_FIX = exports.AuthMeta = exports.RouteMetadata = exports.METADATA_KEYS = void 0;
|
|
4
4
|
exports.ApiPath = ApiPath;
|
|
5
5
|
exports.Endpoint = Endpoint;
|
|
6
6
|
exports.MaskLog = MaskLog;
|
|
@@ -10,6 +10,7 @@ exports.AuthJwt = AuthJwt;
|
|
|
10
10
|
exports.rolesRequired = rolesRequired;
|
|
11
11
|
exports.AuthOidc = AuthOidc;
|
|
12
12
|
exports.AuthSharedSecret = AuthSharedSecret;
|
|
13
|
+
exports.AuthLocalOnly = AuthLocalOnly;
|
|
13
14
|
exports.getApiPath = getApiPath;
|
|
14
15
|
exports.getEndpoints = getEndpoints;
|
|
15
16
|
exports.getEndpointKinds = getEndpointKinds;
|
|
@@ -21,13 +22,6 @@ exports.isApiPath = isApiPath;
|
|
|
21
22
|
exports.getAuthMeta = getAuthMeta;
|
|
22
23
|
exports.getAuthMode = getAuthMode;
|
|
23
24
|
exports.assertEveryEndpointHasAuthMode = assertEveryEndpointHasAuthMode;
|
|
24
|
-
exports.Rpc = Rpc;
|
|
25
|
-
exports.PubSub = PubSub;
|
|
26
|
-
exports.Queue = Queue;
|
|
27
|
-
exports.getApiKind = getApiKind;
|
|
28
|
-
exports.assertApiKind = assertApiKind;
|
|
29
|
-
exports.assertPubSubConventions = assertPubSubConventions;
|
|
30
|
-
exports.getQueueName = getQueueName;
|
|
31
25
|
exports.validateNoConflictingDecorators = validateNoConflictingDecorators;
|
|
32
26
|
require("reflect-metadata");
|
|
33
27
|
const LogFieldMask_1 = require("./LogFieldMask");
|
|
@@ -93,7 +87,7 @@ class RouteMetadata {
|
|
|
93
87
|
exports.RouteMetadata = RouteMetadata;
|
|
94
88
|
/**
|
|
95
89
|
* Auth metadata attached to a class or method via one of the auth decorators
|
|
96
|
-
* (@Public / @AuthJwt / @AuthOidc / @AuthSharedSecret) — one per credential kind.
|
|
90
|
+
* (@Public / @AuthJwt / @AuthOidc / @AuthSharedSecret / @AuthLocalOnly) — one per credential kind.
|
|
97
91
|
*
|
|
98
92
|
* Carries a discriminated {@link AuthMode} and nothing else. It USED to also expose
|
|
99
93
|
* `authenticated`/`roles` getters "for back-compat with readers that only understand the user-JWT
|
|
@@ -233,7 +227,7 @@ function Public() {
|
|
|
233
227
|
*
|
|
234
228
|
* It absorbed the former `@Auth(requirement)` — same argument, same AuthMode, so two spellings of one
|
|
235
229
|
* decision. One decorator per credential kind now: `@Public` / `@AuthJwt` / `@AuthOidc` /
|
|
236
|
-
* `@AuthSharedSecret`.
|
|
230
|
+
* `@AuthSharedSecret` / `@AuthLocalOnly`.
|
|
237
231
|
*/
|
|
238
232
|
// webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope
|
|
239
233
|
function AuthJwt(requirement) {
|
|
@@ -270,6 +264,36 @@ function AuthOidc(...callers) {
|
|
|
270
264
|
function AuthSharedSecret(key) {
|
|
271
265
|
return defineAuthMode({ kind: 'shared-secret', secretKey: key });
|
|
272
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* @AuthLocalOnly() - this endpoint exists ONLY on a developer's machine. Off-local it is not
|
|
269
|
+
* registered as a route at all, and if it is somehow reached it 404s. Class- or method-level.
|
|
270
|
+
*
|
|
271
|
+
* ```typescript
|
|
272
|
+
* @AuthLocalOnly()
|
|
273
|
+
* @Endpoint('/logs', 'rpc')
|
|
274
|
+
* sendBatch(request: SendLogBatchRequest): Promise<SendLogBatchResponse> { ... }
|
|
275
|
+
* ```
|
|
276
|
+
*
|
|
277
|
+
* WHY IT IS AN AUTH MODE AND NOT A ROUTE-MODULE `if`. Apps hand-rolled this in TWO places kept in
|
|
278
|
+
* sync by a comment: a route module that registered the route only locally, PLUS a
|
|
279
|
+
* `if (env !== 'local') throw new HttpForbiddenError(...)` at the top of the handler. Neither half
|
|
280
|
+
* was visible on the CONTRACT, so nothing reading the api — a human, a generated client, or an
|
|
281
|
+
* agent — could tell this endpoint from a `@Public` one. Both halves are the framework's job now,
|
|
282
|
+
* driven by this ONE declaration on the contract, which is where every other "who may call this"
|
|
283
|
+
* fact already lives.
|
|
284
|
+
*
|
|
285
|
+
* It is DELIBERATELY a peer of @Public / @AuthJwt / @AuthOidc / @AuthSharedSecret rather than an
|
|
286
|
+
* option on one of them: one decorator per credential kind, and "local-only" is a different kind of
|
|
287
|
+
* gate — it authenticates nobody, it excludes an entire environment.
|
|
288
|
+
*
|
|
289
|
+
* HOW "local" IS DECIDED: {@link RuntimeLocality}, declared once at startup (a REQUIRED input to
|
|
290
|
+
* `RuntimeSetupOptions`). Undeclared means DEPLOYED, so a forgotten wiring call refuses the endpoint
|
|
291
|
+
* rather than exposing it.
|
|
292
|
+
*/
|
|
293
|
+
// webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope
|
|
294
|
+
function AuthLocalOnly() {
|
|
295
|
+
return defineAuthMode({ kind: 'local-only' });
|
|
296
|
+
}
|
|
273
297
|
// ============================================================
|
|
274
298
|
// Helper functions
|
|
275
299
|
// ============================================================
|
|
@@ -375,7 +399,7 @@ function getAuthMode(apiClass, methodName) {
|
|
|
375
399
|
* the first thing offered should not be the widest grant.
|
|
376
400
|
*/
|
|
377
401
|
exports.MISSING_AUTH_DECORATOR_FIX = "Add one of @AuthJwt({roles: ['admin']}) / @AuthJwt({allRolesAllowed: true}) / @Public() / " +
|
|
378
|
-
'@AuthOidc(...callers) / @AuthSharedSecret(key) to the class or method.';
|
|
402
|
+
'@AuthOidc(...callers) / @AuthSharedSecret(key) / @AuthLocalOnly() to the class or method.';
|
|
379
403
|
/**
|
|
380
404
|
* Fail-fast at wiring time if any endpoint lacks an auth mode. Both the server
|
|
381
405
|
* (ApiRoutingFactory) and the task/rpc clients call this so a missing auth
|
|
@@ -392,106 +416,6 @@ function assertEveryEndpointHasAuthMode(apiClass) {
|
|
|
392
416
|
}
|
|
393
417
|
}
|
|
394
418
|
}
|
|
395
|
-
/**
|
|
396
|
-
* @Rpc() - marks an API class as synchronous request/response (the default kind).
|
|
397
|
-
* Present mostly for symmetry/readability; an undecorated API is treated as 'rpc'.
|
|
398
|
-
*/
|
|
399
|
-
function Rpc() {
|
|
400
|
-
// webpieces-disable no-any-unknown -- reflect-metadata decorator API requires any
|
|
401
|
-
return (target) => {
|
|
402
|
-
Reflect.defineMetadata(exports.METADATA_KEYS.API_KIND, 'rpc', target);
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
/**
|
|
406
|
-
* @PubSub() - marks an API class as fire-and-forget over Cloud Tasks. Every method
|
|
407
|
-
* MUST return Promise<void> (a compile-time contract on the abstract API). The
|
|
408
|
-
* enqueue client and the controller share this one class, exactly like RPC.
|
|
409
|
-
*/
|
|
410
|
-
function PubSub() {
|
|
411
|
-
// webpieces-disable no-any-unknown -- reflect-metadata decorator API requires any
|
|
412
|
-
return (target) => {
|
|
413
|
-
Reflect.defineMetadata(exports.METADATA_KEYS.API_KIND, 'pubsub', target);
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
/**
|
|
417
|
-
* @Queue(name) - override the Cloud Tasks queue name for a @PubSub method. Default
|
|
418
|
-
* (no decorator) is `${ApiClassName}-${methodName}`, matched 1:1 by Terraform.
|
|
419
|
-
*/
|
|
420
|
-
function Queue(name) {
|
|
421
|
-
// webpieces-disable no-any-unknown -- reflect-metadata decorator API requires any
|
|
422
|
-
return (target, propertyKey, _descriptor) => {
|
|
423
|
-
const metadataTarget = typeof target === 'function' ? target : target.constructor;
|
|
424
|
-
const overrides = Reflect.getMetadata(exports.METADATA_KEYS.QUEUE_OVERRIDE, metadataTarget) || {};
|
|
425
|
-
overrides[propertyKey] = name;
|
|
426
|
-
Reflect.defineMetadata(exports.METADATA_KEYS.QUEUE_OVERRIDE, overrides, metadataTarget);
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
/**
|
|
430
|
-
* Get the API kind. Defaults to 'rpc' when neither @Rpc nor @PubSub is present.
|
|
431
|
-
*/
|
|
432
|
-
function getApiKind(apiClass) {
|
|
433
|
-
return Reflect.getMetadata(exports.METADATA_KEYS.API_KIND, apiClass) ?? 'rpc';
|
|
434
|
-
}
|
|
435
|
-
/**
|
|
436
|
-
* Assert the API class is of the expected kind (used by the clients: the RPC
|
|
437
|
-
* client rejects a @PubSub api and vice-versa).
|
|
438
|
-
* @throws Error if the kind doesn't match.
|
|
439
|
-
*/
|
|
440
|
-
function assertApiKind(apiClass, expected) {
|
|
441
|
-
const actual = getApiKind(apiClass);
|
|
442
|
-
if (actual !== expected) {
|
|
443
|
-
const apiName = apiClass.name || 'Unknown';
|
|
444
|
-
throw new Error(`API ${apiName} is @${actual === 'pubsub' ? 'PubSub' : 'Rpc'} but a ` +
|
|
445
|
-
`${expected === 'pubsub' ? '@PubSub (cloud task)' : '@Rpc'} API was required here.`);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
/**
|
|
449
|
-
* Which {@link EndpointKind}s each {@link ApiKind} may declare. A @PubSub contract is delivered
|
|
450
|
-
* asynchronously by definition, so `rpc` is meaningless on it; an @Rpc contract has no queue, so
|
|
451
|
-
* `cloudtasks`/`cron` on it would name a queue/schedule nothing could ever deliver to. `external`
|
|
452
|
-
* is legal on both — a webhook posts synchronously, a push subscription does not.
|
|
453
|
-
*
|
|
454
|
-
* Shared so the wiring-time assert below and the build-time architecture scan enforce ONE rule.
|
|
455
|
-
*/
|
|
456
|
-
exports.ENDPOINT_KINDS_BY_API_KIND = {
|
|
457
|
-
rpc: ['rpc', 'external'],
|
|
458
|
-
pubsub: ['cloudtasks', 'cron', 'external'],
|
|
459
|
-
};
|
|
460
|
-
/**
|
|
461
|
-
* Validate @PubSub conventions at wiring time: the class must be @ApiPath + @PubSub, declare at
|
|
462
|
-
* least one endpoint, and every endpoint must declare a kind this api kind can actually deliver.
|
|
463
|
-
* (Return-type is Promise<void>, a compile-time contract — TS erases types at runtime so it cannot
|
|
464
|
-
* be re-checked here.)
|
|
465
|
-
* @throws Error if conventions are violated.
|
|
466
|
-
*/
|
|
467
|
-
function assertPubSubConventions(apiClass) {
|
|
468
|
-
assertApiKind(apiClass, 'pubsub');
|
|
469
|
-
const apiName = apiClass.name || 'Unknown';
|
|
470
|
-
if (!isApiPath(apiClass)) {
|
|
471
|
-
throw new Error(`@PubSub API ${apiName} must also be decorated with @ApiPath()`);
|
|
472
|
-
}
|
|
473
|
-
const endpoints = getEndpoints(apiClass) || {};
|
|
474
|
-
if (Object.keys(endpoints).length === 0) {
|
|
475
|
-
throw new Error(`@PubSub API ${apiName} declares no @Endpoint methods`);
|
|
476
|
-
}
|
|
477
|
-
const allowed = exports.ENDPOINT_KINDS_BY_API_KIND.pubsub;
|
|
478
|
-
const kinds = getEndpointKinds(apiClass);
|
|
479
|
-
for (const methodName of Object.keys(endpoints)) {
|
|
480
|
-
const kind = kinds[methodName];
|
|
481
|
-
if (kind !== undefined && allowed.includes(kind))
|
|
482
|
-
continue;
|
|
483
|
-
throw new Error(`@PubSub API ${apiName}.${methodName} declares @Endpoint(..., '${kind ?? 'missing'}') — a ` +
|
|
484
|
-
`@PubSub contract is delivered through a queue, so it must be one of: ${allowed.join(' | ')}.`);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
/**
|
|
488
|
-
* Resolve the Cloud Tasks queue name for a @PubSub method: the @Queue override if
|
|
489
|
-
* present, else `${ApiClassName}-${methodName}`.
|
|
490
|
-
*/
|
|
491
|
-
function getQueueName(apiClass, methodName) {
|
|
492
|
-
const overrides = Reflect.getMetadata(exports.METADATA_KEYS.QUEUE_OVERRIDE, apiClass) || {};
|
|
493
|
-
return overrides[methodName] ?? `${apiClass.name || 'Unknown'}-${methodName}`;
|
|
494
|
-
}
|
|
495
419
|
/**
|
|
496
420
|
* Validate that a class/method doesn't have conflicting auth decorators.
|
|
497
421
|
* @throws Error if multiple auth decorators are found on the same target.
|
|
@@ -504,8 +428,8 @@ function validateNoConflictingDecorators(apiClass, methodName) {
|
|
|
504
428
|
const targetName = apiClass.name || 'Unknown';
|
|
505
429
|
const location = methodName ? `method '${methodName}' of ${targetName}` : `class ${targetName}`;
|
|
506
430
|
throw new Error(`Conflicting auth decorator on ${location}. ` +
|
|
507
|
-
`Only one of @Public() / @AuthJwt({...}) / @AuthOidc(...) / @AuthSharedSecret(...) ` +
|
|
508
|
-
|
|
431
|
+
`Only one of @Public() / @AuthJwt({...}) / @AuthOidc(...) / @AuthSharedSecret(...) / ` +
|
|
432
|
+
`@AuthLocalOnly() is allowed per target.`);
|
|
509
433
|
}
|
|
510
434
|
}
|
|
511
435
|
//# sourceMappingURL=decorators.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decorators.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/decorators.ts"],"names":[],"mappings":"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","sourcesContent":["import 'reflect-metadata';\nimport { MaskSpec, MaskMode } from './LogFieldMask';\nimport { DEFAULT_CALLER_KIND, ENDPOINT_CALLER_KEY, ExternalCaller, ExternalSystemKind, getEndpointCaller } from './external-caller';\n\n/**\n * Metadata keys for storing API routing information.\n * These keys are used by both server-side (routing) and client-side (client generation).\n */\nexport const METADATA_KEYS = {\n API_PATH: 'webpieces:api-path',\n ENDPOINTS: 'webpieces:endpoints',\n AUTH_META: 'webpieces:auth-meta',\n /** 'rpc' (default, sync request/response) vs 'pubsub' (fire-and-forget cloud task). */\n API_KIND: 'webpieces:api-kind',\n /** Per-method Cloud Tasks queue-name override (set via @Queue). */\n QUEUE_OVERRIDE: 'webpieces:queue-override',\n /** Per-method @Endpoint options (e.g. formPost), parallel to ENDPOINTS. */\n ENDPOINT_OPTIONS: 'webpieces:endpoint-options',\n /** Per-method @Endpoint trigger kind (rpc | cloudtasks | cron | external), parallel to ENDPOINTS. */\n ENDPOINT_KIND: 'webpieces:endpoint-kind',\n /** Per-method declared external CALLER (only for kind 'external'), parallel to ENDPOINTS. */\n ENDPOINT_CALLER: ENDPOINT_CALLER_KEY,\n /** Per-method @MaskLog spec (which DTO fields the LogApiCall path masks). */\n MASK_LOG: 'webpieces:mask-log',\n};\n\n/**\n * WHAT TRIGGERS an endpoint at runtime — the single fact that decides how the runtime architecture\n * graph draws it, and which Terraform resource must exist for it to ever fire:\n *\n * - `rpc` — a caller in this repo (or a browser) calls it synchronously. A direct arrow.\n * - `cloudtasks` — a producer ENQUEUES it; Cloud Tasks delivers it later. Drawn producer → queue →\n * consumer, one queue node per METHOD (see {@link Queue}). Producer and consumer\n * being the SAME service is legal and common — the queue decouples them.\n * - `cron` — a scheduler fires it on a clock. Nothing in-repo calls it; drawn hanging off a\n * clock symbol. Backed by a Cloud Scheduler job.\n * - `external` — a system OUTSIDE this repo drives it (a GCP Pub/Sub push subscription, a Twilio\n * or Gmail webhook). Drawn as an inbound dashed arrow from that system.\n *\n * Declared PER METHOD, because one api class routinely mixes them: an admin contract can have\n * caller-driven endpoints AND a nightly cron sweep. A class-level marker cannot express that, which\n * is exactly why the graph could not tell these apart before.\n */\nexport type EndpointKind = 'rpc' | 'cloudtasks' | 'cron' | 'external';\n\n/**\n * Options for a single @Endpoint. Kept in a metadata map PARALLEL to ENDPOINTS so the existing\n * `Record<methodName, path>` shape every consumer iterates stays unchanged.\n */\nexport interface EndpointOptions {\n /**\n * Parse the request body as application/x-www-form-urlencoded (flat key→value) instead of JSON.\n * For EXTERNAL webhooks (e.g. Twilio) that post form-encoded. The request DTO must be FLAT —\n * urlencoded has no nesting (unlike JSON). Default false = JSON.\n */\n formPost?: boolean;\n}\n\n/**\n * Options for an `external` @Endpoint: everything {@link EndpointOptions} carries, PLUS a REQUIRED\n * declaration of WHO is calling. See {@link Endpoint} for why, `external-caller.ts` for identity.\n */\nexport interface ExternalEndpointOptions extends EndpointOptions {\n /** The outside system that posts here (`'twilio'`) — the graph node IDENTITY, not display text. */\n calledBy: string;\n /** What that caller IS; picks the node's shape. Defaults to `'saas'` (see DEFAULT_CALLER_KIND). */\n callerKind?: ExternalSystemKind;\n}\n\n/**\n * Route metadata stored per-method at runtime.\n * Used internally by http-routing and http-client as the runtime representation\n * of a route. Constructed from @ApiPath + @Endpoint metadata by ProxyClient\n * and ApiRoutingFactory.\n */\nexport class RouteMetadata {\n httpMethod: string;\n path: string;\n methodName: string;\n controllerClassName?: string;\n authMeta?: AuthMeta;\n /** The API contract class name (e.g. 'SaveApi') — distinct from the controller name. */\n apiName?: string;\n /**\n * True when @Endpoint(..., { formPost: true }): the body is application/x-www-form-urlencoded\n * (flat key→value), not JSON. Rides the route metadata so the per-route body parse can branch\n * without knowing the apiClass/methodName. Default false = JSON.\n */\n readonly formPost: boolean;\n /**\n * The @MaskLog field-mask spec for this route, or undefined when the method declared none. Read\n * ONCE here at route-build time and handed to {@link LogApiCall} via ApiMethodInfo, so the per-call\n * log path pays for masking only on routes that opted in (the rest stay on plain JSON.stringify).\n */\n readonly mask?: MaskSpec;\n\n constructor(\n httpMethod: string,\n path: string,\n methodName: string,\n controllerClassName?: string,\n authMeta?: AuthMeta,\n apiName?: string,\n formPost: boolean = false,\n mask?: MaskSpec,\n ) {\n this.httpMethod = httpMethod;\n this.path = path;\n this.methodName = methodName;\n this.controllerClassName = controllerClassName;\n this.authMeta = authMeta;\n this.apiName = apiName;\n this.formPost = formPost;\n this.mask = mask;\n }\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 * @AuthJwt({ roles: ['admin'] }) // ✅ role-gated (any-of)\n * @AuthJwt({ allRolesAllowed: true }) // ✅ every authenticated user, said out loud\n * @AuthJwt({}) // ❌ pick a branch\n * @AuthJwt({ 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 * `@AuthJwt({ 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 *\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 * 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 */\nexport type AuthMode =\n | { kind: 'public' }\n | { kind: 'jwt'; requirement: JwtRequirement }\n | { kind: 'oidc'; callers: string[] }\n | { kind: 'shared-secret'; secretKey: string };\n\n/**\n * Auth metadata attached to a class or method via one of the auth decorators\n * (@Public / @AuthJwt / @AuthOidc / @AuthSharedSecret) — 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\n/**\n * @ApiPath(basePath) - Class decorator that marks a class as an API definition\n * and sets the base path for all endpoints.\n *\n * Usage:\n * ```typescript\n * @AuthJwt({ roles: ['admin'] })\n * @ApiPath('/api/save')\n * abstract class SaveApi {\n * @Endpoint('/item', 'rpc')\n * save(request: SaveRequest): Promise<SaveResponse> { ... }\n * }\n * ```\n */\nexport function ApiPath(basePath: string): ClassDecorator {\n // webpieces-disable no-any-unknown -- reflect-metadata decorator API requires any\n return (target: any) => {\n Reflect.defineMetadata(METADATA_KEYS.API_PATH, basePath, target);\n\n // Initialize endpoints map if not exists\n if (!Reflect.hasMetadata(METADATA_KEYS.ENDPOINTS, target)) {\n Reflect.defineMetadata(METADATA_KEYS.ENDPOINTS, {}, target);\n }\n };\n}\n\n/**\n * @Endpoint(path, kind, options?) - Method decorator that registers a POST endpoint at the given\n * path and declares WHAT TRIGGERS it.\n *\n * All endpoints are POST-only (matching gRPC/thrift style).\n *\n * Usage:\n * ```typescript\n * @Endpoint('/item', 'rpc')\n * save(request: SaveRequest): Promise<SaveResponse> { ... }\n *\n * // enqueued by a producer, delivered later by Cloud Tasks:\n * @Endpoint('/send', 'cloudtasks')\n * send(request: SendRequest): Promise<void> { ... }\n *\n * // fired by Cloud Scheduler on a clock, called by nobody in this repo:\n * @Endpoint('/nightly', 'cron')\n * nightly(request: NightlyRequest): Promise<void> { ... }\n *\n * // EXTERNAL webhook posting application/x-www-form-urlencoded (e.g. Twilio):\n * @Endpoint('/hook', 'external', { formPost: true, calledBy: 'twilio' })\n * inbound(request: InboundRequest): Promise<InboundResponse> { ... }\n * ```\n *\n * `kind` is REQUIRED and deliberately positional: it makes every pre-existing single-argument\n * `@Endpoint('/x')` a COMPILE error rather than something a lint rule has to chase, so no endpoint\n * can slip into the runtime architecture graph with its trigger left to guesswork. See\n * {@link EndpointKind} for what each value draws and which Terraform resource backs it.\n *\n * `calledBy` is REQUIRED for `external` FOR EXACTLY THE SAME REASON, enforced by the overloads below:\n * the one box on the runtime graph whose whole job is to say who calls us from outside could only\n * restate OUR OWN contract name, because nothing in the source ever said who the caller was. This is\n * BREAKING for published consumers, intentionally — an existing `@Endpoint(p, 'external', {...})`\n * stops compiling until it names its caller. Migration is one property; see the migration note in\n * `external-caller.ts`. Non-`external` endpoints are completely unaffected.\n *\n * The path write to ENDPOINTS is UNCHANGED (every consumer iterates `[methodName, path]`); kind,\n * options and caller ride PARALLEL ENDPOINT_KIND / ENDPOINT_OPTIONS / ENDPOINT_CALLER maps.\n */\n// webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope\nexport function Endpoint(path: string, kind: 'external', options: ExternalEndpointOptions): MethodDecorator;\n// webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope\nexport function Endpoint(path: string, kind: Exclude<EndpointKind, 'external'>, options?: EndpointOptions): MethodDecorator;\n// webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope\nexport function Endpoint(path: string, kind: EndpointKind, options: EndpointOptions = {}): MethodDecorator {\n // webpieces-disable no-any-unknown -- reflect-metadata decorator API requires any\n return (target: any, propertyKey: string | symbol, _descriptor: PropertyDescriptor) => {\n const metadataTarget = typeof target === 'function' ? target : target.constructor;\n\n const endpoints: Record<string, string> =\n Reflect.getMetadata(METADATA_KEYS.ENDPOINTS, metadataTarget) || {};\n\n endpoints[propertyKey as string] = path;\n\n Reflect.defineMetadata(METADATA_KEYS.ENDPOINTS, endpoints, metadataTarget);\n\n const kinds: Record<string, EndpointKind> =\n Reflect.getMetadata(METADATA_KEYS.ENDPOINT_KIND, metadataTarget) || {};\n kinds[propertyKey as string] = kind;\n Reflect.defineMetadata(METADATA_KEYS.ENDPOINT_KIND, kinds, metadataTarget);\n\n const opts: Record<string, EndpointOptions> =\n Reflect.getMetadata(METADATA_KEYS.ENDPOINT_OPTIONS, metadataTarget) || {};\n opts[propertyKey as string] = options;\n Reflect.defineMetadata(METADATA_KEYS.ENDPOINT_OPTIONS, opts, metadataTarget);\n\n // ONLY for 'external', mirroring how a queue name is recorded only for the kinds that HAVE\n // a queue: a caller on an rpc endpoint would be a fact about nothing.\n const declared = options as ExternalEndpointOptions;\n if (kind !== 'external' || typeof declared.calledBy !== 'string' || declared.calledBy === '') return;\n const callers: Record<string, ExternalCaller> = Reflect.getMetadata(METADATA_KEYS.ENDPOINT_CALLER, metadataTarget) || {};\n callers[propertyKey as string] = new ExternalCaller(declared.callerKind ?? DEFAULT_CALLER_KIND, declared.calledBy);\n Reflect.defineMetadata(METADATA_KEYS.ENDPOINT_CALLER, callers, metadataTarget);\n };\n}\n\n/**\n * @MaskLog(fields) - declare which fields of THIS method's request/response DTOs the\n * {@link LogApiCall} logging path must mask, so a secret riding on a DTO (an OAuth refresh token, an\n * id-token JWT) is never written to the logs in cleartext. The REAL value still travels on the wire\n * untouched — masking lives in the logging path only.\n *\n * ```typescript\n * @Endpoint('/account', 'rpc')\n * @MaskLog({ refreshToken: 'full', accessToken: 'last4', credential: 'full' })\n * getEmailAccount(request: GetEmailAccountRequest): Promise<GetEmailAccountResponse> { ... }\n * ```\n *\n * Matching is by field NAME at any depth (nested objects + array elements), so\n * `response.account.refreshToken` is masked. Declared on the SHARED api contract, so BOTH the client\n * `[API-client-*]` and server `[API-server-*]` lines mask it. The spec is read ONCE at route-build\n * time and rides {@link RouteMetadata.mask}, so an unmasked method pays nothing at call time.\n */\n// webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope\nexport function MaskLog(fields: Record<string, MaskMode>): MethodDecorator {\n const spec = new MaskSpec(fields);\n // webpieces-disable no-any-unknown -- reflect-metadata decorator API requires any\n return (target: any, propertyKey: string | symbol, _descriptor: PropertyDescriptor) => {\n const metadataTarget = typeof target === 'function' ? target : target.constructor;\n const specs: Record<string, MaskSpec> =\n Reflect.getMetadata(METADATA_KEYS.MASK_LOG, metadataTarget) || {};\n specs[propertyKey as string] = spec;\n Reflect.defineMetadata(METADATA_KEYS.MASK_LOG, specs, metadataTarget);\n };\n}\n\n/**\n * The @MaskLog spec for one method, or undefined if the method declared none (the common case — the\n * caller then logs the DTO verbatim on the plain JSON.stringify fast path).\n */\n// webpieces-disable no-function-outside-class -- reflect-metadata reader, sibling of getEndpointOptions\nexport function getMaskSpec(apiClass: Function, methodName: string): MaskSpec | undefined {\n const specs: Record<string, MaskSpec> =\n Reflect.getMetadata(METADATA_KEYS.MASK_LOG, apiClass) || {};\n return specs[methodName];\n}\n\n/**\n * Shared implementation for every auth decorator: stores an {@link AuthMeta} for\n * the given {@link AuthMode} at class- or method-level, rejecting a second auth\n * decorator on the same target.\n */\nfunction defineAuthMode(mode: AuthMode): ClassDecorator & MethodDecorator {\n const authMeta = new AuthMeta(mode);\n\n // webpieces-disable no-any-unknown -- reflect-metadata decorator API requires any\n return (target: any, propertyKey?: string | symbol, _descriptor?: PropertyDescriptor) => {\n if (propertyKey !== undefined) {\n // Method decorator\n const metadataTarget = typeof target === 'function' ? target : target.constructor;\n validateNoConflictingDecorators(metadataTarget, propertyKey as string);\n Reflect.defineMetadata(METADATA_KEYS.AUTH_META, authMeta, metadataTarget, propertyKey);\n } else {\n // Class decorator\n validateNoConflictingDecorators(target, undefined);\n Reflect.defineMetadata(METADATA_KEYS.AUTH_META, authMeta, target);\n }\n };\n}\n\n/**\n * @Public() - endpoint requires no authentication. Class- or method-level.\n */\nexport function Public(): ClassDecorator & MethodDecorator {\n return defineAuthMode({ kind: 'public' });\n}\n\n/**\n * @AuthJwt(requirement) - THE user-facing JWT decorator, covering the whole user-JWT axis: the\n * compiler-enforced role decision ({@link JwtRoles}) plus app-defined fields ({@link JwtRequirement}).\n *\n * ```typescript\n * @AuthJwt({ roles: ['admin', 'editor'] }) // any-of\n * @AuthJwt({ allRolesAllowed: true, inOrg: true }) // wide + an app rule enforced by authorizeJwt\n * ```\n *\n * It absorbed the former `@Auth(requirement)` — same argument, same AuthMode, so two spellings of one\n * decision. One decorator per credential kind now: `@Public` / `@AuthJwt` / `@AuthOidc` /\n * `@AuthSharedSecret`.\n */\n// webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope\nexport function AuthJwt(requirement: JwtRequirement): ClassDecorator & MethodDecorator {\n return defineAuthMode({ kind: 'jwt', requirement });\n}\n\n/**\n * The roles an endpoint accepts, or [] when it accepts every authenticated user. The ONE reader of\n * the {@link JwtRoles} union, so no caller has to re-derive \"does absent mean wide?\" — a question\n * whose two plausible answers is how the widest grant kept hiding behind an absent field.\n */\n// webpieces-disable no-function-outside-class -- reflect-metadata reader, sibling of getAuthMode\nexport function rolesRequired(requirement: JwtRequirement): readonly string[] {\n return requirement.allRolesAllowed === true ? [] : requirement.roles;\n}\n\n/**\n * @AuthOidc(...callers) - Google OIDC service-to-service auth (Cloud Tasks delivery / cross-service\n * RPC). `callers` is an OPTIONAL app-level allow-list of caller service accounts.\n *\n * NO args = TRUST THE EDGE: accept any genuine Google-signed OIDC caller, because a PRIVATE Cloud\n * Run service's edge already gates WHO via `run.invoker` IAM (managed in terraform — one source of\n * truth, no hand-synced list in code). If the service is actually PUBLIC, the verifier logs a loud\n * warning (it can't be the gate then). Pass explicit SAs (`@AuthOidc('svc-a')`) only when you want\n * an additional app-level allow-list as defense-in-depth.\n */\nexport function AuthOidc(...callers: string[]): ClassDecorator & MethodDecorator {\n return defineAuthMode({ kind: 'oidc', callers });\n}\n\n/**\n * @AuthSharedSecret(key) - constant-time compare of an inbound header against the secret bound for\n * `key`. `key` is a LOOKUP KEY (not an env var): the server looks up its accepted {@link SharedSecrets}\n * by this key, and each client looks up the value it sends by the SAME key (see {@link Secrets}).\n * For internal callers that cannot mint OIDC tokens.\n */\nexport function AuthSharedSecret(key: string): ClassDecorator & MethodDecorator {\n return defineAuthMode({ kind: 'shared-secret', secretKey: key });\n}\n\n// ============================================================\n// Helper functions\n// ============================================================\n\n/**\n * Get the base path from @ApiPath decorator.\n */\nexport function getApiPath(apiClass: Function): string | undefined {\n return Reflect.getMetadata(METADATA_KEYS.API_PATH, apiClass);\n}\n\n/**\n * Get all endpoints from @Endpoint decorators.\n * Returns a record of methodName -> endpoint path.\n */\nexport function getEndpoints(apiClass: Function): Record<string, string> | undefined {\n return Reflect.getMetadata(METADATA_KEYS.ENDPOINTS, apiClass);\n}\n\n/**\n * Every method's declared trigger kind, as `methodName -> kind`. Parallel to {@link getEndpoints}.\n * Empty for a class carrying no @Endpoint at all.\n */\n// webpieces-disable no-function-outside-class -- reflect-metadata reader, sibling of getEndpoints\nexport function getEndpointKinds(apiClass: Function): Record<string, EndpointKind> {\n return Reflect.getMetadata(METADATA_KEYS.ENDPOINT_KIND, apiClass) || {};\n}\n\n/**\n * What triggers ONE method, or undefined when the method carries no @Endpoint.\n *\n * Defaults to nothing rather than to 'rpc': `kind` is a required argument, so a missing entry means\n * \"this is not an endpoint\", never \"an endpoint that forgot to say\". Silently defaulting here would\n * put an undeclared cron or webhook back into the graph as a normal rpc call — the exact blindness\n * the required argument exists to remove.\n */\n// webpieces-disable no-function-outside-class -- reflect-metadata reader, sibling of getEndpoints\nexport function getEndpointKind(apiClass: Function, methodName: string): EndpointKind | undefined {\n return getEndpointKinds(apiClass)[methodName];\n}\n\n/**\n * Get the @Endpoint options for one method (empty object if the method had no options).\n */\n// webpieces-disable no-function-outside-class -- reflect-metadata reader, sibling of getEndpoints\nexport function getEndpointOptions(apiClass: Function, methodName: string): EndpointOptions {\n const opts: Record<string, EndpointOptions> =\n Reflect.getMetadata(METADATA_KEYS.ENDPOINT_OPTIONS, apiClass) || {};\n return opts[methodName] ?? {};\n}\n\n/**\n * Fail-fast at wiring time when an `external` endpoint declared no caller. The {@link Endpoint}\n * overloads already make that a COMPILE error; this is the backstop for the ways TS is bypassed —\n * a JS caller, an `as any` options object, a hand-rolled Reflect.defineMetadata.\n * @throws Error naming the first external endpoint with no `calledBy`.\n */\n// webpieces-disable no-function-outside-class -- wiring-time assert, sibling of assertEveryEndpointHasAuthMode\nexport function assertEveryExternalEndpointDeclaresCaller(apiClass: Function): void {\n const kinds = getEndpointKinds(apiClass);\n for (const methodName of Object.keys(kinds)) {\n if (kinds[methodName] !== 'external' || getEndpointCaller(apiClass, methodName) !== undefined) continue;\n throw new Error(\n `External endpoint '${methodName}' in ${apiClass.name || 'Unknown'} declares no caller. Say WHO ` +\n `posts to it: @Endpoint(path, 'external', { calledBy: '<vendor>' }) — the runtime architecture ` +\n `graph cannot name an inbound caller it was never told about.`,\n );\n }\n}\n\n/**\n * True when the method's @Endpoint declared `{ formPost: true }` — its body is\n * application/x-www-form-urlencoded (flat), not JSON.\n */\n// webpieces-disable no-function-outside-class -- reflect-metadata reader, sibling of getEndpoints\nexport function isFormPost(apiClass: Function, methodName: string): boolean {\n return getEndpointOptions(apiClass, methodName).formPost === true;\n}\n\n/**\n * Check if a class has @ApiPath decorator.\n */\nexport function isApiPath(apiClass: Function): boolean {\n return Reflect.hasMetadata(METADATA_KEYS.API_PATH, apiClass);\n}\n\n/**\n * Get auth metadata for a specific method, falling back to class-level auth.\n * Method-level auth takes precedence over class-level auth.\n */\nexport function getAuthMeta(apiClass: Function, methodName?: string): AuthMeta | undefined {\n // Check method-level first\n if (methodName) {\n const methodAuth = Reflect.getMetadata(METADATA_KEYS.AUTH_META, apiClass, methodName);\n if (methodAuth) {\n return methodAuth;\n }\n }\n\n // Fall back to class-level\n return Reflect.getMetadata(METADATA_KEYS.AUTH_META, apiClass);\n}\n\n/**\n * Get the auth mode for a method (falling back to class-level), or undefined.\n * Convenience wrapper over getAuthMeta for callers that only want the mode.\n */\nexport function getAuthMode(apiClass: Function, methodName?: string): AuthMode | undefined {\n return getAuthMeta(apiClass, methodName)?.mode;\n}\n\n/**\n * The ONE prescription for \"this endpoint declares no auth\", shared by the two places that raise it\n * (here and http-routing's ApiRoutingFactory) because they had drifted into teaching different menus.\n * A message teaching an incomplete API is the same defect as an API with two spellings: whichever menu\n * the caller hits becomes the API they believe exists. It leads with the ROLE-GATED member on purpose —\n * the first thing offered should not be the widest grant.\n */\nexport const MISSING_AUTH_DECORATOR_FIX =\n \"Add one of @AuthJwt({roles: ['admin']}) / @AuthJwt({allRolesAllowed: true}) / @Public() / \" +\n '@AuthOidc(...callers) / @AuthSharedSecret(key) to the class or method.';\n\n/**\n * Fail-fast at wiring time if any endpoint lacks an auth mode. Both the server\n * (ApiRoutingFactory) and the task/rpc clients call this so a missing auth\n * decorator is a startup error, never a silent open endpoint.\n * @throws Error naming the first endpoint with no auth decorator, via {@link MISSING_AUTH_DECORATOR_FIX}.\n */\nexport function assertEveryEndpointHasAuthMode(apiClass: Function): void {\n const apiName = apiClass.name || 'Unknown';\n const endpoints = getEndpoints(apiClass) || {};\n for (const methodName of Object.keys(endpoints)) {\n if (!getAuthMeta(apiClass, methodName)) {\n throw new Error(\n `Endpoint '${methodName}' in ${apiName} has no auth decorator. ` +\n MISSING_AUTH_DECORATOR_FIX,\n );\n }\n }\n}\n\n// ============================================================\n// API kind (RPC vs PubSub/Cloud Tasks) + queue naming\n// ============================================================\n\n/**\n * API kind. 'rpc' = synchronous request/response (http-client ↔ ApiRoutingFactory).\n * 'pubsub' = fire-and-forget cloud task; the enqueue client (cloudtasks-client)\n * schedules a Cloud Task that is later delivered to the SAME controller endpoint.\n */\nexport type ApiKind = 'rpc' | 'pubsub';\n\n/**\n * @Rpc() - marks an API class as synchronous request/response (the default kind).\n * Present mostly for symmetry/readability; an undecorated API is treated as 'rpc'.\n */\nexport function Rpc(): ClassDecorator {\n // webpieces-disable no-any-unknown -- reflect-metadata decorator API requires any\n return (target: any) => {\n Reflect.defineMetadata(METADATA_KEYS.API_KIND, 'rpc' as ApiKind, target);\n };\n}\n\n/**\n * @PubSub() - marks an API class as fire-and-forget over Cloud Tasks. Every method\n * MUST return Promise<void> (a compile-time contract on the abstract API). The\n * enqueue client and the controller share this one class, exactly like RPC.\n */\nexport function PubSub(): ClassDecorator {\n // webpieces-disable no-any-unknown -- reflect-metadata decorator API requires any\n return (target: any) => {\n Reflect.defineMetadata(METADATA_KEYS.API_KIND, 'pubsub' as ApiKind, target);\n };\n}\n\n/**\n * @Queue(name) - override the Cloud Tasks queue name for a @PubSub method. Default\n * (no decorator) is `${ApiClassName}-${methodName}`, matched 1:1 by Terraform.\n */\nexport function Queue(name: string): MethodDecorator {\n // webpieces-disable no-any-unknown -- reflect-metadata decorator API requires any\n return (target: any, propertyKey: string | symbol, _descriptor: PropertyDescriptor) => {\n const metadataTarget = typeof target === 'function' ? target : target.constructor;\n const overrides: Record<string, string> =\n Reflect.getMetadata(METADATA_KEYS.QUEUE_OVERRIDE, metadataTarget) || {};\n overrides[propertyKey as string] = name;\n Reflect.defineMetadata(METADATA_KEYS.QUEUE_OVERRIDE, overrides, metadataTarget);\n };\n}\n\n/**\n * Get the API kind. Defaults to 'rpc' when neither @Rpc nor @PubSub is present.\n */\nexport function getApiKind(apiClass: Function): ApiKind {\n return (Reflect.getMetadata(METADATA_KEYS.API_KIND, apiClass) as ApiKind) ?? 'rpc';\n}\n\n/**\n * Assert the API class is of the expected kind (used by the clients: the RPC\n * client rejects a @PubSub api and vice-versa).\n * @throws Error if the kind doesn't match.\n */\nexport function assertApiKind(apiClass: Function, expected: ApiKind): void {\n const actual = getApiKind(apiClass);\n if (actual !== expected) {\n const apiName = apiClass.name || 'Unknown';\n throw new Error(\n `API ${apiName} is @${actual === 'pubsub' ? 'PubSub' : 'Rpc'} but a ` +\n `${expected === 'pubsub' ? '@PubSub (cloud task)' : '@Rpc'} API was required here.`,\n );\n }\n}\n\n/**\n * Which {@link EndpointKind}s each {@link ApiKind} may declare. A @PubSub contract is delivered\n * asynchronously by definition, so `rpc` is meaningless on it; an @Rpc contract has no queue, so\n * `cloudtasks`/`cron` on it would name a queue/schedule nothing could ever deliver to. `external`\n * is legal on both — a webhook posts synchronously, a push subscription does not.\n *\n * Shared so the wiring-time assert below and the build-time architecture scan enforce ONE rule.\n */\nexport const ENDPOINT_KINDS_BY_API_KIND: Record<ApiKind, readonly EndpointKind[]> = {\n rpc: ['rpc', 'external'],\n pubsub: ['cloudtasks', 'cron', 'external'],\n};\n\n/**\n * Validate @PubSub conventions at wiring time: the class must be @ApiPath + @PubSub, declare at\n * least one endpoint, and every endpoint must declare a kind this api kind can actually deliver.\n * (Return-type is Promise<void>, a compile-time contract — TS erases types at runtime so it cannot\n * be re-checked here.)\n * @throws Error if conventions are violated.\n */\nexport function assertPubSubConventions(apiClass: Function): void {\n assertApiKind(apiClass, 'pubsub');\n const apiName = apiClass.name || 'Unknown';\n if (!isApiPath(apiClass)) {\n throw new Error(`@PubSub API ${apiName} must also be decorated with @ApiPath()`);\n }\n const endpoints = getEndpoints(apiClass) || {};\n if (Object.keys(endpoints).length === 0) {\n throw new Error(`@PubSub API ${apiName} declares no @Endpoint methods`);\n }\n const allowed = ENDPOINT_KINDS_BY_API_KIND.pubsub;\n const kinds = getEndpointKinds(apiClass);\n for (const methodName of Object.keys(endpoints)) {\n const kind = kinds[methodName];\n if (kind !== undefined && allowed.includes(kind)) continue;\n throw new Error(\n `@PubSub API ${apiName}.${methodName} declares @Endpoint(..., '${kind ?? 'missing'}') — a ` +\n `@PubSub contract is delivered through a queue, so it must be one of: ${allowed.join(' | ')}.`,\n );\n }\n}\n\n/**\n * Resolve the Cloud Tasks queue name for a @PubSub method: the @Queue override if\n * present, else `${ApiClassName}-${methodName}`.\n */\nexport function getQueueName(apiClass: Function, methodName: string): string {\n const overrides: Record<string, string> =\n Reflect.getMetadata(METADATA_KEYS.QUEUE_OVERRIDE, apiClass) || {};\n return overrides[methodName] ?? `${apiClass.name || 'Unknown'}-${methodName}`;\n}\n\n/**\n * Validate that a class/method doesn't have conflicting auth decorators.\n * @throws Error if multiple auth decorators are found on the same target.\n */\nexport function validateNoConflictingDecorators(apiClass: Function, methodName: string | undefined): void {\n const existing = methodName\n ? Reflect.getMetadata(METADATA_KEYS.AUTH_META, apiClass, methodName)\n : Reflect.getMetadata(METADATA_KEYS.AUTH_META, apiClass);\n\n if (existing) {\n const targetName = apiClass.name || 'Unknown';\n const location = methodName ? `method '${methodName}' of ${targetName}` : `class ${targetName}`;\n throw new Error(\n `Conflicting auth decorator on ${location}. ` +\n `Only one of @Public() / @AuthJwt({...}) / @AuthOidc(...) / @AuthSharedSecret(...) ` +\n `is allowed per target.`\n );\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"decorators.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/decorators.ts"],"names":[],"mappings":"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","sourcesContent":["import 'reflect-metadata';\nimport { MaskSpec, MaskMode } from './LogFieldMask';\nimport { DEFAULT_CALLER_KIND, ENDPOINT_CALLER_KEY, ExternalCaller, ExternalSystemKind, getEndpointCaller } from './external-caller';\n\n/**\n * Metadata keys for storing API routing information.\n * These keys are used by both server-side (routing) and client-side (client generation).\n */\nexport const METADATA_KEYS = {\n API_PATH: 'webpieces:api-path',\n ENDPOINTS: 'webpieces:endpoints',\n AUTH_META: 'webpieces:auth-meta',\n /** 'rpc' (default, sync request/response) vs 'pubsub' (fire-and-forget cloud task). */\n API_KIND: 'webpieces:api-kind',\n /** Per-method Cloud Tasks queue-name override (set via @Queue). */\n QUEUE_OVERRIDE: 'webpieces:queue-override',\n /** Per-method @Endpoint options (e.g. formPost), parallel to ENDPOINTS. */\n ENDPOINT_OPTIONS: 'webpieces:endpoint-options',\n /** Per-method @Endpoint trigger kind (rpc | cloudtasks | cron | external), parallel to ENDPOINTS. */\n ENDPOINT_KIND: 'webpieces:endpoint-kind',\n /** Per-method declared external CALLER (only for kind 'external'), parallel to ENDPOINTS. */\n ENDPOINT_CALLER: ENDPOINT_CALLER_KEY,\n /** Per-method @MaskLog spec (which DTO fields the LogApiCall path masks). */\n MASK_LOG: 'webpieces:mask-log',\n};\n\n/**\n * WHAT TRIGGERS an endpoint at runtime — the single fact that decides how the runtime architecture\n * graph draws it, and which Terraform resource must exist for it to ever fire:\n *\n * - `rpc` — a caller in this repo (or a browser) calls it synchronously. A direct arrow.\n * - `cloudtasks` — a producer ENQUEUES it; Cloud Tasks delivers it later. Drawn producer → queue →\n * consumer, one queue node per METHOD (see {@link Queue}). Producer and consumer\n * being the SAME service is legal and common — the queue decouples them.\n * - `cron` — a scheduler fires it on a clock. Nothing in-repo calls it; drawn hanging off a\n * clock symbol. Backed by a Cloud Scheduler job.\n * - `external` — a system OUTSIDE this repo drives it (a GCP Pub/Sub push subscription, a Twilio\n * or Gmail webhook). Drawn as an inbound dashed arrow from that system.\n *\n * Declared PER METHOD, because one api class routinely mixes them: an admin contract can have\n * caller-driven endpoints AND a nightly cron sweep. A class-level marker cannot express that, which\n * is exactly why the graph could not tell these apart before.\n */\nexport type EndpointKind = 'rpc' | 'cloudtasks' | 'cron' | 'external';\n\n/**\n * Options for a single @Endpoint. Kept in a metadata map PARALLEL to ENDPOINTS so the existing\n * `Record<methodName, path>` shape every consumer iterates stays unchanged.\n */\nexport interface EndpointOptions {\n /**\n * Parse the request body as application/x-www-form-urlencoded (flat key→value) instead of JSON.\n * For EXTERNAL webhooks (e.g. Twilio) that post form-encoded. The request DTO must be FLAT —\n * urlencoded has no nesting (unlike JSON). Default false = JSON.\n */\n formPost?: boolean;\n}\n\n/**\n * Options for an `external` @Endpoint: everything {@link EndpointOptions} carries, PLUS a REQUIRED\n * declaration of WHO is calling. See {@link Endpoint} for why, `external-caller.ts` for identity.\n */\nexport interface ExternalEndpointOptions extends EndpointOptions {\n /** The outside system that posts here (`'twilio'`) — the graph node IDENTITY, not display text. */\n calledBy: string;\n /** What that caller IS; picks the node's shape. Defaults to `'saas'` (see DEFAULT_CALLER_KIND). */\n callerKind?: ExternalSystemKind;\n}\n\n/**\n * Route metadata stored per-method at runtime.\n * Used internally by http-routing and http-client as the runtime representation\n * of a route. Constructed from @ApiPath + @Endpoint metadata by ProxyClient\n * and ApiRoutingFactory.\n */\nexport class RouteMetadata {\n httpMethod: string;\n path: string;\n methodName: string;\n controllerClassName?: string;\n authMeta?: AuthMeta;\n /** The API contract class name (e.g. 'SaveApi') — distinct from the controller name. */\n apiName?: string;\n /**\n * True when @Endpoint(..., { formPost: true }): the body is application/x-www-form-urlencoded\n * (flat key→value), not JSON. Rides the route metadata so the per-route body parse can branch\n * without knowing the apiClass/methodName. Default false = JSON.\n */\n readonly formPost: boolean;\n /**\n * The @MaskLog field-mask spec for this route, or undefined when the method declared none. Read\n * ONCE here at route-build time and handed to {@link LogApiCall} via ApiMethodInfo, so the per-call\n * log path pays for masking only on routes that opted in (the rest stay on plain JSON.stringify).\n */\n readonly mask?: MaskSpec;\n\n constructor(\n httpMethod: string,\n path: string,\n methodName: string,\n controllerClassName?: string,\n authMeta?: AuthMeta,\n apiName?: string,\n formPost: boolean = false,\n mask?: MaskSpec,\n ) {\n this.httpMethod = httpMethod;\n this.path = path;\n this.methodName = methodName;\n this.controllerClassName = controllerClassName;\n this.authMeta = authMeta;\n this.apiName = apiName;\n this.formPost = formPost;\n this.mask = mask;\n }\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 * @AuthJwt({ roles: ['admin'] }) // ✅ role-gated (any-of)\n * @AuthJwt({ allRolesAllowed: true }) // ✅ every authenticated user, said out loud\n * @AuthJwt({}) // ❌ pick a branch\n * @AuthJwt({ 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 * `@AuthJwt({ 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 *\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 * 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 * - `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: 'local-only' };\n\n/**\n * Auth metadata attached to a class or method via one of the auth decorators\n * (@Public / @AuthJwt / @AuthOidc / @AuthSharedSecret / @AuthLocalOnly) — 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\n/**\n * @ApiPath(basePath) - Class decorator that marks a class as an API definition\n * and sets the base path for all endpoints.\n *\n * Usage:\n * ```typescript\n * @AuthJwt({ roles: ['admin'] })\n * @ApiPath('/api/save')\n * abstract class SaveApi {\n * @Endpoint('/item', 'rpc')\n * save(request: SaveRequest): Promise<SaveResponse> { ... }\n * }\n * ```\n */\nexport function ApiPath(basePath: string): ClassDecorator {\n // webpieces-disable no-any-unknown -- reflect-metadata decorator API requires any\n return (target: any) => {\n Reflect.defineMetadata(METADATA_KEYS.API_PATH, basePath, target);\n\n // Initialize endpoints map if not exists\n if (!Reflect.hasMetadata(METADATA_KEYS.ENDPOINTS, target)) {\n Reflect.defineMetadata(METADATA_KEYS.ENDPOINTS, {}, target);\n }\n };\n}\n\n/**\n * @Endpoint(path, kind, options?) - Method decorator that registers a POST endpoint at the given\n * path and declares WHAT TRIGGERS it.\n *\n * All endpoints are POST-only (matching gRPC/thrift style).\n *\n * Usage:\n * ```typescript\n * @Endpoint('/item', 'rpc')\n * save(request: SaveRequest): Promise<SaveResponse> { ... }\n *\n * // enqueued by a producer, delivered later by Cloud Tasks:\n * @Endpoint('/send', 'cloudtasks')\n * send(request: SendRequest): Promise<void> { ... }\n *\n * // fired by Cloud Scheduler on a clock, called by nobody in this repo:\n * @Endpoint('/nightly', 'cron')\n * nightly(request: NightlyRequest): Promise<void> { ... }\n *\n * // EXTERNAL webhook posting application/x-www-form-urlencoded (e.g. Twilio):\n * @Endpoint('/hook', 'external', { formPost: true, calledBy: 'twilio' })\n * inbound(request: InboundRequest): Promise<InboundResponse> { ... }\n * ```\n *\n * `kind` is REQUIRED and deliberately positional: it makes every pre-existing single-argument\n * `@Endpoint('/x')` a COMPILE error rather than something a lint rule has to chase, so no endpoint\n * can slip into the runtime architecture graph with its trigger left to guesswork. See\n * {@link EndpointKind} for what each value draws and which Terraform resource backs it.\n *\n * `calledBy` is REQUIRED for `external` FOR EXACTLY THE SAME REASON, enforced by the overloads below:\n * the one box on the runtime graph whose whole job is to say who calls us from outside could only\n * restate OUR OWN contract name, because nothing in the source ever said who the caller was. This is\n * BREAKING for published consumers, intentionally — an existing `@Endpoint(p, 'external', {...})`\n * stops compiling until it names its caller. Migration is one property; see the migration note in\n * `external-caller.ts`. Non-`external` endpoints are completely unaffected.\n *\n * The path write to ENDPOINTS is UNCHANGED (every consumer iterates `[methodName, path]`); kind,\n * options and caller ride PARALLEL ENDPOINT_KIND / ENDPOINT_OPTIONS / ENDPOINT_CALLER maps.\n */\n// webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope\nexport function Endpoint(path: string, kind: 'external', options: ExternalEndpointOptions): MethodDecorator;\n// webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope\nexport function Endpoint(path: string, kind: Exclude<EndpointKind, 'external'>, options?: EndpointOptions): MethodDecorator;\n// webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope\nexport function Endpoint(path: string, kind: EndpointKind, options: EndpointOptions = {}): MethodDecorator {\n // webpieces-disable no-any-unknown -- reflect-metadata decorator API requires any\n return (target: any, propertyKey: string | symbol, _descriptor: PropertyDescriptor) => {\n const metadataTarget = typeof target === 'function' ? target : target.constructor;\n\n const endpoints: Record<string, string> =\n Reflect.getMetadata(METADATA_KEYS.ENDPOINTS, metadataTarget) || {};\n\n endpoints[propertyKey as string] = path;\n\n Reflect.defineMetadata(METADATA_KEYS.ENDPOINTS, endpoints, metadataTarget);\n\n const kinds: Record<string, EndpointKind> =\n Reflect.getMetadata(METADATA_KEYS.ENDPOINT_KIND, metadataTarget) || {};\n kinds[propertyKey as string] = kind;\n Reflect.defineMetadata(METADATA_KEYS.ENDPOINT_KIND, kinds, metadataTarget);\n\n const opts: Record<string, EndpointOptions> =\n Reflect.getMetadata(METADATA_KEYS.ENDPOINT_OPTIONS, metadataTarget) || {};\n opts[propertyKey as string] = options;\n Reflect.defineMetadata(METADATA_KEYS.ENDPOINT_OPTIONS, opts, metadataTarget);\n\n // ONLY for 'external', mirroring how a queue name is recorded only for the kinds that HAVE\n // a queue: a caller on an rpc endpoint would be a fact about nothing.\n const declared = options as ExternalEndpointOptions;\n if (kind !== 'external' || typeof declared.calledBy !== 'string' || declared.calledBy === '') return;\n const callers: Record<string, ExternalCaller> = Reflect.getMetadata(METADATA_KEYS.ENDPOINT_CALLER, metadataTarget) || {};\n callers[propertyKey as string] = new ExternalCaller(declared.callerKind ?? DEFAULT_CALLER_KIND, declared.calledBy);\n Reflect.defineMetadata(METADATA_KEYS.ENDPOINT_CALLER, callers, metadataTarget);\n };\n}\n\n/**\n * @MaskLog(fields) - declare which fields of THIS method's request/response DTOs the\n * {@link LogApiCall} logging path must mask, so a secret riding on a DTO (an OAuth refresh token, an\n * id-token JWT) is never written to the logs in cleartext. The REAL value still travels on the wire\n * untouched — masking lives in the logging path only.\n *\n * ```typescript\n * @Endpoint('/account', 'rpc')\n * @MaskLog({ refreshToken: 'full', accessToken: 'last4', credential: 'full' })\n * getEmailAccount(request: GetEmailAccountRequest): Promise<GetEmailAccountResponse> { ... }\n * ```\n *\n * Matching is by field NAME at any depth (nested objects + array elements), so\n * `response.account.refreshToken` is masked. Declared on the SHARED api contract, so BOTH the client\n * `[API-client-*]` and server `[API-server-*]` lines mask it. The spec is read ONCE at route-build\n * time and rides {@link RouteMetadata.mask}, so an unmasked method pays nothing at call time.\n */\n// webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope\nexport function MaskLog(fields: Record<string, MaskMode>): MethodDecorator {\n const spec = new MaskSpec(fields);\n // webpieces-disable no-any-unknown -- reflect-metadata decorator API requires any\n return (target: any, propertyKey: string | symbol, _descriptor: PropertyDescriptor) => {\n const metadataTarget = typeof target === 'function' ? target : target.constructor;\n const specs: Record<string, MaskSpec> =\n Reflect.getMetadata(METADATA_KEYS.MASK_LOG, metadataTarget) || {};\n specs[propertyKey as string] = spec;\n Reflect.defineMetadata(METADATA_KEYS.MASK_LOG, specs, metadataTarget);\n };\n}\n\n/**\n * The @MaskLog spec for one method, or undefined if the method declared none (the common case — the\n * caller then logs the DTO verbatim on the plain JSON.stringify fast path).\n */\n// webpieces-disable no-function-outside-class -- reflect-metadata reader, sibling of getEndpointOptions\nexport function getMaskSpec(apiClass: Function, methodName: string): MaskSpec | undefined {\n const specs: Record<string, MaskSpec> =\n Reflect.getMetadata(METADATA_KEYS.MASK_LOG, apiClass) || {};\n return specs[methodName];\n}\n\n/**\n * Shared implementation for every auth decorator: stores an {@link AuthMeta} for\n * the given {@link AuthMode} at class- or method-level, rejecting a second auth\n * decorator on the same target.\n */\nfunction defineAuthMode(mode: AuthMode): ClassDecorator & MethodDecorator {\n const authMeta = new AuthMeta(mode);\n\n // webpieces-disable no-any-unknown -- reflect-metadata decorator API requires any\n return (target: any, propertyKey?: string | symbol, _descriptor?: PropertyDescriptor) => {\n if (propertyKey !== undefined) {\n // Method decorator\n const metadataTarget = typeof target === 'function' ? target : target.constructor;\n validateNoConflictingDecorators(metadataTarget, propertyKey as string);\n Reflect.defineMetadata(METADATA_KEYS.AUTH_META, authMeta, metadataTarget, propertyKey);\n } else {\n // Class decorator\n validateNoConflictingDecorators(target, undefined);\n Reflect.defineMetadata(METADATA_KEYS.AUTH_META, authMeta, target);\n }\n };\n}\n\n/**\n * @Public() - endpoint requires no authentication. Class- or method-level.\n */\nexport function Public(): ClassDecorator & MethodDecorator {\n return defineAuthMode({ kind: 'public' });\n}\n\n/**\n * @AuthJwt(requirement) - THE user-facing JWT decorator, covering the whole user-JWT axis: the\n * compiler-enforced role decision ({@link JwtRoles}) plus app-defined fields ({@link JwtRequirement}).\n *\n * ```typescript\n * @AuthJwt({ roles: ['admin', 'editor'] }) // any-of\n * @AuthJwt({ allRolesAllowed: true, inOrg: true }) // wide + an app rule enforced by authorizeJwt\n * ```\n *\n * It absorbed the former `@Auth(requirement)` — same argument, same AuthMode, so two spellings of one\n * decision. One decorator per credential kind now: `@Public` / `@AuthJwt` / `@AuthOidc` /\n * `@AuthSharedSecret` / `@AuthLocalOnly`.\n */\n// webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope\nexport function AuthJwt(requirement: JwtRequirement): ClassDecorator & MethodDecorator {\n return defineAuthMode({ kind: 'jwt', requirement });\n}\n\n/**\n * The roles an endpoint accepts, or [] when it accepts every authenticated user. The ONE reader of\n * the {@link JwtRoles} union, so no caller has to re-derive \"does absent mean wide?\" — a question\n * whose two plausible answers is how the widest grant kept hiding behind an absent field.\n */\n// webpieces-disable no-function-outside-class -- reflect-metadata reader, sibling of getAuthMode\nexport function rolesRequired(requirement: JwtRequirement): readonly string[] {\n return requirement.allRolesAllowed === true ? [] : requirement.roles;\n}\n\n/**\n * @AuthOidc(...callers) - Google OIDC service-to-service auth (Cloud Tasks delivery / cross-service\n * RPC). `callers` is an OPTIONAL app-level allow-list of caller service accounts.\n *\n * NO args = TRUST THE EDGE: accept any genuine Google-signed OIDC caller, because a PRIVATE Cloud\n * Run service's edge already gates WHO via `run.invoker` IAM (managed in terraform — one source of\n * truth, no hand-synced list in code). If the service is actually PUBLIC, the verifier logs a loud\n * warning (it can't be the gate then). Pass explicit SAs (`@AuthOidc('svc-a')`) only when you want\n * an additional app-level allow-list as defense-in-depth.\n */\nexport function AuthOidc(...callers: string[]): ClassDecorator & MethodDecorator {\n return defineAuthMode({ kind: 'oidc', callers });\n}\n\n/**\n * @AuthSharedSecret(key) - constant-time compare of an inbound header against the secret bound for\n * `key`. `key` is a LOOKUP KEY (not an env var): the server looks up its accepted {@link SharedSecrets}\n * by this key, and each client looks up the value it sends by the SAME key (see {@link Secrets}).\n * For internal callers that cannot mint OIDC tokens.\n */\nexport function AuthSharedSecret(key: string): ClassDecorator & MethodDecorator {\n return defineAuthMode({ kind: 'shared-secret', secretKey: key });\n}\n\n/**\n * @AuthLocalOnly() - this endpoint exists ONLY on a developer's machine. Off-local it is not\n * registered as a route at all, and if it is somehow reached it 404s. Class- or method-level.\n *\n * ```typescript\n * @AuthLocalOnly()\n * @Endpoint('/logs', 'rpc')\n * sendBatch(request: SendLogBatchRequest): Promise<SendLogBatchResponse> { ... }\n * ```\n *\n * WHY IT IS AN AUTH MODE AND NOT A ROUTE-MODULE `if`. Apps hand-rolled this in TWO places kept in\n * sync by a comment: a route module that registered the route only locally, PLUS a\n * `if (env !== 'local') throw new HttpForbiddenError(...)` at the top of the handler. Neither half\n * was visible on the CONTRACT, so nothing reading the api — a human, a generated client, or an\n * agent — could tell this endpoint from a `@Public` one. Both halves are the framework's job now,\n * driven by this ONE declaration on the contract, which is where every other \"who may call this\"\n * fact already lives.\n *\n * It is DELIBERATELY a peer of @Public / @AuthJwt / @AuthOidc / @AuthSharedSecret rather than an\n * option on one of them: one decorator per credential kind, and \"local-only\" is a different kind of\n * gate — it authenticates nobody, it excludes an entire environment.\n *\n * HOW \"local\" IS DECIDED: {@link RuntimeLocality}, declared once at startup (a REQUIRED input to\n * `RuntimeSetupOptions`). Undeclared means DEPLOYED, so a forgotten wiring call refuses the endpoint\n * rather than exposing it.\n */\n// webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope\nexport function AuthLocalOnly(): ClassDecorator & MethodDecorator {\n return defineAuthMode({ kind: 'local-only' });\n}\n\n// ============================================================\n// Helper functions\n// ============================================================\n\n/**\n * Get the base path from @ApiPath decorator.\n */\nexport function getApiPath(apiClass: Function): string | undefined {\n return Reflect.getMetadata(METADATA_KEYS.API_PATH, apiClass);\n}\n\n/**\n * Get all endpoints from @Endpoint decorators.\n * Returns a record of methodName -> endpoint path.\n */\nexport function getEndpoints(apiClass: Function): Record<string, string> | undefined {\n return Reflect.getMetadata(METADATA_KEYS.ENDPOINTS, apiClass);\n}\n\n/**\n * Every method's declared trigger kind, as `methodName -> kind`. Parallel to {@link getEndpoints}.\n * Empty for a class carrying no @Endpoint at all.\n */\n// webpieces-disable no-function-outside-class -- reflect-metadata reader, sibling of getEndpoints\nexport function getEndpointKinds(apiClass: Function): Record<string, EndpointKind> {\n return Reflect.getMetadata(METADATA_KEYS.ENDPOINT_KIND, apiClass) || {};\n}\n\n/**\n * What triggers ONE method, or undefined when the method carries no @Endpoint.\n *\n * Defaults to nothing rather than to 'rpc': `kind` is a required argument, so a missing entry means\n * \"this is not an endpoint\", never \"an endpoint that forgot to say\". Silently defaulting here would\n * put an undeclared cron or webhook back into the graph as a normal rpc call — the exact blindness\n * the required argument exists to remove.\n */\n// webpieces-disable no-function-outside-class -- reflect-metadata reader, sibling of getEndpoints\nexport function getEndpointKind(apiClass: Function, methodName: string): EndpointKind | undefined {\n return getEndpointKinds(apiClass)[methodName];\n}\n\n/**\n * Get the @Endpoint options for one method (empty object if the method had no options).\n */\n// webpieces-disable no-function-outside-class -- reflect-metadata reader, sibling of getEndpoints\nexport function getEndpointOptions(apiClass: Function, methodName: string): EndpointOptions {\n const opts: Record<string, EndpointOptions> =\n Reflect.getMetadata(METADATA_KEYS.ENDPOINT_OPTIONS, apiClass) || {};\n return opts[methodName] ?? {};\n}\n\n/**\n * Fail-fast at wiring time when an `external` endpoint declared no caller. The {@link Endpoint}\n * overloads already make that a COMPILE error; this is the backstop for the ways TS is bypassed —\n * a JS caller, an `as any` options object, a hand-rolled Reflect.defineMetadata.\n * @throws Error naming the first external endpoint with no `calledBy`.\n */\n// webpieces-disable no-function-outside-class -- wiring-time assert, sibling of assertEveryEndpointHasAuthMode\nexport function assertEveryExternalEndpointDeclaresCaller(apiClass: Function): void {\n const kinds = getEndpointKinds(apiClass);\n for (const methodName of Object.keys(kinds)) {\n if (kinds[methodName] !== 'external' || getEndpointCaller(apiClass, methodName) !== undefined) continue;\n throw new Error(\n `External endpoint '${methodName}' in ${apiClass.name || 'Unknown'} declares no caller. Say WHO ` +\n `posts to it: @Endpoint(path, 'external', { calledBy: '<vendor>' }) — the runtime architecture ` +\n `graph cannot name an inbound caller it was never told about.`,\n );\n }\n}\n\n/**\n * True when the method's @Endpoint declared `{ formPost: true }` — its body is\n * application/x-www-form-urlencoded (flat), not JSON.\n */\n// webpieces-disable no-function-outside-class -- reflect-metadata reader, sibling of getEndpoints\nexport function isFormPost(apiClass: Function, methodName: string): boolean {\n return getEndpointOptions(apiClass, methodName).formPost === true;\n}\n\n/**\n * Check if a class has @ApiPath decorator.\n */\nexport function isApiPath(apiClass: Function): boolean {\n return Reflect.hasMetadata(METADATA_KEYS.API_PATH, apiClass);\n}\n\n/**\n * Get auth metadata for a specific method, falling back to class-level auth.\n * Method-level auth takes precedence over class-level auth.\n */\nexport function getAuthMeta(apiClass: Function, methodName?: string): AuthMeta | undefined {\n // Check method-level first\n if (methodName) {\n const methodAuth = Reflect.getMetadata(METADATA_KEYS.AUTH_META, apiClass, methodName);\n if (methodAuth) {\n return methodAuth;\n }\n }\n\n // Fall back to class-level\n return Reflect.getMetadata(METADATA_KEYS.AUTH_META, apiClass);\n}\n\n/**\n * Get the auth mode for a method (falling back to class-level), or undefined.\n * Convenience wrapper over getAuthMeta for callers that only want the mode.\n */\nexport function getAuthMode(apiClass: Function, methodName?: string): AuthMode | undefined {\n return getAuthMeta(apiClass, methodName)?.mode;\n}\n\n/**\n * The ONE prescription for \"this endpoint declares no auth\", shared by the two places that raise it\n * (here and http-routing's ApiRoutingFactory) because they had drifted into teaching different menus.\n * A message teaching an incomplete API is the same defect as an API with two spellings: whichever menu\n * the caller hits becomes the API they believe exists. It leads with the ROLE-GATED member on purpose —\n * the first thing offered should not be the widest grant.\n */\nexport const MISSING_AUTH_DECORATOR_FIX =\n \"Add one of @AuthJwt({roles: ['admin']}) / @AuthJwt({allRolesAllowed: true}) / @Public() / \" +\n '@AuthOidc(...callers) / @AuthSharedSecret(key) / @AuthLocalOnly() to the class or method.';\n\n/**\n * Fail-fast at wiring time if any endpoint lacks an auth mode. Both the server\n * (ApiRoutingFactory) and the task/rpc clients call this so a missing auth\n * decorator is a startup error, never a silent open endpoint.\n * @throws Error naming the first endpoint with no auth decorator, via {@link MISSING_AUTH_DECORATOR_FIX}.\n */\nexport function assertEveryEndpointHasAuthMode(apiClass: Function): void {\n const apiName = apiClass.name || 'Unknown';\n const endpoints = getEndpoints(apiClass) || {};\n for (const methodName of Object.keys(endpoints)) {\n if (!getAuthMeta(apiClass, methodName)) {\n throw new Error(\n `Endpoint '${methodName}' in ${apiName} has no auth decorator. ` +\n MISSING_AUTH_DECORATOR_FIX,\n );\n }\n }\n}\n\n/**\n * Validate that a class/method doesn't have conflicting auth decorators.\n * @throws Error if multiple auth decorators are found on the same target.\n */\nexport function validateNoConflictingDecorators(apiClass: Function, methodName: string | undefined): void {\n const existing = methodName\n ? Reflect.getMetadata(METADATA_KEYS.AUTH_META, apiClass, methodName)\n : Reflect.getMetadata(METADATA_KEYS.AUTH_META, apiClass);\n\n if (existing) {\n const targetName = apiClass.name || 'Unknown';\n const location = methodName ? `method '${methodName}' of ${targetName}` : `class ${targetName}`;\n throw new Error(\n `Conflicting auth decorator on ${location}. ` +\n `Only one of @Public() / @AuthJwt({...}) / @AuthOidc(...) / @AuthSharedSecret(...) / ` +\n `@AuthLocalOnly() is allowed per target.`\n );\n }\n}\n"]}
|
package/src/index.d.ts
CHANGED
|
@@ -17,8 +17,10 @@ export { ConsoleLogger } from './logging/ConsoleLogger';
|
|
|
17
17
|
export { ConsoleLoggerFactory } from './logging/ConsoleLoggerFactory';
|
|
18
18
|
export { LogManager } from './logging/LogManager';
|
|
19
19
|
export { LogChunker, LogChunkerImpl, LogChunkInfo, MAX_GCP_LOG_BYTES, GCP_LOG_BUDGET_BYTES } from './logging/LogChunker';
|
|
20
|
-
export { ApiPath, Endpoint, Public, AuthJwt, rolesRequired, MISSING_AUTH_DECORATOR_FIX, AuthOidc, AuthSharedSecret,
|
|
21
|
-
export type { AuthMode,
|
|
20
|
+
export { ApiPath, Endpoint, Public, AuthJwt, rolesRequired, MISSING_AUTH_DECORATOR_FIX, AuthOidc, AuthSharedSecret, AuthLocalOnly, MaskLog, getApiPath, getEndpoints, getEndpointOptions, getEndpointKind, getEndpointKinds, getMaskSpec, isFormPost, isApiPath, getAuthMeta, getAuthMode, assertEveryEndpointHasAuthMode, assertEveryExternalEndpointDeclaresCaller, validateNoConflictingDecorators, AuthMeta, RouteMetadata, METADATA_KEYS, } from './http/decorators';
|
|
21
|
+
export type { AuthMode, EndpointKind, JwtRoles, JwtRequirement, EndpointOptions, ExternalEndpointOptions } from './http/decorators';
|
|
22
|
+
export { Rpc, PubSub, Queue, ENDPOINT_KINDS_BY_API_KIND, getApiKind, assertApiKind, assertPubSubConventions, getQueueName, } from './http/api-kind';
|
|
23
|
+
export type { ApiKind } from './http/api-kind';
|
|
22
24
|
export { EXTERNAL_SYSTEM_KINDS, DEFAULT_CALLER_KIND, ExternalCaller, isExternalSystemKind, getEndpointCaller } from './http/external-caller';
|
|
23
25
|
export type { ExternalSystemKind } from './http/external-caller';
|
|
24
26
|
export { Secrets, SECRETS } from './http/Secrets';
|
|
@@ -30,6 +32,8 @@ export { HeaderRegistry } from './http/HeaderRegistry';
|
|
|
30
32
|
export { ClientRegistry } from './http/ClientRegistry';
|
|
31
33
|
export type { ServiceUrlDeriver } from './http/ClientRegistry';
|
|
32
34
|
export { ServiceInfo } from './http/ServiceInfo';
|
|
35
|
+
export { RuntimeLocality } from './http/RuntimeLocality';
|
|
36
|
+
export type { Locality } from './http/RuntimeLocality';
|
|
33
37
|
export { ErrorWireForm } from './http/ErrorTranslation';
|
|
34
38
|
export type { ErrorTranslation } from './http/ErrorTranslation';
|
|
35
39
|
export type { FailureClassifier } from './http/FailureClassifier';
|
package/src/index.js
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* @packageDocumentation
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
-
exports.
|
|
12
|
-
exports.
|
|
13
|
-
exports.SerializedError = exports.SerializedMap = exports.RecordSerializer = exports.getDoNotRecordFields = exports.DoNotRecord = exports.RecordedTestCase = exports.RecordedError = exports.RecordedEndpoint = exports.RecorderKeys = exports.ApiCallContextHolder = exports.ApiMethodInfo = void 0;
|
|
11
|
+
exports.DEFAULT_CALLER_KIND = exports.EXTERNAL_SYSTEM_KINDS = exports.getQueueName = exports.assertPubSubConventions = exports.assertApiKind = exports.getApiKind = exports.ENDPOINT_KINDS_BY_API_KIND = exports.Queue = exports.PubSub = exports.Rpc = exports.METADATA_KEYS = exports.RouteMetadata = exports.AuthMeta = exports.validateNoConflictingDecorators = exports.assertEveryExternalEndpointDeclaresCaller = exports.assertEveryEndpointHasAuthMode = exports.getAuthMode = exports.getAuthMeta = exports.isApiPath = exports.isFormPost = exports.getMaskSpec = exports.getEndpointKinds = exports.getEndpointKind = exports.getEndpointOptions = exports.getEndpoints = exports.getApiPath = exports.MaskLog = exports.AuthLocalOnly = exports.AuthSharedSecret = exports.AuthOidc = exports.MISSING_AUTH_DECORATOR_FIX = exports.rolesRequired = exports.AuthJwt = exports.Public = exports.Endpoint = exports.ApiPath = exports.GCP_LOG_BUDGET_BYTES = exports.MAX_GCP_LOG_BYTES = exports.LogChunkInfo = exports.LogChunkerImpl = exports.LogChunker = exports.LogManager = exports.ConsoleLoggerFactory = exports.ConsoleLogger = exports.DESIGN_METADATA_KEYS = exports.isDocumentDesign = exports.DocumentDesign = exports.ContextTuple = exports.ContextKey = exports.toError = void 0;
|
|
12
|
+
exports.ApiCallLogName = exports.ApiCallInfo = exports.MaskSpec = exports.LogApiCallImpl = exports.LogApiCall = exports.ContextMgr = exports.DestinationTrust = exports.WebpiecesCoreHeaders = exports.templateDeriver = exports.WEBPIECES_DEFAULT_FAILURE_CLASSIFIER = exports.WebpiecesDefaultFailureClassifier = exports.KeyedFailureClassifier = exports.ErrorWireForm = exports.RuntimeLocality = exports.ServiceInfo = exports.ClientRegistry = exports.HeaderRegistry = exports.DateTimeUtil = exports.TimeUtil = exports.DateUtil = exports.InstantUtil = exports.NetworkRejectClassifier = exports.NO_REG_CODE = exports.WRONG_COMPANY = exports.WRONG_DOMAIN = exports.EMAIL_NOT_CONFIRMED = exports.NOT_APPROVED = exports.WRONG_LOGIN = exports.WRONG_LOGIN_TYPE = exports.ENTITY_NOT_FOUND = exports.OfflineError = exports.HttpUserError = exports.HttpVendorError = exports.HttpTooManyRequestsError = exports.HttpInternalServerError = exports.HttpGatewayTimeoutError = exports.HttpBadGatewayError = exports.HttpTimeoutError = exports.HttpForbiddenError = exports.HttpUnauthorizedError = exports.HttpBadRequestError = exports.EndpointNotFoundError = exports.HttpNotFoundError = exports.HttpError = exports.ProtocolError = exports.SECRETS = exports.Secrets = exports.getEndpointCaller = exports.isExternalSystemKind = exports.ExternalCaller = void 0;
|
|
13
|
+
exports.SerializedError = exports.SerializedMap = exports.RecordSerializer = exports.getDoNotRecordFields = exports.DoNotRecord = exports.RecordedTestCase = exports.RecordedError = exports.RecordedEndpoint = exports.RecorderKeys = exports.ApiCallContextHolder = exports.ApiMethodInfo = exports.LOG_API_CALL_LOGGER_NAME = exports.ApiCallLogNameImpl = void 0;
|
|
14
14
|
var errorUtils_1 = require("./lib/errorUtils");
|
|
15
15
|
Object.defineProperty(exports, "toError", { enumerable: true, get: function () { return errorUtils_1.toError; } });
|
|
16
16
|
var ContextKey_1 = require("./ContextKey");
|
|
@@ -52,17 +52,13 @@ Object.defineProperty(exports, "rolesRequired", { enumerable: true, get: functio
|
|
|
52
52
|
Object.defineProperty(exports, "MISSING_AUTH_DECORATOR_FIX", { enumerable: true, get: function () { return decorators_1.MISSING_AUTH_DECORATOR_FIX; } });
|
|
53
53
|
Object.defineProperty(exports, "AuthOidc", { enumerable: true, get: function () { return decorators_1.AuthOidc; } });
|
|
54
54
|
Object.defineProperty(exports, "AuthSharedSecret", { enumerable: true, get: function () { return decorators_1.AuthSharedSecret; } });
|
|
55
|
-
|
|
56
|
-
Object.defineProperty(exports, "Rpc", { enumerable: true, get: function () { return decorators_1.Rpc; } });
|
|
57
|
-
Object.defineProperty(exports, "PubSub", { enumerable: true, get: function () { return decorators_1.PubSub; } });
|
|
58
|
-
Object.defineProperty(exports, "Queue", { enumerable: true, get: function () { return decorators_1.Queue; } });
|
|
55
|
+
Object.defineProperty(exports, "AuthLocalOnly", { enumerable: true, get: function () { return decorators_1.AuthLocalOnly; } });
|
|
59
56
|
Object.defineProperty(exports, "MaskLog", { enumerable: true, get: function () { return decorators_1.MaskLog; } });
|
|
60
57
|
Object.defineProperty(exports, "getApiPath", { enumerable: true, get: function () { return decorators_1.getApiPath; } });
|
|
61
58
|
Object.defineProperty(exports, "getEndpoints", { enumerable: true, get: function () { return decorators_1.getEndpoints; } });
|
|
62
59
|
Object.defineProperty(exports, "getEndpointOptions", { enumerable: true, get: function () { return decorators_1.getEndpointOptions; } });
|
|
63
60
|
Object.defineProperty(exports, "getEndpointKind", { enumerable: true, get: function () { return decorators_1.getEndpointKind; } });
|
|
64
61
|
Object.defineProperty(exports, "getEndpointKinds", { enumerable: true, get: function () { return decorators_1.getEndpointKinds; } });
|
|
65
|
-
Object.defineProperty(exports, "ENDPOINT_KINDS_BY_API_KIND", { enumerable: true, get: function () { return decorators_1.ENDPOINT_KINDS_BY_API_KIND; } });
|
|
66
62
|
Object.defineProperty(exports, "getMaskSpec", { enumerable: true, get: function () { return decorators_1.getMaskSpec; } });
|
|
67
63
|
Object.defineProperty(exports, "isFormPost", { enumerable: true, get: function () { return decorators_1.isFormPost; } });
|
|
68
64
|
Object.defineProperty(exports, "isApiPath", { enumerable: true, get: function () { return decorators_1.isApiPath; } });
|
|
@@ -70,14 +66,21 @@ Object.defineProperty(exports, "getAuthMeta", { enumerable: true, get: function
|
|
|
70
66
|
Object.defineProperty(exports, "getAuthMode", { enumerable: true, get: function () { return decorators_1.getAuthMode; } });
|
|
71
67
|
Object.defineProperty(exports, "assertEveryEndpointHasAuthMode", { enumerable: true, get: function () { return decorators_1.assertEveryEndpointHasAuthMode; } });
|
|
72
68
|
Object.defineProperty(exports, "assertEveryExternalEndpointDeclaresCaller", { enumerable: true, get: function () { return decorators_1.assertEveryExternalEndpointDeclaresCaller; } });
|
|
73
|
-
Object.defineProperty(exports, "getApiKind", { enumerable: true, get: function () { return decorators_1.getApiKind; } });
|
|
74
|
-
Object.defineProperty(exports, "assertApiKind", { enumerable: true, get: function () { return decorators_1.assertApiKind; } });
|
|
75
|
-
Object.defineProperty(exports, "assertPubSubConventions", { enumerable: true, get: function () { return decorators_1.assertPubSubConventions; } });
|
|
76
|
-
Object.defineProperty(exports, "getQueueName", { enumerable: true, get: function () { return decorators_1.getQueueName; } });
|
|
77
69
|
Object.defineProperty(exports, "validateNoConflictingDecorators", { enumerable: true, get: function () { return decorators_1.validateNoConflictingDecorators; } });
|
|
78
70
|
Object.defineProperty(exports, "AuthMeta", { enumerable: true, get: function () { return decorators_1.AuthMeta; } });
|
|
79
71
|
Object.defineProperty(exports, "RouteMetadata", { enumerable: true, get: function () { return decorators_1.RouteMetadata; } });
|
|
80
72
|
Object.defineProperty(exports, "METADATA_KEYS", { enumerable: true, get: function () { return decorators_1.METADATA_KEYS; } });
|
|
73
|
+
// API kind (RPC vs PubSub/Cloud Tasks) + queue naming. Split out of decorators.ts for file size only;
|
|
74
|
+
// one-way dependency api-kind -> decorators, and the barrel keeps the surface identical.
|
|
75
|
+
var api_kind_1 = require("./http/api-kind");
|
|
76
|
+
Object.defineProperty(exports, "Rpc", { enumerable: true, get: function () { return api_kind_1.Rpc; } });
|
|
77
|
+
Object.defineProperty(exports, "PubSub", { enumerable: true, get: function () { return api_kind_1.PubSub; } });
|
|
78
|
+
Object.defineProperty(exports, "Queue", { enumerable: true, get: function () { return api_kind_1.Queue; } });
|
|
79
|
+
Object.defineProperty(exports, "ENDPOINT_KINDS_BY_API_KIND", { enumerable: true, get: function () { return api_kind_1.ENDPOINT_KINDS_BY_API_KIND; } });
|
|
80
|
+
Object.defineProperty(exports, "getApiKind", { enumerable: true, get: function () { return api_kind_1.getApiKind; } });
|
|
81
|
+
Object.defineProperty(exports, "assertApiKind", { enumerable: true, get: function () { return api_kind_1.assertApiKind; } });
|
|
82
|
+
Object.defineProperty(exports, "assertPubSubConventions", { enumerable: true, get: function () { return api_kind_1.assertPubSubConventions; } });
|
|
83
|
+
Object.defineProperty(exports, "getQueueName", { enumerable: true, get: function () { return api_kind_1.getQueueName; } });
|
|
81
84
|
// WHO calls an `external` endpoint — the caller declaration @Endpoint(..., 'external', {calledBy})
|
|
82
85
|
// requires, and the reader for it.
|
|
83
86
|
var external_caller_1 = require("./http/external-caller");
|
|
@@ -133,6 +136,10 @@ Object.defineProperty(exports, "ClientRegistry", { enumerable: true, get: functi
|
|
|
133
136
|
// RequestContextHeaders (to stamp requestIdSource on ids this service mints).
|
|
134
137
|
var ServiceInfo_1 = require("./http/ServiceInfo");
|
|
135
138
|
Object.defineProperty(exports, "ServiceInfo", { enumerable: true, get: function () { return ServiceInfo_1.ServiceInfo; } });
|
|
139
|
+
// "Where am I running" — declared once at startup (setupRuntime, from RuntimeSetupOptions.locality).
|
|
140
|
+
// The ONE input to @AuthLocalOnly enforcement. Undeclared reads as DEPLOYED (fail safe).
|
|
141
|
+
var RuntimeLocality_1 = require("./http/RuntimeLocality");
|
|
142
|
+
Object.defineProperty(exports, "RuntimeLocality", { enumerable: true, get: function () { return RuntimeLocality_1.RuntimeLocality; } });
|
|
136
143
|
// Pluggable, bidirectional error translation (app exception <-> wire form). Registered on
|
|
137
144
|
// ClientRegistry at startup; consulted before the built-in webpieces mapping on BOTH sides.
|
|
138
145
|
var ErrorTranslation_1 = require("./http/ErrorTranslation");
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/core/core-util/src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @webpieces/core-util\n *\n * Utility functions for WebPieces applications.\n * This package works in both browser and Node.js environments.\n *\n * @packageDocumentation\n */\n\nexport { toError } from './lib/errorUtils';\nexport { ContextKey } from './ContextKey';\nexport type { AnyContextKey, AnyTrustedContextKey, AnyUntrustedContextKey, Trust } from './ContextKey';\nexport { ContextTuple } from './ContextTuple';\n\n// @DocumentDesign — DI-design-root marker. Applies to ANY project kind (server\n// controllers AND library impl classes), so it lives here (browser + Node) rather\n// than in a server-only routing package.\nexport { DocumentDesign, isDocumentDesign, DESIGN_METADATA_KEYS } from './DocumentDesign';\n\n// Logging (merged from former @webpieces/wp-logging).\n// Pluggable logging interface + a browser-safe console default; apps plug in\n// bunyan/winston/pino/etc. via LogManager.setFactory(...). Browser + Node.\nexport type { Logger, LogLevel } from './logging/Logger';\nexport type { LoggerFactory } from './logging/LoggerFactory';\nexport { ConsoleLogger } from './logging/ConsoleLogger';\nexport { ConsoleLoggerFactory } from './logging/ConsoleLoggerFactory';\nexport { LogManager } from './logging/LogManager';\nexport { LogChunker, LogChunkerImpl, LogChunkInfo, MAX_GCP_LOG_BYTES, GCP_LOG_BUDGET_BYTES } from './logging/LogChunker';\n\n// HTTP API contract (merged from former @webpieces/http-api).\n// Shared HTTP API definition consumed by both client and server: REST\n// decorators, the HttpError hierarchy, datetime DTOs, platform-header\n// registry/readers, ValidateImplementation, and the test-case recorder\n// contract. Pure definitions — express-free, browser + Node safe.\n\n// API definition decorators\nexport {\n ApiPath,\n Endpoint,\n // Auth mode decorators (clean service-to-service + user JWT model)\n Public,\n AuthJwt,\n rolesRequired,\n MISSING_AUTH_DECORATOR_FIX,\n AuthOidc,\n AuthSharedSecret,\n // API kind (RPC vs PubSub/Cloud Tasks) + queue naming\n Rpc,\n PubSub,\n Queue,\n MaskLog,\n getApiPath,\n getEndpoints,\n getEndpointOptions,\n getEndpointKind,\n getEndpointKinds,\n ENDPOINT_KINDS_BY_API_KIND,\n getMaskSpec,\n isFormPost,\n isApiPath,\n getAuthMeta,\n getAuthMode,\n assertEveryEndpointHasAuthMode,\n assertEveryExternalEndpointDeclaresCaller,\n getApiKind,\n assertApiKind,\n assertPubSubConventions,\n getQueueName,\n validateNoConflictingDecorators,\n AuthMeta,\n RouteMetadata,\n METADATA_KEYS,\n} from './http/decorators';\nexport type { AuthMode, ApiKind, EndpointKind, JwtRoles, JwtRequirement, EndpointOptions, ExternalEndpointOptions } from './http/decorators';\n// WHO calls an `external` endpoint — the caller declaration @Endpoint(..., 'external', {calledBy})\n// requires, and the reader for it.\nexport { EXTERNAL_SYSTEM_KINDS, DEFAULT_CALLER_KIND, ExternalCaller, isExternalSystemKind, getEndpointCaller } from './http/external-caller';\nexport type { ExternalSystemKind } from './http/external-caller';\n// Client-side shared-secret store (the value THIS service sends per @AuthSharedSecret key).\nexport { Secrets, SECRETS } from './http/Secrets';\n\n// Type validators\nexport { ValidateImplementation } from './http/validators';\n\n// HTTP errors\nexport {\n ProtocolError,\n HttpError,\n HttpNotFoundError,\n EndpointNotFoundError,\n HttpBadRequestError,\n HttpUnauthorizedError,\n HttpForbiddenError,\n HttpTimeoutError,\n HttpBadGatewayError,\n HttpGatewayTimeoutError,\n HttpInternalServerError,\n HttpTooManyRequestsError,\n HttpVendorError,\n HttpUserError,\n OfflineError,\n // Error subtype constants\n ENTITY_NOT_FOUND,\n WRONG_LOGIN_TYPE,\n WRONG_LOGIN,\n NOT_APPROVED,\n EMAIL_NOT_CONFIRMED,\n WRONG_DOMAIN,\n WRONG_COMPANY,\n NO_REG_CODE,\n} from './http/errors';\n\nexport { NetworkRejectClassifier } from './http/networkReject';\n\n// Date/Time DTOs and Utilities (inspired by Java Time / JSR-310)\nexport {\n InstantDto,\n DateDto,\n TimeDto,\n DateTimeDto,\n InstantUtil,\n DateUtil,\n TimeUtil,\n DateTimeUtil,\n} from './http/datetime';\n\n// Context keys + registry (the global magic-context header system)\nexport { HeaderRegistry } from './http/HeaderRegistry';\nexport { ClientRegistry } from './http/ClientRegistry';\nexport type { ServiceUrlDeriver } from './http/ClientRegistry';\n\n// \"What service am I\" — set once at startup, read by the logging backends and by\n// RequestContextHeaders (to stamp requestIdSource on ids this service mints).\nexport { ServiceInfo } from './http/ServiceInfo';\n// Pluggable, bidirectional error translation (app exception <-> wire form). Registered on\n// ClientRegistry at startup; consulted before the built-in webpieces mapping on BOTH sides.\nexport { ErrorWireForm } from './http/ErrorTranslation';\nexport type { ErrorTranslation } from './http/ErrorTranslation';\n// Pluggable per-client failure classification (is a thrown API-call error a real failure or an\n// expected non-failure?). Registered on ClientRegistry at startup; consulted by LogApiCall.\nexport type { FailureClassifier } from './http/FailureClassifier';\nexport { KeyedFailureClassifier } from './http/FailureClassifier';\nexport {\n WebpiecesDefaultFailureClassifier,\n WEBPIECES_DEFAULT_FAILURE_CLASSIFIER,\n} from './http/WebpiecesDefaultFailureClassifier';\nexport { templateDeriver } from './http/templateDeriver';\nexport { WebpiecesCoreHeaders } from './http/WebpiecesCoreHeaders';\nexport { ContextReader } from './http/ContextReader';\n\n// The OUTBOUND half of the trust model: whether a TRUSTED context key may ride to the endpoint being\n// called. Built ONLY from the destination endpoint's AuthMode — see the class doc.\nexport { DestinationTrust } from './http/DestinationTrust';\n\n// BROWSER-ONLY outbound-header propagation (app-held store + registry -> outbound HTTP headers).\n// Only @webpieces/http-client-browser may name it; the server reads RequestContext directly via\n// RequestContextHeaders in the Node-only @webpieces/core-context.\nexport { ContextMgr } from './http/ContextMgr';\n\n// API-call logging helper (uses LogManager above). Singleton: use the LogApiCall constant, not `new`.\nexport { LogApiCall, LogApiCallImpl } from './http/LogApiCall';\n\n// Opt-in field masking for the LogApiCall log path — declare per-api sensitive fields so secrets\n// (OAuth refresh tokens, id-token JWTs) are masked in the logs while the real value stays on the wire.\nexport { MaskSpec } from './http/LogFieldMask';\nexport type { MaskMode } from './http/LogFieldMask';\n\n// The structured `api` tag + the context-writer seam LogApiCall stamps through. The Node\n// RequestContext-backed impl is installed by @webpieces/core-context; the browser gets the no-op.\nexport { ApiCallInfo } from './http/ApiCallInfo';\nexport type { ApiType, ApiResult } from './http/ApiCallInfo';\n// Console-render bridge: turns LogApiCall's [LogApiCall] bracket into [API.{side}.{phase}] locally.\nexport { ApiCallLogName, ApiCallLogNameImpl, LOG_API_CALL_LOGGER_NAME } from './http/ApiCallLogName';\nexport { ApiMethodInfo } from './http/ApiMethodInfo';\nexport type { ApiSide } from './http/ApiMethodInfo';\nexport { ApiCallContextHolder } from './http/ApiCallContext';\nexport type { ApiCallContext } from './http/ApiCallContext';\n\n// Test-case recording contract (impl lives in http-server; hooks in http-client)\nexport { TestCaseRecorder, RecorderKeys } from './http/recorder/TestCaseRecorder';\nexport { RecordedEndpoint, RecordedError, RecordedTestCase } from './http/recorder/RecordedEndpoint';\nexport { DoNotRecord, getDoNotRecordFields } from './http/recorder/DoNotRecord';\nexport { RecordSerializer, SerializedMap, SerializedError } from './http/recorder/RecordSerializer';\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/core/core-util/src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;;;AAEH,+CAA2C;AAAlC,qGAAA,OAAO,OAAA;AAChB,2CAA0C;AAAjC,wGAAA,UAAU,OAAA;AAEnB,+CAA8C;AAArC,4GAAA,YAAY,OAAA;AAErB,+EAA+E;AAC/E,kFAAkF;AAClF,yCAAyC;AACzC,mDAA0F;AAAjF,gHAAA,cAAc,OAAA;AAAE,kHAAA,gBAAgB,OAAA;AAAE,sHAAA,oBAAoB,OAAA;AAO/D,yDAAwD;AAA/C,8GAAA,aAAa,OAAA;AACtB,uEAAsE;AAA7D,4HAAA,oBAAoB,OAAA;AAC7B,mDAAkD;AAAzC,wGAAA,UAAU,OAAA;AACnB,mDAAyH;AAAhH,wGAAA,UAAU,OAAA;AAAE,4GAAA,cAAc,OAAA;AAAE,0GAAA,YAAY,OAAA;AAAE,+GAAA,iBAAiB,OAAA;AAAE,kHAAA,oBAAoB,OAAA;AAE1F,8DAA8D;AAC9D,sEAAsE;AACtE,sEAAsE;AACtE,uEAAuE;AACvE,kEAAkE;AAElE,4BAA4B;AAC5B,gDA4B2B;AA3BvB,qGAAA,OAAO,OAAA;AACP,sGAAA,QAAQ,OAAA;AACR,mEAAmE;AACnE,oGAAA,MAAM,OAAA;AACN,qGAAA,OAAO,OAAA;AACP,2GAAA,aAAa,OAAA;AACb,wHAAA,0BAA0B,OAAA;AAC1B,sGAAA,QAAQ,OAAA;AACR,8GAAA,gBAAgB,OAAA;AAChB,2GAAA,aAAa,OAAA;AACb,qGAAA,OAAO,OAAA;AACP,wGAAA,UAAU,OAAA;AACV,0GAAA,YAAY,OAAA;AACZ,gHAAA,kBAAkB,OAAA;AAClB,6GAAA,eAAe,OAAA;AACf,8GAAA,gBAAgB,OAAA;AAChB,yGAAA,WAAW,OAAA;AACX,wGAAA,UAAU,OAAA;AACV,uGAAA,SAAS,OAAA;AACT,yGAAA,WAAW,OAAA;AACX,yGAAA,WAAW,OAAA;AACX,4HAAA,8BAA8B,OAAA;AAC9B,uIAAA,yCAAyC,OAAA;AACzC,6HAAA,+BAA+B,OAAA;AAC/B,sGAAA,QAAQ,OAAA;AACR,2GAAA,aAAa,OAAA;AACb,2GAAA,aAAa,OAAA;AAGjB,sGAAsG;AACtG,yFAAyF;AACzF,4CASyB;AARrB,+FAAA,GAAG,OAAA;AACH,kGAAA,MAAM,OAAA;AACN,iGAAA,KAAK,OAAA;AACL,sHAAA,0BAA0B,OAAA;AAC1B,sGAAA,UAAU,OAAA;AACV,yGAAA,aAAa,OAAA;AACb,mHAAA,uBAAuB,OAAA;AACvB,wGAAA,YAAY,OAAA;AAGhB,mGAAmG;AACnG,mCAAmC;AACnC,0DAA6I;AAApI,wHAAA,qBAAqB,OAAA;AAAE,sHAAA,mBAAmB,OAAA;AAAE,iHAAA,cAAc,OAAA;AAAE,uHAAA,oBAAoB,OAAA;AAAE,oHAAA,iBAAiB,OAAA;AAE5G,4FAA4F;AAC5F,0CAAkD;AAAzC,kGAAA,OAAO,OAAA;AAAE,kGAAA,OAAO,OAAA;AAKzB,cAAc;AACd,wCAyBuB;AAxBnB,uGAAA,aAAa,OAAA;AACb,mGAAA,SAAS,OAAA;AACT,2GAAA,iBAAiB,OAAA;AACjB,+GAAA,qBAAqB,OAAA;AACrB,6GAAA,mBAAmB,OAAA;AACnB,+GAAA,qBAAqB,OAAA;AACrB,4GAAA,kBAAkB,OAAA;AAClB,0GAAA,gBAAgB,OAAA;AAChB,6GAAA,mBAAmB,OAAA;AACnB,iHAAA,uBAAuB,OAAA;AACvB,iHAAA,uBAAuB,OAAA;AACvB,kHAAA,wBAAwB,OAAA;AACxB,yGAAA,eAAe,OAAA;AACf,uGAAA,aAAa,OAAA;AACb,sGAAA,YAAY,OAAA;AACZ,0BAA0B;AAC1B,0GAAA,gBAAgB,OAAA;AAChB,0GAAA,gBAAgB,OAAA;AAChB,qGAAA,WAAW,OAAA;AACX,sGAAA,YAAY,OAAA;AACZ,6GAAA,mBAAmB,OAAA;AACnB,sGAAA,YAAY,OAAA;AACZ,uGAAA,aAAa,OAAA;AACb,qGAAA,WAAW,OAAA;AAGf,sDAA+D;AAAtD,wHAAA,uBAAuB,OAAA;AAEhC,iEAAiE;AACjE,4CASyB;AAJrB,uGAAA,WAAW,OAAA;AACX,oGAAA,QAAQ,OAAA;AACR,oGAAA,QAAQ,OAAA;AACR,wGAAA,YAAY,OAAA;AAGhB,mEAAmE;AACnE,wDAAuD;AAA9C,gHAAA,cAAc,OAAA;AACvB,wDAAuD;AAA9C,gHAAA,cAAc,OAAA;AAGvB,iFAAiF;AACjF,8EAA8E;AAC9E,kDAAiD;AAAxC,0GAAA,WAAW,OAAA;AACpB,qGAAqG;AACrG,yFAAyF;AACzF,0DAAyD;AAAhD,kHAAA,eAAe,OAAA;AAExB,0FAA0F;AAC1F,4FAA4F;AAC5F,4DAAwD;AAA/C,iHAAA,aAAa,OAAA;AAKtB,8DAAkE;AAAzD,2HAAA,sBAAsB,OAAA;AAC/B,8FAGkD;AAF9C,sJAAA,iCAAiC,OAAA;AACjC,yJAAA,oCAAoC,OAAA;AAExC,0DAAyD;AAAhD,kHAAA,eAAe,OAAA;AACxB,oEAAmE;AAA1D,4HAAA,oBAAoB,OAAA;AAG7B,qGAAqG;AACrG,mFAAmF;AACnF,4DAA2D;AAAlD,oHAAA,gBAAgB,OAAA;AAEzB,iGAAiG;AACjG,gGAAgG;AAChG,kEAAkE;AAClE,gDAA+C;AAAtC,wGAAA,UAAU,OAAA;AAEnB,sGAAsG;AACtG,gDAA+D;AAAtD,wGAAA,UAAU,OAAA;AAAE,4GAAA,cAAc,OAAA;AAEnC,iGAAiG;AACjG,uGAAuG;AACvG,oDAA+C;AAAtC,wGAAA,QAAQ,OAAA;AAGjB,yFAAyF;AACzF,kGAAkG;AAClG,kDAAiD;AAAxC,0GAAA,WAAW,OAAA;AAEpB,oGAAoG;AACpG,wDAAqG;AAA5F,gHAAA,cAAc,OAAA;AAAE,oHAAA,kBAAkB,OAAA;AAAE,0HAAA,wBAAwB,OAAA;AACrE,sDAAqD;AAA5C,8GAAA,aAAa,OAAA;AAEtB,wDAA6D;AAApD,sHAAA,oBAAoB,OAAA;AAG7B,iFAAiF;AACjF,qEAAkF;AAAvD,gHAAA,YAAY,OAAA;AACvC,qEAAqG;AAA5F,oHAAA,gBAAgB,OAAA;AAAE,iHAAA,aAAa,OAAA;AAAE,oHAAA,gBAAgB,OAAA;AAC1D,2DAAgF;AAAvE,0GAAA,WAAW,OAAA;AAAE,mHAAA,oBAAoB,OAAA;AAC1C,qEAAoG;AAA3F,oHAAA,gBAAgB,OAAA;AAAE,iHAAA,aAAa,OAAA;AAAE,mHAAA,eAAe,OAAA","sourcesContent":["/**\n * @webpieces/core-util\n *\n * Utility functions for WebPieces applications.\n * This package works in both browser and Node.js environments.\n *\n * @packageDocumentation\n */\n\nexport { toError } from './lib/errorUtils';\nexport { ContextKey } from './ContextKey';\nexport type { AnyContextKey, AnyTrustedContextKey, AnyUntrustedContextKey, Trust } from './ContextKey';\nexport { ContextTuple } from './ContextTuple';\n\n// @DocumentDesign — DI-design-root marker. Applies to ANY project kind (server\n// controllers AND library impl classes), so it lives here (browser + Node) rather\n// than in a server-only routing package.\nexport { DocumentDesign, isDocumentDesign, DESIGN_METADATA_KEYS } from './DocumentDesign';\n\n// Logging (merged from former @webpieces/wp-logging).\n// Pluggable logging interface + a browser-safe console default; apps plug in\n// bunyan/winston/pino/etc. via LogManager.setFactory(...). Browser + Node.\nexport type { Logger, LogLevel } from './logging/Logger';\nexport type { LoggerFactory } from './logging/LoggerFactory';\nexport { ConsoleLogger } from './logging/ConsoleLogger';\nexport { ConsoleLoggerFactory } from './logging/ConsoleLoggerFactory';\nexport { LogManager } from './logging/LogManager';\nexport { LogChunker, LogChunkerImpl, LogChunkInfo, MAX_GCP_LOG_BYTES, GCP_LOG_BUDGET_BYTES } from './logging/LogChunker';\n\n// HTTP API contract (merged from former @webpieces/http-api).\n// Shared HTTP API definition consumed by both client and server: REST\n// decorators, the HttpError hierarchy, datetime DTOs, platform-header\n// registry/readers, ValidateImplementation, and the test-case recorder\n// contract. Pure definitions — express-free, browser + Node safe.\n\n// API definition decorators\nexport {\n ApiPath,\n Endpoint,\n // Auth mode decorators (clean service-to-service + user JWT model)\n Public,\n AuthJwt,\n rolesRequired,\n MISSING_AUTH_DECORATOR_FIX,\n AuthOidc,\n AuthSharedSecret,\n AuthLocalOnly,\n MaskLog,\n getApiPath,\n getEndpoints,\n getEndpointOptions,\n getEndpointKind,\n getEndpointKinds,\n getMaskSpec,\n isFormPost,\n isApiPath,\n getAuthMeta,\n getAuthMode,\n assertEveryEndpointHasAuthMode,\n assertEveryExternalEndpointDeclaresCaller,\n validateNoConflictingDecorators,\n AuthMeta,\n RouteMetadata,\n METADATA_KEYS,\n} from './http/decorators';\nexport type { AuthMode, EndpointKind, JwtRoles, JwtRequirement, EndpointOptions, ExternalEndpointOptions } from './http/decorators';\n// API kind (RPC vs PubSub/Cloud Tasks) + queue naming. Split out of decorators.ts for file size only;\n// one-way dependency api-kind -> decorators, and the barrel keeps the surface identical.\nexport {\n Rpc,\n PubSub,\n Queue,\n ENDPOINT_KINDS_BY_API_KIND,\n getApiKind,\n assertApiKind,\n assertPubSubConventions,\n getQueueName,\n} from './http/api-kind';\nexport type { ApiKind } from './http/api-kind';\n// WHO calls an `external` endpoint — the caller declaration @Endpoint(..., 'external', {calledBy})\n// requires, and the reader for it.\nexport { EXTERNAL_SYSTEM_KINDS, DEFAULT_CALLER_KIND, ExternalCaller, isExternalSystemKind, getEndpointCaller } from './http/external-caller';\nexport type { ExternalSystemKind } from './http/external-caller';\n// Client-side shared-secret store (the value THIS service sends per @AuthSharedSecret key).\nexport { Secrets, SECRETS } from './http/Secrets';\n\n// Type validators\nexport { ValidateImplementation } from './http/validators';\n\n// HTTP errors\nexport {\n ProtocolError,\n HttpError,\n HttpNotFoundError,\n EndpointNotFoundError,\n HttpBadRequestError,\n HttpUnauthorizedError,\n HttpForbiddenError,\n HttpTimeoutError,\n HttpBadGatewayError,\n HttpGatewayTimeoutError,\n HttpInternalServerError,\n HttpTooManyRequestsError,\n HttpVendorError,\n HttpUserError,\n OfflineError,\n // Error subtype constants\n ENTITY_NOT_FOUND,\n WRONG_LOGIN_TYPE,\n WRONG_LOGIN,\n NOT_APPROVED,\n EMAIL_NOT_CONFIRMED,\n WRONG_DOMAIN,\n WRONG_COMPANY,\n NO_REG_CODE,\n} from './http/errors';\n\nexport { NetworkRejectClassifier } from './http/networkReject';\n\n// Date/Time DTOs and Utilities (inspired by Java Time / JSR-310)\nexport {\n InstantDto,\n DateDto,\n TimeDto,\n DateTimeDto,\n InstantUtil,\n DateUtil,\n TimeUtil,\n DateTimeUtil,\n} from './http/datetime';\n\n// Context keys + registry (the global magic-context header system)\nexport { HeaderRegistry } from './http/HeaderRegistry';\nexport { ClientRegistry } from './http/ClientRegistry';\nexport type { ServiceUrlDeriver } from './http/ClientRegistry';\n\n// \"What service am I\" — set once at startup, read by the logging backends and by\n// RequestContextHeaders (to stamp requestIdSource on ids this service mints).\nexport { ServiceInfo } from './http/ServiceInfo';\n// \"Where am I running\" — declared once at startup (setupRuntime, from RuntimeSetupOptions.locality).\n// The ONE input to @AuthLocalOnly enforcement. Undeclared reads as DEPLOYED (fail safe).\nexport { RuntimeLocality } from './http/RuntimeLocality';\nexport type { Locality } from './http/RuntimeLocality';\n// Pluggable, bidirectional error translation (app exception <-> wire form). Registered on\n// ClientRegistry at startup; consulted before the built-in webpieces mapping on BOTH sides.\nexport { ErrorWireForm } from './http/ErrorTranslation';\nexport type { ErrorTranslation } from './http/ErrorTranslation';\n// Pluggable per-client failure classification (is a thrown API-call error a real failure or an\n// expected non-failure?). Registered on ClientRegistry at startup; consulted by LogApiCall.\nexport type { FailureClassifier } from './http/FailureClassifier';\nexport { KeyedFailureClassifier } from './http/FailureClassifier';\nexport {\n WebpiecesDefaultFailureClassifier,\n WEBPIECES_DEFAULT_FAILURE_CLASSIFIER,\n} from './http/WebpiecesDefaultFailureClassifier';\nexport { templateDeriver } from './http/templateDeriver';\nexport { WebpiecesCoreHeaders } from './http/WebpiecesCoreHeaders';\nexport { ContextReader } from './http/ContextReader';\n\n// The OUTBOUND half of the trust model: whether a TRUSTED context key may ride to the endpoint being\n// called. Built ONLY from the destination endpoint's AuthMode — see the class doc.\nexport { DestinationTrust } from './http/DestinationTrust';\n\n// BROWSER-ONLY outbound-header propagation (app-held store + registry -> outbound HTTP headers).\n// Only @webpieces/http-client-browser may name it; the server reads RequestContext directly via\n// RequestContextHeaders in the Node-only @webpieces/core-context.\nexport { ContextMgr } from './http/ContextMgr';\n\n// API-call logging helper (uses LogManager above). Singleton: use the LogApiCall constant, not `new`.\nexport { LogApiCall, LogApiCallImpl } from './http/LogApiCall';\n\n// Opt-in field masking for the LogApiCall log path — declare per-api sensitive fields so secrets\n// (OAuth refresh tokens, id-token JWTs) are masked in the logs while the real value stays on the wire.\nexport { MaskSpec } from './http/LogFieldMask';\nexport type { MaskMode } from './http/LogFieldMask';\n\n// The structured `api` tag + the context-writer seam LogApiCall stamps through. The Node\n// RequestContext-backed impl is installed by @webpieces/core-context; the browser gets the no-op.\nexport { ApiCallInfo } from './http/ApiCallInfo';\nexport type { ApiType, ApiResult } from './http/ApiCallInfo';\n// Console-render bridge: turns LogApiCall's [LogApiCall] bracket into [API.{side}.{phase}] locally.\nexport { ApiCallLogName, ApiCallLogNameImpl, LOG_API_CALL_LOGGER_NAME } from './http/ApiCallLogName';\nexport { ApiMethodInfo } from './http/ApiMethodInfo';\nexport type { ApiSide } from './http/ApiMethodInfo';\nexport { ApiCallContextHolder } from './http/ApiCallContext';\nexport type { ApiCallContext } from './http/ApiCallContext';\n\n// Test-case recording contract (impl lives in http-server; hooks in http-client)\nexport { TestCaseRecorder, RecorderKeys } from './http/recorder/TestCaseRecorder';\nexport { RecordedEndpoint, RecordedError, RecordedTestCase } from './http/recorder/RecordedEndpoint';\nexport { DoNotRecord, getDoNotRecordFields } from './http/recorder/DoNotRecord';\nexport { RecordSerializer, SerializedMap, SerializedError } from './http/recorder/RecordSerializer';\n"]}
|