@webpieces/core-util 0.4.794 → 0.4.796

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/package.json +1 -1
  2. package/src/errors/ApiError.d.ts +9 -5
  3. package/src/errors/ApiError.js +9 -4
  4. package/src/errors/ApiError.js.map +1 -1
  5. package/src/errors/ApiErrorCodec.js +5 -0
  6. package/src/errors/ApiErrorCodec.js.map +1 -1
  7. package/src/errors/ReceivedApiErrorRule.d.ts +5 -3
  8. package/src/errors/ReceivedApiErrorRule.js +23 -4
  9. package/src/errors/ReceivedApiErrorRule.js.map +1 -1
  10. package/src/errors/index.d.ts +1 -1
  11. package/src/errors/index.js +2 -1
  12. package/src/errors/index.js.map +1 -1
  13. package/src/http/ApiCallTimeoutError.d.ts +2 -1
  14. package/src/http/ApiCallTimeoutError.js +2 -1
  15. package/src/http/ApiCallTimeoutError.js.map +1 -1
  16. package/src/http/ApiErrorHttpStatus.js +1 -0
  17. package/src/http/ApiErrorHttpStatus.js.map +1 -1
  18. package/src/http/EndpointCompileAssertions.d.ts +1 -0
  19. package/src/http/EndpointCompileAssertions.js +21 -0
  20. package/src/http/EndpointCompileAssertions.js.map +1 -0
  21. package/src/http/HttpContract.d.ts +10 -1
  22. package/src/http/HttpContract.js +10 -1
  23. package/src/http/HttpContract.js.map +1 -1
  24. package/src/http/HttpEndpointOptions.d.ts +14 -3
  25. package/src/http/HttpEndpointOptions.js +12 -0
  26. package/src/http/HttpEndpointOptions.js.map +1 -1
  27. package/src/http/RouteMetadata.d.ts +6 -1
  28. package/src/http/RouteMetadata.js +5 -1
  29. package/src/http/RouteMetadata.js.map +1 -1
  30. package/src/http/RouteMetadataFactory.d.ts +3 -3
  31. package/src/http/RouteMetadataFactory.js +13 -16
  32. package/src/http/RouteMetadataFactory.js.map +1 -1
  33. package/src/http/WebpiecesDefaultErrorTranslator.d.ts +5 -3
  34. package/src/http/WebpiecesDefaultErrorTranslator.js +17 -4
  35. package/src/http/WebpiecesDefaultErrorTranslator.js.map +1 -1
  36. package/src/http/api-kind.js +2 -2
  37. package/src/http/api-kind.js.map +1 -1
  38. package/src/http/decorators.d.ts +41 -143
  39. package/src/http/decorators.js +123 -110
  40. package/src/http/decorators.js.map +1 -1
  41. package/src/http/external-caller.d.ts +4 -4
  42. package/src/http/external-caller.js +12 -5
  43. package/src/http/external-caller.js.map +1 -1
  44. package/src/index.d.ts +7 -5
  45. package/src/index.js +21 -5
  46. package/src/index.js.map +1 -1
  47. package/src/mcp/McpCompileAssertions.js +3 -9
  48. package/src/mcp/McpCompileAssertions.js.map +1 -1
  49. package/src/mcp/McpMetadata.d.ts +8 -14
  50. package/src/mcp/McpMetadata.js +19 -5
  51. package/src/mcp/McpMetadata.js.map +1 -1
@@ -1 +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"]}
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 {\n EndpointKind,\n RPC,\n CLOUDTASKS,\n CRON,\n EXTERNAL,\n METADATA_KEYS,\n getEndpoints,\n getEndpointKinds,\n isApiPath,\n} 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"]}
@@ -1,8 +1,13 @@
1
1
  import 'reflect-metadata';
2
2
  import { MaskSpec, MaskMode } from './LogFieldMask';
3
3
  import { ApiKeyCredentials, AuthMeta, AuthMode, JwtRequirement } from './auth-mode';
4
- import { EndpointOptions, ExternalEndpointOptions } from './HttpEndpointOptions';
4
+ import { EndpointOperation, EndpointOptions, ExternalEndpointOptions } from './HttpEndpointOptions';
5
+ import { HttpMethod } from './HttpContract';
5
6
  export type { EndpointOptions, ExternalEndpointOptions } from './HttpEndpointOptions';
7
+ export type { EndpointOperation } from './HttpEndpointOptions';
8
+ export { READ, WRITE_IDEMPOTENT, WRITE } from './HttpEndpointOptions';
9
+ export type { HttpMethod } from './HttpContract';
10
+ export { GET, POST } from './HttpContract';
6
11
  export { PathParam, QueryParam, getHttpParameterDeclarations } from './http-parameter-decorators';
7
12
  /**
8
13
  * Metadata keys for storing API routing information.
@@ -20,6 +25,10 @@ export declare const METADATA_KEYS: {
20
25
  QUEUE_OVERRIDE: string;
21
26
  /** Per-method @Endpoint options (e.g. formPost), parallel to ENDPOINTS. */
22
27
  ENDPOINT_OPTIONS: string;
28
+ /** Per-method required HTTP method, parallel to ENDPOINTS. */
29
+ ENDPOINT_HTTP_METHOD: string;
30
+ /** Per-method required read/write semantics, parallel to ENDPOINTS. */
31
+ ENDPOINT_OPERATION: string;
23
32
  /** Per-method @Endpoint trigger kind (rpc | cloudtasks | cron | external), parallel to ENDPOINTS. */
24
33
  ENDPOINT_KIND: string;
25
34
  /** Per-method declared external CALLER (only for kind 'external'), parallel to ENDPOINTS. */
@@ -35,99 +44,30 @@ export declare const METADATA_KEYS: {
35
44
  /** Per-method explicit path/query parameter declarations, keyed by parameter index. */
36
45
  HTTP_PARAMETERS: string;
37
46
  };
38
- /**
39
- * WHAT TRIGGERS an endpoint at runtime — the single fact that decides how the runtime architecture
40
- * graph draws it, and which Terraform resource must exist for it to ever fire:
41
- *
42
- * - `rpc` — a caller in this repo (or a browser) calls it synchronously. A direct arrow.
43
- * - `cloudtasks` — a producer ENQUEUES it; Cloud Tasks delivers it later. Drawn producer → queue →
44
- * consumer, one queue node per METHOD (see {@link Queue}). Producer and consumer
45
- * being the SAME service is legal and common — the queue decouples them.
46
- * - `cron` — a scheduler fires it on a clock. Nothing in-repo calls it; drawn hanging off a
47
- * clock symbol. Backed by a Cloud Scheduler job.
48
- * - `external` — a system OUTSIDE this repo drives it (a GCP Pub/Sub push subscription, a Twilio
49
- * or Gmail webhook). Drawn as an inbound dashed arrow from that system.
50
- *
51
- * Declared PER METHOD, because one api class routinely mixes them: an admin contract can have
52
- * caller-driven endpoints AND a nightly cron sweep. A class-level marker cannot express that, which
53
- * is exactly why the graph could not tell these apart before.
54
- */
55
- export type EndpointKind = 'rpc' | 'cloudtasks' | 'cron' | 'external';
56
- /**
57
- * Options for a single @Endpoint. Kept in a metadata map PARALLEL to ENDPOINTS so the existing
58
- * `Record<methodName, path>` shape every consumer iterates stays unchanged.
59
- */
60
- /**
61
- * @ApiPath(basePath) - Class decorator that marks a class as an API definition
62
- * and sets the base path for all endpoints.
63
- *
64
- * Usage:
65
- * ```typescript
66
- * @ApiPath('/api/save')
67
- * abstract class SaveApi {
68
- * @WpAuthJwt({ roles: ['admin'] })
69
- * @Endpoint('/item', 'rpc')
70
- * save(request: SaveRequest): Promise<SaveResponse> { ... }
71
- * }
72
- * ```
73
- */
47
+ /** Nominal backing keeps raw string literals out of endpoint declarations. */
48
+ declare enum EndpointKindValue {
49
+ RPC = "rpc",
50
+ CLOUDTASKS = "cloudtasks",
51
+ CRON = "cron",
52
+ EXTERNAL = "external"
53
+ }
54
+ /** Short, statically importable decorator arguments. */
55
+ export declare const RPC = EndpointKindValue.RPC;
56
+ export declare const CLOUDTASKS = EndpointKindValue.CLOUDTASKS;
57
+ export declare const CRON = EndpointKindValue.CRON;
58
+ export declare const EXTERNAL = EndpointKindValue.EXTERNAL;
59
+ /** Runtime trigger/delivery owner used by validation, architecture, and infrastructure tooling. */
60
+ export type EndpointKind = typeof RPC | typeof CLOUDTASKS | typeof CRON | typeof EXTERNAL;
61
+ /** Class decorator that sets the shared base path for all endpoints. */
74
62
  export declare function ApiPath(basePath: string): ClassDecorator;
75
63
  /**
76
- * @Endpoint(path, kind, options?) - Method decorator that registers an HTTP endpoint at the given
77
- * path and declares WHAT TRIGGERS it. POST is the default; GET is explicit in `httpMethod`.
78
- *
79
- * Usage:
80
- * ```typescript
81
- * @Endpoint('/item', 'rpc')
82
- * save(request: SaveRequest): Promise<SaveResponse> { ... }
83
- *
84
- * // enqueued by a producer, delivered later by Cloud Tasks:
85
- * @Endpoint('/send', 'cloudtasks')
86
- * send(request: SendRequest): Promise<void> { ... }
87
- *
88
- * // fired by Cloud Scheduler on a clock, called by nobody in this repo:
89
- * @Endpoint('/nightly', 'cron')
90
- * nightly(request: NightlyRequest): Promise<void> { ... }
91
- *
92
- * // EXTERNAL webhook posting application/x-www-form-urlencoded (e.g. Twilio):
93
- * @Endpoint('/hook', 'external', { formPost: true, calledBy: 'twilio' })
94
- * inbound(request: InboundRequest): Promise<InboundResponse> { ... }
95
- * ```
96
- *
97
- * `kind` is REQUIRED and deliberately positional: it makes every pre-existing single-argument
98
- * `@Endpoint('/x')` a COMPILE error rather than something a lint rule has to chase, so no endpoint
99
- * can slip into the runtime architecture graph with its trigger left to guesswork. See
100
- * {@link EndpointKind} for what each value draws and which Terraform resource backs it.
101
- *
102
- * `calledBy` is REQUIRED for `external` FOR EXACTLY THE SAME REASON, enforced by the overloads below:
103
- * the one box on the runtime graph whose whole job is to say who calls us from outside could only
104
- * restate OUR OWN contract name, because nothing in the source ever said who the caller was. This is
105
- * BREAKING for published consumers, intentionally — an existing `@Endpoint(p, 'external', {...})`
106
- * stops compiling until it names its caller. Migration is one property; see the migration note in
107
- * `external-caller.ts`. Non-`external` endpoints are completely unaffected.
108
- *
109
- * The path write to ENDPOINTS is UNCHANGED (every consumer iterates `[methodName, path]`); kind,
110
- * options and caller ride PARALLEL ENDPOINT_KIND / ENDPOINT_OPTIONS / ENDPOINT_CALLER maps.
111
- */
112
- export declare function Endpoint(path: string, kind: 'external', options: ExternalEndpointOptions): MethodDecorator;
113
- export declare function Endpoint(path: string, kind: Exclude<EndpointKind, 'external'>, options?: EndpointOptions): MethodDecorator;
114
- /**
115
- * @MaskLog(fields) - declare which fields of THIS method's request/response DTOs the
116
- * {@link LogApiCallImpl} logging path must mask, so a secret riding on a DTO (an OAuth refresh token, an
117
- * id-token JWT) is never written to the logs in cleartext. The REAL value still travels on the wire
118
- * untouched — masking lives in the logging path only.
119
- *
120
- * ```typescript
121
- * @Endpoint('/account', 'rpc')
122
- * @MaskLog({ refreshToken: 'full', accessToken: 'last4', credential: 'full' })
123
- * getEmailAccount(request: GetEmailAccountRequest): Promise<GetEmailAccountResponse> { ... }
124
- * ```
125
- *
126
- * Matching is by field NAME at any depth (nested objects + array elements), so
127
- * `response.account.refreshToken` is masked. Declared on the SHARED api contract, so BOTH the client
128
- * `[API-client-*]` and server `[API-server-*]` lines mask it. The spec is read ONCE at route-build
129
- * time and rides {@link RouteMetadata.mask}, so an unmasked method pays nothing at call time.
64
+ * Registers `@Endpoint(POST, path, WRITE, RPC)`. Method, operation, and trigger are required enum
65
+ * values. Operation is independent of the HTTP verb. EXTERNAL additionally requires `calledBy`.
66
+ * Each classification rides a parallel metadata map while ENDPOINTS remains method-name -> path.
130
67
  */
68
+ export declare function Endpoint(httpMethod: HttpMethod, path: string, operation: EndpointOperation, kind: typeof EXTERNAL, options: ExternalEndpointOptions): MethodDecorator;
69
+ export declare function Endpoint(httpMethod: HttpMethod, path: string, operation: EndpointOperation, kind: Exclude<EndpointKind, typeof EXTERNAL>, options?: EndpointOptions): MethodDecorator;
70
+ /** Declares request/response DTO field names that API logging must mask at any depth. */
131
71
  export declare function MaskLog(fields: Record<string, MaskMode>): MethodDecorator;
132
72
  /**
133
73
  * The @MaskLog spec for one method, or undefined if the method declared none (the common case — the
@@ -186,7 +126,7 @@ export declare function WpAuthSharedSecret(key: string): MethodDecorator;
186
126
  *
187
127
  * ```typescript
188
128
  * @WpAuthWebhook('sentry')
189
- * @Endpoint('/hook/sentry/issue', 'external', { calledBy: 'sentry', rawBody: true })
129
+ * @Endpoint(POST, '/hook/sentry/issue', WRITE, EXTERNAL, { calledBy: 'sentry', rawBody: true })
190
130
  * abstract notify(request: SentryIssueHook): Promise<HookAck>;
191
131
  * ```
192
132
  *
@@ -208,56 +148,10 @@ export declare function WpAuthSharedSecret(key: string): MethodDecorator;
208
148
  */
209
149
  export declare function WpAuthWebhook(name: string): MethodDecorator;
210
150
  /**
211
- * @WpAuthApiKey(regime, credentials) - a CUSTOMER holds the credential. The app's bound `ApiKeyHook`
212
- * authenticates the inbound request against its own datastore and returns the `ContextTuple` entries
213
- * the framework seeds into `RequestContext`. THE mode for a partner-facing contract consumed by other
214
- * companies' codebases (POS vendors, back-office platforms, ETL pipelines).
215
- *
216
- * ```typescript
217
- * @WpAuthApiKey('onetablet-partner', [
218
- * { in: 'header', name: 'x-api-key', description: 'The key issued to your integration.' },
219
- * { in: 'header', name: 'x-organization-id', description: 'Which of your organizations to act on.' },
220
- * ])
221
- * @ApiPath('/management/v1')
222
- * abstract class ManagementApi { ... }
223
- * ```
224
- *
225
- * `regime` is a bare STRING selecting WHICH key regime this route belongs to, exactly as
226
- * `@WpAuthSharedSecret(key)` and `@WpAuthWebhook(vendor)` already are — one hook serves several regimes,
227
- * and an api contract is level 0, so it never references a verifier directly.
228
- *
229
- * `credentials` DECLARES where the credential rides ({@link ApiKeyCredential}), so a spec generator
230
- * reading route auth metadata can emit `components.securitySchemes` instead of a human hand-writing
231
- * them into a manifest. It is a NON-EMPTY, ORDERED list rather than one credential because a real
232
- * regime authenticates a PAIR — the key names a customer, a second header names which of that
233
- * customer's organizations the request acts on, and a mismatch is a 401. Its OpenAPI form is two
234
- * schemes plus ONE security-requirement object holding BOTH keys (an AND); a LIST of two objects
235
- * would mean "either alone suffices", which is a load-bearing difference a single-credential shape
236
- * cannot even express. ORDER IS SIGNIFICANT and preserved: it is the order the credentials are
237
- * presented in the published document.
238
- *
239
- * The list can never be EMPTY — a contract that declares a key regime and then names no credential
240
- * would generate a document with no security block, which is the exact silent failure this argument
241
- * exists to remove. That is a compile error, not a runtime throw (see {@link JwtRoles}'s non-empty
242
- * tuple, the same device for the same reason).
243
- *
244
- * WHY IT IS NOT `@WpAuthSharedSecret`. Shared-secret declares that AN INTERNAL SERVICE is on the other
245
- * end, so the framework BELIEVES the trusted context headers that caller forwarded (see
246
- * `DestinationTrust.forAuthMode` and `AuthFilter.verifiesCaller`). A customer is not an internal
247
- * service: declaring a partner endpoint `@WpAuthSharedSecret` would let that partner assert someone
248
- * else's org id on the wire and have it admitted — a privilege escalation. `apikey` therefore sits
249
- * with `jwt` on the caller-NOT-verified side, where an inbound trusted header is admitted only when
250
- * the hook independently derived the SAME value.
251
- *
252
- * WHY THE HOOK SEES THE REQUEST, NOT ONE TOKEN. A real key regime checks the key TOGETHER WITH a
253
- * second header (the organization it is acting for), and `JwtHook.parseJwt` — handed one pre-extracted
254
- * token from one header — physically cannot. `ApiKeyHook.verifyApiKey(regime, request)` gets the whole
255
- * inbound request instead, so the app owns which headers carry the credential and validates them as a
256
- * PAIR. `credentials` does NOT change that: the framework reads no header from it and performs no
257
- * extraction. It is DECLARATION for readers of the contract, and enforcement stays entirely the hook's.
258
- *
259
- * FAILS CLOSED: with no `ApiKeyHook` bound, every `@WpAuthApiKey` endpoint 401s, matching `JwtHook` and
260
- * `WebhookAuthCallback`.
151
+ * Declares a customer-held API-key regime and its non-empty ordered credential locations. The app's
152
+ * `ApiKeyHook` validates the whole request and supplies trusted context; the declaration only drives
153
+ * contract/spec metadata. Unlike shared-secret auth, partner callers cannot assert trusted context.
154
+ * Missing hooks fail closed with 401.
261
155
  */
262
156
  export declare function WpAuthApiKey(regime: string, credentials: ApiKeyCredentials): MethodDecorator;
263
157
  /**
@@ -267,7 +161,7 @@ export declare function WpAuthApiKey(regime: string, credentials: ApiKeyCredenti
267
161
  *
268
162
  * ```typescript
269
163
  * @WpAuthLocalOnly()
270
- * @Endpoint('/logs', 'rpc')
164
+ * @Endpoint(POST, '/logs', WRITE, RPC)
271
165
  * sendBatch(request: SendLogBatchRequest): Promise<SendLogBatchResponse> { ... }
272
166
  * ```
273
167
  *
@@ -311,10 +205,14 @@ export declare function getEndpointKinds(apiClass: Function): Record<string, End
311
205
  * the required argument exists to remove.
312
206
  */
313
207
  export declare function getEndpointKind(apiClass: Function, methodName: string): EndpointKind | undefined;
208
+ /** The required HTTP method for one endpoint. */
209
+ export declare function getEndpointHttpMethod(apiClass: Function, methodName: string): HttpMethod;
314
210
  /**
315
211
  * Get the @Endpoint options for one method (empty object if the method had no options).
316
212
  */
317
213
  export declare function getEndpointOptions(apiClass: Function, methodName: string): EndpointOptions;
214
+ /** Read one endpoint's required side-effect semantics. */
215
+ export declare function getEndpointOperation(apiClass: Function, methodName: string): EndpointOperation;
318
216
  /**
319
217
  * Fail-fast at wiring time when an `external` endpoint declared no caller. The {@link Endpoint}
320
218
  * overloads already make that a COMPILE error; this is the backstop for the ways TS is bypassed —
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MISSING_AUTH_DECORATOR_FIX = exports.METADATA_KEYS = exports.getHttpParameterDeclarations = exports.QueryParam = exports.PathParam = void 0;
3
+ exports.MISSING_AUTH_DECORATOR_FIX = exports.EXTERNAL = exports.CRON = exports.CLOUDTASKS = exports.RPC = exports.METADATA_KEYS = exports.getHttpParameterDeclarations = exports.QueryParam = exports.PathParam = exports.POST = exports.GET = exports.WRITE = exports.WRITE_IDEMPOTENT = exports.READ = void 0;
4
4
  exports.ApiPath = ApiPath;
5
5
  exports.Endpoint = Endpoint;
6
6
  exports.MaskLog = MaskLog;
@@ -17,7 +17,9 @@ exports.getApiPath = getApiPath;
17
17
  exports.getEndpoints = getEndpoints;
18
18
  exports.getEndpointKinds = getEndpointKinds;
19
19
  exports.getEndpointKind = getEndpointKind;
20
+ exports.getEndpointHttpMethod = getEndpointHttpMethod;
20
21
  exports.getEndpointOptions = getEndpointOptions;
22
+ exports.getEndpointOperation = getEndpointOperation;
21
23
  exports.assertEveryExternalEndpointDeclaresCaller = assertEveryExternalEndpointDeclaresCaller;
22
24
  exports.isFormPost = isFormPost;
23
25
  exports.isRawBody = isRawBody;
@@ -32,7 +34,16 @@ const LogFieldMask_1 = require("./LogFieldMask");
32
34
  const external_caller_1 = require("./external-caller");
33
35
  // The TYPE layer these decorators attach — split out for file size only (see auth-mode.ts).
34
36
  const auth_mode_1 = require("./auth-mode");
37
+ const HttpEndpointOptions_1 = require("./HttpEndpointOptions");
38
+ const HttpContract_1 = require("./HttpContract");
35
39
  const http_parameter_decorators_1 = require("./http-parameter-decorators");
40
+ var HttpEndpointOptions_2 = require("./HttpEndpointOptions");
41
+ Object.defineProperty(exports, "READ", { enumerable: true, get: function () { return HttpEndpointOptions_2.READ; } });
42
+ Object.defineProperty(exports, "WRITE_IDEMPOTENT", { enumerable: true, get: function () { return HttpEndpointOptions_2.WRITE_IDEMPOTENT; } });
43
+ Object.defineProperty(exports, "WRITE", { enumerable: true, get: function () { return HttpEndpointOptions_2.WRITE; } });
44
+ var HttpContract_2 = require("./HttpContract");
45
+ Object.defineProperty(exports, "GET", { enumerable: true, get: function () { return HttpContract_2.GET; } });
46
+ Object.defineProperty(exports, "POST", { enumerable: true, get: function () { return HttpContract_2.POST; } });
36
47
  var http_parameter_decorators_2 = require("./http-parameter-decorators");
37
48
  Object.defineProperty(exports, "PathParam", { enumerable: true, get: function () { return http_parameter_decorators_2.PathParam; } });
38
49
  Object.defineProperty(exports, "QueryParam", { enumerable: true, get: function () { return http_parameter_decorators_2.QueryParam; } });
@@ -53,6 +64,10 @@ exports.METADATA_KEYS = {
53
64
  QUEUE_OVERRIDE: 'webpieces:queue-override',
54
65
  /** Per-method @Endpoint options (e.g. formPost), parallel to ENDPOINTS. */
55
66
  ENDPOINT_OPTIONS: 'webpieces:endpoint-options',
67
+ /** Per-method required HTTP method, parallel to ENDPOINTS. */
68
+ ENDPOINT_HTTP_METHOD: 'webpieces:endpoint-http-method',
69
+ /** Per-method required read/write semantics, parallel to ENDPOINTS. */
70
+ ENDPOINT_OPERATION: 'webpieces:endpoint-operation',
56
71
  /** Per-method @Endpoint trigger kind (rpc | cloudtasks | cron | external), parallel to ENDPOINTS. */
57
72
  ENDPOINT_KIND: 'webpieces:endpoint-kind',
58
73
  /** Per-method declared external CALLER (only for kind 'external'), parallel to ENDPOINTS. */
@@ -68,24 +83,20 @@ exports.METADATA_KEYS = {
68
83
  /** Per-method explicit path/query parameter declarations, keyed by parameter index. */
69
84
  HTTP_PARAMETERS: http_parameter_decorators_1.HTTP_PARAMETERS_METADATA_KEY,
70
85
  };
71
- /**
72
- * Options for a single @Endpoint. Kept in a metadata map PARALLEL to ENDPOINTS so the existing
73
- * `Record<methodName, path>` shape every consumer iterates stays unchanged.
74
- */
75
- /**
76
- * @ApiPath(basePath) - Class decorator that marks a class as an API definition
77
- * and sets the base path for all endpoints.
78
- *
79
- * Usage:
80
- * ```typescript
81
- * @ApiPath('/api/save')
82
- * abstract class SaveApi {
83
- * @WpAuthJwt({ roles: ['admin'] })
84
- * @Endpoint('/item', 'rpc')
85
- * save(request: SaveRequest): Promise<SaveResponse> { ... }
86
- * }
87
- * ```
88
- */
86
+ /** Nominal backing keeps raw string literals out of endpoint declarations. */
87
+ var EndpointKindValue;
88
+ (function (EndpointKindValue) {
89
+ EndpointKindValue["RPC"] = "rpc";
90
+ EndpointKindValue["CLOUDTASKS"] = "cloudtasks";
91
+ EndpointKindValue["CRON"] = "cron";
92
+ EndpointKindValue["EXTERNAL"] = "external";
93
+ })(EndpointKindValue || (EndpointKindValue = {}));
94
+ /** Short, statically importable decorator arguments. */
95
+ exports.RPC = EndpointKindValue.RPC;
96
+ exports.CLOUDTASKS = EndpointKindValue.CLOUDTASKS;
97
+ exports.CRON = EndpointKindValue.CRON;
98
+ exports.EXTERNAL = EndpointKindValue.EXTERNAL;
99
+ /** Class decorator that sets the shared base path for all endpoints. */
89
100
  function ApiPath(basePath) {
90
101
  return (target) => {
91
102
  if (Reflect.hasMetadata('webpieces:ipc-api-id', target)) {
@@ -99,48 +110,53 @@ function ApiPath(basePath) {
99
110
  };
100
111
  }
101
112
  // webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope
102
- function Endpoint(path, kind, options = {}) {
113
+ function Endpoint(httpMethod, path, operation, kind, options = {}) {
114
+ validateEndpointClassification(httpMethod, operation, kind);
103
115
  // webpieces-disable no-any-unknown -- reflect-metadata decorator API requires any
104
116
  return (target, propertyKey, _descriptor) => {
105
117
  const metadataTarget = typeof target === 'function' ? target : target.constructor;
106
- const endpoints = Reflect.getMetadata(exports.METADATA_KEYS.ENDPOINTS, metadataTarget) || {};
107
- endpoints[propertyKey] = path;
108
- Reflect.defineMetadata(exports.METADATA_KEYS.ENDPOINTS, endpoints, metadataTarget);
109
- const kinds = Reflect.getMetadata(exports.METADATA_KEYS.ENDPOINT_KIND, metadataTarget) || {};
110
- kinds[propertyKey] = kind;
111
- Reflect.defineMetadata(exports.METADATA_KEYS.ENDPOINT_KIND, kinds, metadataTarget);
112
- const opts = Reflect.getMetadata(exports.METADATA_KEYS.ENDPOINT_OPTIONS, metadataTarget) || {};
113
- opts[propertyKey] = options;
114
- Reflect.defineMetadata(exports.METADATA_KEYS.ENDPOINT_OPTIONS, opts, metadataTarget);
115
- // ONLY for 'external', mirroring how a queue name is recorded only for the kinds that HAVE
116
- // a queue: a caller on an rpc endpoint would be a fact about nothing.
117
- const declared = options;
118
- if (kind !== 'external' ||
119
- typeof declared.calledBy !== 'string' ||
120
- declared.calledBy === '')
121
- return;
122
- const callers = Reflect.getMetadata(exports.METADATA_KEYS.ENDPOINT_CALLER, metadataTarget) || {};
123
- callers[propertyKey] = new external_caller_1.ExternalCaller(declared.callerKind ?? external_caller_1.DEFAULT_CALLER_KIND, declared.calledBy);
124
- Reflect.defineMetadata(exports.METADATA_KEYS.ENDPOINT_CALLER, callers, metadataTarget);
118
+ recordEndpointMetadata(metadataTarget, propertyKey, httpMethod, path, operation, kind, options);
125
119
  };
126
120
  }
127
- /**
128
- * @MaskLog(fields) - declare which fields of THIS method's request/response DTOs the
129
- * {@link LogApiCallImpl} logging path must mask, so a secret riding on a DTO (an OAuth refresh token, an
130
- * id-token JWT) is never written to the logs in cleartext. The REAL value still travels on the wire
131
- * untouched — masking lives in the logging path only.
132
- *
133
- * ```typescript
134
- * @Endpoint('/account', 'rpc')
135
- * @MaskLog({ refreshToken: 'full', accessToken: 'last4', credential: 'full' })
136
- * getEmailAccount(request: GetEmailAccountRequest): Promise<GetEmailAccountResponse> { ... }
137
- * ```
138
- *
139
- * Matching is by field NAME at any depth (nested objects + array elements), so
140
- * `response.account.refreshToken` is masked. Declared on the SHARED api contract, so BOTH the client
141
- * `[API-client-*]` and server `[API-server-*]` lines mask it. The spec is read ONCE at route-build
142
- * time and rides {@link RouteMetadata.mask}, so an unmasked method pays nothing at call time.
143
- */
121
+ // webpieces-disable no-function-outside-class -- runtime validation backs up nominal TS enums
122
+ function validateEndpointClassification(httpMethod, operation, kind) {
123
+ if (!isHttpMethod(httpMethod)) {
124
+ throw new Error(`@Endpoint httpMethod must be GET or POST, received '${String(httpMethod)}'.`);
125
+ }
126
+ if (!isEndpointOperation(operation)) {
127
+ throw new Error(`@Endpoint operation must be READ, WRITE_IDEMPOTENT, or WRITE, received '${String(operation)}'.`);
128
+ }
129
+ if (!isEndpointKind(kind)) {
130
+ throw new Error(`@Endpoint trigger must be RPC, CLOUDTASKS, CRON, or EXTERNAL, received '${String(kind)}'.`);
131
+ }
132
+ }
133
+ // webpieces-disable no-function-outside-class -- one metadata write shared by the decorator overloads
134
+ function recordEndpointMetadata(metadataTarget, propertyKey, httpMethod, path, operation, kind, options) {
135
+ const endpoints = Reflect.getMetadata(exports.METADATA_KEYS.ENDPOINTS, metadataTarget) || {};
136
+ endpoints[propertyKey] = path;
137
+ Reflect.defineMetadata(exports.METADATA_KEYS.ENDPOINTS, endpoints, metadataTarget);
138
+ const methods = Reflect.getMetadata(exports.METADATA_KEYS.ENDPOINT_HTTP_METHOD, metadataTarget) || {};
139
+ methods[propertyKey] = httpMethod;
140
+ Reflect.defineMetadata(exports.METADATA_KEYS.ENDPOINT_HTTP_METHOD, methods, metadataTarget);
141
+ const operations = Reflect.getMetadata(exports.METADATA_KEYS.ENDPOINT_OPERATION, metadataTarget) || {};
142
+ operations[propertyKey] = operation;
143
+ Reflect.defineMetadata(exports.METADATA_KEYS.ENDPOINT_OPERATION, operations, metadataTarget);
144
+ const kinds = Reflect.getMetadata(exports.METADATA_KEYS.ENDPOINT_KIND, metadataTarget) || {};
145
+ kinds[propertyKey] = kind;
146
+ Reflect.defineMetadata(exports.METADATA_KEYS.ENDPOINT_KIND, kinds, metadataTarget);
147
+ const opts = Reflect.getMetadata(exports.METADATA_KEYS.ENDPOINT_OPTIONS, metadataTarget) || {};
148
+ opts[propertyKey] = options;
149
+ Reflect.defineMetadata(exports.METADATA_KEYS.ENDPOINT_OPTIONS, opts, metadataTarget);
150
+ const declared = options;
151
+ if (kind !== exports.EXTERNAL ||
152
+ typeof declared.calledBy !== 'string' ||
153
+ declared.calledBy === '')
154
+ return;
155
+ const callers = Reflect.getMetadata(exports.METADATA_KEYS.ENDPOINT_CALLER, metadataTarget) || {};
156
+ callers[propertyKey] = new external_caller_1.ExternalCaller(declared.callerKind ?? external_caller_1.DEFAULT_CALLER_KIND, declared.calledBy);
157
+ Reflect.defineMetadata(exports.METADATA_KEYS.ENDPOINT_CALLER, callers, metadataTarget);
158
+ }
159
+ /** Declares request/response DTO field names that API logging must mask at any depth. */
144
160
  // webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope
145
161
  function MaskLog(fields) {
146
162
  const spec = new LogFieldMask_1.MaskSpec(fields);
@@ -254,7 +270,7 @@ function WpAuthSharedSecret(key) {
254
270
  *
255
271
  * ```typescript
256
272
  * @WpAuthWebhook('sentry')
257
- * @Endpoint('/hook/sentry/issue', 'external', { calledBy: 'sentry', rawBody: true })
273
+ * @Endpoint(POST, '/hook/sentry/issue', WRITE, EXTERNAL, { calledBy: 'sentry', rawBody: true })
258
274
  * abstract notify(request: SentryIssueHook): Promise<HookAck>;
259
275
  * ```
260
276
  *
@@ -279,56 +295,10 @@ function WpAuthWebhook(name) {
279
295
  return defineAuthMode({ kind: 'webhook', name });
280
296
  }
281
297
  /**
282
- * @WpAuthApiKey(regime, credentials) - a CUSTOMER holds the credential. The app's bound `ApiKeyHook`
283
- * authenticates the inbound request against its own datastore and returns the `ContextTuple` entries
284
- * the framework seeds into `RequestContext`. THE mode for a partner-facing contract consumed by other
285
- * companies' codebases (POS vendors, back-office platforms, ETL pipelines).
286
- *
287
- * ```typescript
288
- * @WpAuthApiKey('onetablet-partner', [
289
- * { in: 'header', name: 'x-api-key', description: 'The key issued to your integration.' },
290
- * { in: 'header', name: 'x-organization-id', description: 'Which of your organizations to act on.' },
291
- * ])
292
- * @ApiPath('/management/v1')
293
- * abstract class ManagementApi { ... }
294
- * ```
295
- *
296
- * `regime` is a bare STRING selecting WHICH key regime this route belongs to, exactly as
297
- * `@WpAuthSharedSecret(key)` and `@WpAuthWebhook(vendor)` already are — one hook serves several regimes,
298
- * and an api contract is level 0, so it never references a verifier directly.
299
- *
300
- * `credentials` DECLARES where the credential rides ({@link ApiKeyCredential}), so a spec generator
301
- * reading route auth metadata can emit `components.securitySchemes` instead of a human hand-writing
302
- * them into a manifest. It is a NON-EMPTY, ORDERED list rather than one credential because a real
303
- * regime authenticates a PAIR — the key names a customer, a second header names which of that
304
- * customer's organizations the request acts on, and a mismatch is a 401. Its OpenAPI form is two
305
- * schemes plus ONE security-requirement object holding BOTH keys (an AND); a LIST of two objects
306
- * would mean "either alone suffices", which is a load-bearing difference a single-credential shape
307
- * cannot even express. ORDER IS SIGNIFICANT and preserved: it is the order the credentials are
308
- * presented in the published document.
309
- *
310
- * The list can never be EMPTY — a contract that declares a key regime and then names no credential
311
- * would generate a document with no security block, which is the exact silent failure this argument
312
- * exists to remove. That is a compile error, not a runtime throw (see {@link JwtRoles}'s non-empty
313
- * tuple, the same device for the same reason).
314
- *
315
- * WHY IT IS NOT `@WpAuthSharedSecret`. Shared-secret declares that AN INTERNAL SERVICE is on the other
316
- * end, so the framework BELIEVES the trusted context headers that caller forwarded (see
317
- * `DestinationTrust.forAuthMode` and `AuthFilter.verifiesCaller`). A customer is not an internal
318
- * service: declaring a partner endpoint `@WpAuthSharedSecret` would let that partner assert someone
319
- * else's org id on the wire and have it admitted — a privilege escalation. `apikey` therefore sits
320
- * with `jwt` on the caller-NOT-verified side, where an inbound trusted header is admitted only when
321
- * the hook independently derived the SAME value.
322
- *
323
- * WHY THE HOOK SEES THE REQUEST, NOT ONE TOKEN. A real key regime checks the key TOGETHER WITH a
324
- * second header (the organization it is acting for), and `JwtHook.parseJwt` — handed one pre-extracted
325
- * token from one header — physically cannot. `ApiKeyHook.verifyApiKey(regime, request)` gets the whole
326
- * inbound request instead, so the app owns which headers carry the credential and validates them as a
327
- * PAIR. `credentials` does NOT change that: the framework reads no header from it and performs no
328
- * extraction. It is DECLARATION for readers of the contract, and enforcement stays entirely the hook's.
329
- *
330
- * FAILS CLOSED: with no `ApiKeyHook` bound, every `@WpAuthApiKey` endpoint 401s, matching `JwtHook` and
331
- * `WebhookAuthCallback`.
298
+ * Declares a customer-held API-key regime and its non-empty ordered credential locations. The app's
299
+ * `ApiKeyHook` validates the whole request and supplies trusted context; the declaration only drives
300
+ * contract/spec metadata. Unlike shared-secret auth, partner callers cannot assert trusted context.
301
+ * Missing hooks fail closed with 401.
332
302
  */
333
303
  // webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope
334
304
  function WpAuthApiKey(regime, credentials) {
@@ -341,7 +311,7 @@ function WpAuthApiKey(regime, credentials) {
341
311
  *
342
312
  * ```typescript
343
313
  * @WpAuthLocalOnly()
344
- * @Endpoint('/logs', 'rpc')
314
+ * @Endpoint(POST, '/logs', WRITE, RPC)
345
315
  * sendBatch(request: SendLogBatchRequest): Promise<SendLogBatchResponse> { ... }
346
316
  * ```
347
317
  *
@@ -401,13 +371,56 @@ function getEndpointKinds(apiClass) {
401
371
  function getEndpointKind(apiClass, methodName) {
402
372
  return getEndpointKinds(apiClass)[methodName];
403
373
  }
374
+ /** The required HTTP method for one endpoint. */
375
+ // webpieces-disable no-function-outside-class -- reflect-metadata reader, sibling of getEndpointKind
376
+ function getEndpointHttpMethod(apiClass, methodName) {
377
+ const methods = Reflect.getMetadata(exports.METADATA_KEYS.ENDPOINT_HTTP_METHOD, apiClass) || {};
378
+ const method = methods[methodName];
379
+ if (!isHttpMethod(method)) {
380
+ throw new Error(`@Endpoint ${apiClass.name || 'Unknown'}.${methodName} must declare GET or POST.`);
381
+ }
382
+ return method;
383
+ }
404
384
  /**
405
385
  * Get the @Endpoint options for one method (empty object if the method had no options).
406
386
  */
407
387
  // webpieces-disable no-function-outside-class -- reflect-metadata reader, sibling of getEndpoints
408
388
  function getEndpointOptions(apiClass, methodName) {
409
389
  const opts = Reflect.getMetadata(exports.METADATA_KEYS.ENDPOINT_OPTIONS, apiClass) || {};
410
- return opts[methodName] ?? {};
390
+ const options = opts[methodName];
391
+ if (!options && getEndpoints(apiClass)?.[methodName] === undefined) {
392
+ // Preserve the historical helper behavior for a method that is not an endpoint at all.
393
+ return {};
394
+ }
395
+ return options ?? {};
396
+ }
397
+ /** Read one endpoint's required side-effect semantics. */
398
+ // webpieces-disable no-function-outside-class -- reflect-metadata reader, sibling of getEndpointOptions
399
+ function getEndpointOperation(apiClass, methodName) {
400
+ const operations = Reflect.getMetadata(exports.METADATA_KEYS.ENDPOINT_OPERATION, apiClass) || {};
401
+ const operation = operations[methodName];
402
+ if (!isEndpointOperation(operation)) {
403
+ throw new Error(`@Endpoint ${apiClass.name || 'Unknown'}.${methodName} must declare READ, ` +
404
+ 'WRITE_IDEMPOTENT, or WRITE.');
405
+ }
406
+ return operation;
407
+ }
408
+ // webpieces-disable no-function-outside-class -- runtime backstop for JavaScript and `as any` callers; webpieces-disable no-any-unknown -- untrusted runtime value is narrowed to an endpoint operation
409
+ function isEndpointOperation(value) {
410
+ return (value === HttpEndpointOptions_1.READ ||
411
+ value === HttpEndpointOptions_1.WRITE_IDEMPOTENT ||
412
+ value === HttpEndpointOptions_1.WRITE);
413
+ }
414
+ // webpieces-disable no-function-outside-class -- runtime backstop for JavaScript and `as any` callers; webpieces-disable no-any-unknown -- untrusted runtime value is narrowed to an HTTP method
415
+ function isHttpMethod(value) {
416
+ return value === HttpContract_1.GET || value === HttpContract_1.POST;
417
+ }
418
+ // webpieces-disable no-function-outside-class -- runtime backstop for JavaScript and `as any` callers; webpieces-disable no-any-unknown -- untrusted runtime value is narrowed to an endpoint kind
419
+ function isEndpointKind(value) {
420
+ return (value === exports.RPC ||
421
+ value === exports.CLOUDTASKS ||
422
+ value === exports.CRON ||
423
+ value === exports.EXTERNAL);
411
424
  }
412
425
  /**
413
426
  * Fail-fast at wiring time when an `external` endpoint declared no caller. The {@link Endpoint}
@@ -423,7 +436,7 @@ function assertEveryExternalEndpointDeclaresCaller(apiClass) {
423
436
  (0, external_caller_1.getEndpointCaller)(apiClass, methodName) !== undefined)
424
437
  continue;
425
438
  throw new Error(`External endpoint '${methodName}' in ${apiClass.name || 'Unknown'} declares no caller. Say WHO ` +
426
- `posts to it: @Endpoint(path, 'external', { calledBy: '<vendor>' }) — the runtime architecture ` +
439
+ `posts to it: @Endpoint(POST, path, WRITE, EXTERNAL, { calledBy: '<vendor>' }) — the runtime architecture ` +
427
440
  `graph cannot name an inbound caller it was never told about.`);
428
441
  }
429
442
  }