@webpieces/cloudtasks-client 0.4.863 → 0.4.865

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,8 +7,13 @@ where it runs through the full server filter chain.
7
7
 
8
8
  ```ts
9
9
  // one shared contract
10
- @PubSub() @WpAuthOidc() @ApiPath('/email')
11
- abstract class EmailApi { @Endpoint(POST, '/send', WRITE, CLOUDTASKS) sendEmail(r: SendEmailRequest): Promise<void> {…} }
10
+ @PubSub() @ApiPath('/email')
11
+ abstract class EmailApi {
12
+ @Endpoint(POST, '/send', WRITE, CLOUDTASKS)
13
+ @WpAuth([oidc()])
14
+ @WpAuthorization({ authType: AuthorizationType.SERVICE_ONLY })
15
+ abstract sendEmail(r: SendEmailRequest): Promise<void>;
16
+ }
12
17
 
13
18
  // build the client once (sync); 'email-svc' is the callee's Cloud Run service name
14
19
  const emailTasks = factory.createPubSubClient(EmailApi, new TaskClientConfig('email-svc'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/cloudtasks-client",
3
- "version": "0.4.863",
3
+ "version": "0.4.865",
4
4
  "description": "Cloud Tasks enqueue client generated from a shared @PubSub API contract (twin of http-client)",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -23,9 +23,9 @@
23
23
  "access": "public"
24
24
  },
25
25
  "dependencies": {
26
- "@webpieces/core-context": "0.4.863",
27
- "@webpieces/core-util": "0.4.863",
28
- "@webpieces/gcp-identity": "0.4.863",
26
+ "@webpieces/core-context": "0.4.865",
27
+ "@webpieces/core-util": "0.4.865",
28
+ "@webpieces/gcp-identity": "0.4.865",
29
29
  "@google-cloud/tasks": "5.5.2",
30
30
  "inversify": "7.10.4",
31
31
  "reflect-metadata": "0.2.2"
@@ -1,3 +1,4 @@
1
+ import { AuthMeta, AuthMode } from '@webpieces/core-util';
1
2
  import { Secrets } from '@webpieces/core-util';
2
3
  import { TaskInvoker, TaskRequest, JobReference } from './TaskTypes';
3
4
  /**
@@ -11,6 +12,7 @@ export declare class GcpTaskInvoker extends TaskInvoker {
11
12
  private readonly secrets?;
12
13
  private readonly client;
13
14
  constructor(secrets?: Secrets | undefined);
15
+ selectAuthentication(meta: AuthMeta): AuthMode;
14
16
  enqueue(request: TaskRequest): Promise<JobReference>;
15
17
  delete(ref: JobReference): Promise<void>;
16
18
  private buildTask;
@@ -22,11 +22,18 @@ const log = core_util_1.LogManager.getLogger('GcpTaskInvoker');
22
22
  let GcpTaskInvoker = class GcpTaskInvoker extends TaskTypes_1.TaskInvoker {
23
23
  secrets;
24
24
  client = new tasks_1.CloudTasksClient();
25
- // @optional: only @WpAuthSharedSecret task endpoints need it; the client sends its bound value.
25
+ // @optional: only sharedSecret(...) task endpoints need it; the client sends its bound value.
26
26
  constructor(secrets) {
27
27
  super();
28
28
  this.secrets = secrets;
29
29
  }
30
+ selectAuthentication(meta) {
31
+ const selected = meta.methods.find((method) => method.kind === 'oidc' || method.kind === 'public' ||
32
+ (method.kind === 'shared-secret' && this.secrets?.get(method.secretKey) !== undefined));
33
+ if (!selected)
34
+ throw new Error('No declared task credential has a configured delivery mechanism.');
35
+ return selected;
36
+ }
30
37
  async enqueue(request) {
31
38
  const projectId = await (0, gcp_identity_1.getProjectId)();
32
39
  const region = await (0, gcp_identity_1.getRegion)();
@@ -1 +1 @@
1
- {"version":3,"file":"GcpTaskInvoker.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/GcpTaskInvoker.ts"],"names":[],"mappings":"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","sourcesContent":["import { inject, optional } from 'inversify';\nimport { CloudTasksClient, protos } from '@google-cloud/tasks';\nimport { provideFrameworkSingletonDefaultForApi } from '@webpieces/core-context';\nimport { getProjectId, getRegion, getRuntimeServiceAccountEmail } from '@webpieces/gcp-identity';\nimport { LogManager, Secrets, SECRETS } from '@webpieces/core-util';\nimport { TaskInvoker, TaskRequest, JobReference } from './TaskTypes';\n\nconst log = LogManager.getLogger('GcpTaskInvoker');\n\ntype ITask = protos.google.cloud.tasks.v2.ITask;\n\n/**\n * TaskInvoker that enqueues to Google Cloud Tasks (`@google-cloud/tasks`). Builds an\n * HTTP-target task delivered as POST to `targetUrl + path`, authenticated per the\n * endpoint's auth mode (OIDC token minted as this service's runtime SA, or a\n * shared-secret header). The task name is derived from the dedup name for idempotency\n * (a duplicate enqueue is rejected ALREADY_EXISTS = idempotent success upstream).\n */\n// Default (overridable) impl of the TaskInvoker contract: binds `TaskInvoker -> GcpTaskInvoker`\n// as a framework singleton. Tests/local dev override it via appOverrides:\n// `(await options.rebind(TaskInvoker)).to(InMemoryTaskInvoker)` — same idiom as AuthConfig.\n@provideFrameworkSingletonDefaultForApi(TaskInvoker)\nexport class GcpTaskInvoker extends TaskInvoker {\n private readonly client = new CloudTasksClient();\n\n // @optional: only @WpAuthSharedSecret task endpoints need it; the client sends its bound value.\n constructor(@optional() @inject(SECRETS) private readonly secrets?: Secrets) {\n super();\n }\n\n override async enqueue(request: TaskRequest): Promise<JobReference> {\n const projectId = await getProjectId();\n const region = await getRegion();\n const parent = this.client.queuePath(projectId, region, request.queueName);\n\n const task = await this.buildTask(request, parent);\n const result = await this.client.createTask({ parent: parent, task: task });\n const created = result[0];\n const name = created.name ?? request.scheduleInfo.dedupName ?? 'unknown';\n log.info(`enqueued cloud task ${name}`);\n return new JobReference(name);\n }\n\n override async delete(ref: JobReference): Promise<void> {\n await this.client.deleteTask({ name: ref.taskId });\n }\n\n private async buildTask(request: TaskRequest, parent: string): Promise<ITask> {\n const headers = new Map<string, string>(request.contextHeaders);\n headers.set('content-type', 'application/json');\n\n const httpRequest: protos.google.cloud.tasks.v2.IHttpRequest = {\n httpMethod: 'POST',\n url: request.targetUrl + request.path,\n headers: Object.fromEntries(headers),\n body: Buffer.from(JSON.stringify(request.body ?? {}), 'utf8').toString('base64'),\n };\n await this.applyAuth(request, httpRequest, headers);\n\n const task: ITask = { httpRequest: httpRequest };\n if (request.scheduleInfo.dedupName) {\n task.name = `${parent}/tasks/${request.scheduleInfo.dedupName}`;\n }\n if (request.scheduleInfo.epochMsToRunAt) {\n task.scheduleTime = { seconds: Math.floor(request.scheduleInfo.epochMsToRunAt / 1000) };\n }\n if (request.scheduleInfo.taskTimeoutSeconds) {\n task.dispatchDeadline = { seconds: request.scheduleInfo.taskTimeoutSeconds };\n }\n return task;\n }\n\n private async applyAuth(\n request: TaskRequest,\n httpRequest: protos.google.cloud.tasks.v2.IHttpRequest,\n headers: Map<string, string>,\n ): Promise<void> {\n const mode = request.authMode;\n if (mode.kind === 'oidc') {\n httpRequest.oidcToken = {\n serviceAccountEmail: await getRuntimeServiceAccountEmail(),\n audience: request.targetUrl,\n };\n return;\n }\n if (mode.kind === 'shared-secret') {\n const secret = this.secrets?.get(mode.secretKey);\n if (secret) {\n // One credential header; the 'Webpieces' scheme says a shared secret follows, not a token.\n headers.set('authorization', `Webpieces ${secret}`);\n httpRequest.headers = Object.fromEntries(headers);\n }\n }\n }\n}\n"]}
1
+ {"version":3,"file":"GcpTaskInvoker.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/GcpTaskInvoker.ts"],"names":[],"mappings":"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","sourcesContent":["import { AuthMeta, AuthMode } from '@webpieces/core-util';\nimport { inject, optional } from 'inversify';\nimport { CloudTasksClient, protos } from '@google-cloud/tasks';\nimport { provideFrameworkSingletonDefaultForApi } from '@webpieces/core-context';\nimport { getProjectId, getRegion, getRuntimeServiceAccountEmail } from '@webpieces/gcp-identity';\nimport { LogManager, Secrets, SECRETS } from '@webpieces/core-util';\nimport { TaskInvoker, TaskRequest, JobReference } from './TaskTypes';\n\nconst log = LogManager.getLogger('GcpTaskInvoker');\n\ntype ITask = protos.google.cloud.tasks.v2.ITask;\n\n/**\n * TaskInvoker that enqueues to Google Cloud Tasks (`@google-cloud/tasks`). Builds an\n * HTTP-target task delivered as POST to `targetUrl + path`, authenticated per the\n * endpoint's auth mode (OIDC token minted as this service's runtime SA, or a\n * shared-secret header). The task name is derived from the dedup name for idempotency\n * (a duplicate enqueue is rejected ALREADY_EXISTS = idempotent success upstream).\n */\n// Default (overridable) impl of the TaskInvoker contract: binds `TaskInvoker -> GcpTaskInvoker`\n// as a framework singleton. Tests/local dev override it via appOverrides:\n// `(await options.rebind(TaskInvoker)).to(InMemoryTaskInvoker)` — same idiom as AuthConfig.\n@provideFrameworkSingletonDefaultForApi(TaskInvoker)\nexport class GcpTaskInvoker extends TaskInvoker {\n private readonly client = new CloudTasksClient();\n\n // @optional: only sharedSecret(...) task endpoints need it; the client sends its bound value.\n constructor(@optional() @inject(SECRETS) private readonly secrets?: Secrets) {\n super();\n }\n\n override selectAuthentication(meta: AuthMeta): AuthMode {\n const selected = meta.methods.find((method: AuthMode) => method.kind === 'oidc' || method.kind === 'public' ||\n (method.kind === 'shared-secret' && this.secrets?.get(method.secretKey) !== undefined));\n if (!selected) throw new Error('No declared task credential has a configured delivery mechanism.');\n return selected;\n }\n\n override async enqueue(request: TaskRequest): Promise<JobReference> {\n const projectId = await getProjectId();\n const region = await getRegion();\n const parent = this.client.queuePath(projectId, region, request.queueName);\n\n const task = await this.buildTask(request, parent);\n const result = await this.client.createTask({ parent: parent, task: task });\n const created = result[0];\n const name = created.name ?? request.scheduleInfo.dedupName ?? 'unknown';\n log.info(`enqueued cloud task ${name}`);\n return new JobReference(name);\n }\n\n override async delete(ref: JobReference): Promise<void> {\n await this.client.deleteTask({ name: ref.taskId });\n }\n\n private async buildTask(request: TaskRequest, parent: string): Promise<ITask> {\n const headers = new Map<string, string>(request.contextHeaders);\n headers.set('content-type', 'application/json');\n\n const httpRequest: protos.google.cloud.tasks.v2.IHttpRequest = {\n httpMethod: 'POST',\n url: request.targetUrl + request.path,\n headers: Object.fromEntries(headers),\n body: Buffer.from(JSON.stringify(request.body ?? {}), 'utf8').toString('base64'),\n };\n await this.applyAuth(request, httpRequest, headers);\n\n const task: ITask = { httpRequest: httpRequest };\n if (request.scheduleInfo.dedupName) {\n task.name = `${parent}/tasks/${request.scheduleInfo.dedupName}`;\n }\n if (request.scheduleInfo.epochMsToRunAt) {\n task.scheduleTime = { seconds: Math.floor(request.scheduleInfo.epochMsToRunAt / 1000) };\n }\n if (request.scheduleInfo.taskTimeoutSeconds) {\n task.dispatchDeadline = { seconds: request.scheduleInfo.taskTimeoutSeconds };\n }\n return task;\n }\n\n private async applyAuth(\n request: TaskRequest,\n httpRequest: protos.google.cloud.tasks.v2.IHttpRequest,\n headers: Map<string, string>,\n ): Promise<void> {\n const mode = request.authMode;\n if (mode.kind === 'oidc') {\n httpRequest.oidcToken = {\n serviceAccountEmail: await getRuntimeServiceAccountEmail(),\n audience: request.targetUrl,\n };\n return;\n }\n if (mode.kind === 'shared-secret') {\n const secret = this.secrets?.get(mode.secretKey);\n if (secret) {\n // One credential header; the 'Webpieces' scheme says a shared secret follows, not a token.\n headers.set('authorization', `Webpieces ${secret}`);\n httpRequest.headers = Object.fromEntries(headers);\n }\n }\n }\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import { AuthMeta, AuthMode } from '@webpieces/core-util';
1
2
  import { GcpOidc } from '@webpieces/gcp-identity';
2
3
  import { Secrets } from '@webpieces/core-util';
3
4
  import { TaskInvoker, TaskRequest, JobReference } from './TaskTypes';
@@ -26,6 +27,7 @@ export declare class InMemoryTaskInvoker extends TaskInvoker {
26
27
  /** Sharpens the delivery-failure LOG: "unreachable target" vs a bare "Failed to fetch". */
27
28
  private readonly networkRejectClassifier;
28
29
  constructor(gcpOidc: GcpOidc, secrets?: Secrets | undefined);
30
+ selectAuthentication(meta: AuthMeta): AuthMode;
29
31
  enqueue(request: TaskRequest): Promise<JobReference>;
30
32
  delete(ref: JobReference): Promise<void>;
31
33
  /** ms until the scheduled run time (0 = as soon as possible / already due). */
@@ -37,6 +37,13 @@ let InMemoryTaskInvoker = class InMemoryTaskInvoker extends TaskTypes_1.TaskInvo
37
37
  this.gcpOidc = gcpOidc;
38
38
  this.secrets = secrets;
39
39
  }
40
+ selectAuthentication(meta) {
41
+ const selected = meta.methods.find((method) => method.kind === 'oidc' || method.kind === 'public' ||
42
+ (method.kind === 'shared-secret' && this.secrets?.get(method.secretKey) !== undefined));
43
+ if (!selected)
44
+ throw new Error('No declared task credential has a configured delivery mechanism.');
45
+ return selected;
46
+ }
40
47
  async enqueue(request) {
41
48
  this.counter += 1;
42
49
  const taskId = request.scheduleInfo.dedupName ?? `inmem-${request.queueName}-${this.counter}`;
@@ -1 +1 @@
1
- {"version":3,"file":"InMemoryTaskInvoker.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/InMemoryTaskInvoker.ts"],"names":[],"mappings":"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","sourcesContent":["import { inject, optional } from 'inversify';\nimport { provideFrameworkSingleton } from '@webpieces/core-context';\nimport { GcpOidc } from '@webpieces/gcp-identity';\nimport {\n LogManager,\n toError,\n NetworkRejectClassifier,\n Secrets,\n SECRETS,\n} from '@webpieces/core-util';\nimport { TaskInvoker, TaskRequest, JobReference } from './TaskTypes';\n\nconst log = LogManager.getLogger('InMemoryTaskInvoker');\n\n/**\n * TaskInvoker for tests + local dev — the in-memory-queue twin of GcpTaskInvoker\n * (a TypeScript port of webpieces-java's LocalRemoteInvoker).\n *\n * Instead of enqueuing to Google Cloud Tasks, it drops the delivery onto an in-process\n * timer queue and returns IMMEDIATELY (breaking from the caller, exactly like handing a\n * task to Cloud Tasks). The queued job then delivers the task the SAME way real Cloud\n * Tasks does: a plain HTTP POST to `targetUrl + path` with the JSON body and the\n * synthesized delivery auth (OIDC bearer / shared-secret). It does NOT invoke the server\n * in-process — the target may be a different process entirely — so the request travels\n * over real HTTP (e.g. http://localhost:{port}/queue/path) and is served by the target's\n * real routing + filter chain, giving production parity without GCP.\n *\n * Bind it (via appOverrides) in place of GcpTaskInvoker:\n * bind(TaskInvoker).to(InMemoryTaskInvoker)\n */\n@provideFrameworkSingleton()\nexport class InMemoryTaskInvoker extends TaskInvoker {\n private counter = 0;\n /** Scheduled (not-yet-delivered) jobs, so delete() can cancel them. */\n private readonly pending = new Map<string, ReturnType<typeof setTimeout>>();\n /** Sharpens the delivery-failure LOG: \"unreachable target\" vs a bare \"Failed to fetch\". */\n private readonly networkRejectClassifier = new NetworkRejectClassifier();\n\n constructor(\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @inject(GcpOidc) private readonly gcpOidc: GcpOidc,\n // @optional: only @WpAuthSharedSecret task endpoints need it; the client sends its bound value.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @optional() @inject(SECRETS) private readonly secrets?: Secrets,\n ) {\n super();\n }\n\n override async enqueue(request: TaskRequest): Promise<JobReference> {\n this.counter += 1;\n const taskId =\n request.scheduleInfo.dedupName ?? `inmem-${request.queueName}-${this.counter}`;\n const delayMs = this.computeDelayMs(request.scheduleInfo.epochMsToRunAt);\n\n // Break from the caller: queue the HTTP delivery and return the reference NOW.\n const timer = setTimeout(() => {\n this.pending.delete(taskId);\n void this.deliver(request, taskId);\n }, delayMs);\n // A queued task must not keep the process alive on its own.\n timer.unref?.();\n this.pending.set(taskId, timer);\n\n log.debug(\n `queued local task ${taskId} -> ${request.targetUrl}${request.path} (delay ${delayMs}ms)`,\n );\n return new JobReference(taskId);\n }\n\n override async delete(ref: JobReference): Promise<void> {\n const timer = this.pending.get(ref.taskId);\n if (timer) {\n clearTimeout(timer);\n this.pending.delete(ref.taskId);\n }\n }\n\n /** ms until the scheduled run time (0 = as soon as possible / already due). */\n private computeDelayMs(epochMsToRunAt?: number): number {\n if (epochMsToRunAt === undefined) {\n return 0;\n }\n const delay = epochMsToRunAt - Date.now();\n return delay > 0 ? delay : 0;\n }\n\n /** Deliver the queued task over real HTTP, mirroring Cloud Tasks' HTTP callback. */\n private async deliver(request: TaskRequest, taskId: string): Promise<void> {\n const url = `${request.targetUrl}${request.path}`;\n const headers = await this.buildHeaders(request);\n log.info(`delivering local task ${taskId}: POST ${url}`);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- detached queue job: log delivery failure, never crash the queue\n try {\n // webpieces-disable no-fetch -- in-memory queue delivers over real HTTP like Cloud Tasks (no server to invoke in-process)\n const response = await fetch(url, {\n method: 'POST',\n headers: headers,\n body: JSON.stringify(request.body ?? {}),\n });\n if (!response.ok) {\n log.error(\n `local task ${taskId} delivery to ${url} failed: HTTP ${response.status}`,\n );\n return;\n }\n log.debug(`local task ${taskId} delivered: HTTP ${response.status}`);\n } catch (err: unknown) {\n const error = toError(err);\n // Detached job — we never rethrow, so classify only to sharpen the LOG: an operator reads\n // \"the target was unreachable\" (ApiConnectionError) instead of a bare \"Failed to fetch\".\n const deliveryError = this.networkRejectClassifier.toNetworkError(error, url);\n log.error(`local task ${taskId} delivery to ${url} threw: ${deliveryError.message}`);\n }\n }\n\n /** content-type + propagated context headers + synthesized delivery credential. */\n private async buildHeaders(request: TaskRequest): Promise<Record<string, string>> {\n const headers: Record<string, string> = { 'content-type': 'application/json' };\n for (const entry of request.contextHeaders.entries()) {\n headers[entry[0]] = entry[1];\n }\n await this.attachAuth(request, headers);\n return headers;\n }\n\n /** Synthesize the delivery credential Cloud Tasks would attach, per the auth mode. */\n private async attachAuth(request: TaskRequest, headers: Record<string, string>): Promise<void> {\n const mode = request.authMode;\n if (mode.kind === 'oidc') {\n headers['authorization'] =\n `Bearer ${await this.gcpOidc.mintIdToken(request.targetUrl)}`;\n return;\n }\n if (mode.kind === 'shared-secret') {\n const secret = this.secrets?.get(mode.secretKey);\n if (secret) {\n // One credential header; the 'Webpieces' scheme says a shared secret follows, not a token.\n headers['authorization'] = `Webpieces ${secret}`;\n }\n }\n // public / jwt → no service credential is synthesized.\n }\n}\n"]}
1
+ {"version":3,"file":"InMemoryTaskInvoker.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/InMemoryTaskInvoker.ts"],"names":[],"mappings":"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","sourcesContent":["import { AuthMeta, AuthMode } from '@webpieces/core-util';\nimport { inject, optional } from 'inversify';\nimport { provideFrameworkSingleton } from '@webpieces/core-context';\nimport { GcpOidc } from '@webpieces/gcp-identity';\nimport {\n LogManager,\n toError,\n NetworkRejectClassifier,\n Secrets,\n SECRETS,\n} from '@webpieces/core-util';\nimport { TaskInvoker, TaskRequest, JobReference } from './TaskTypes';\n\nconst log = LogManager.getLogger('InMemoryTaskInvoker');\n\n/**\n * TaskInvoker for tests + local dev — the in-memory-queue twin of GcpTaskInvoker\n * (a TypeScript port of webpieces-java's LocalRemoteInvoker).\n *\n * Instead of enqueuing to Google Cloud Tasks, it drops the delivery onto an in-process\n * timer queue and returns IMMEDIATELY (breaking from the caller, exactly like handing a\n * task to Cloud Tasks). The queued job then delivers the task the SAME way real Cloud\n * Tasks does: a plain HTTP POST to `targetUrl + path` with the JSON body and the\n * synthesized delivery auth (OIDC bearer / shared-secret). It does NOT invoke the server\n * in-process — the target may be a different process entirely — so the request travels\n * over real HTTP (e.g. http://localhost:{port}/queue/path) and is served by the target's\n * real routing + filter chain, giving production parity without GCP.\n *\n * Bind it (via appOverrides) in place of GcpTaskInvoker:\n * bind(TaskInvoker).to(InMemoryTaskInvoker)\n */\n@provideFrameworkSingleton()\nexport class InMemoryTaskInvoker extends TaskInvoker {\n private counter = 0;\n /** Scheduled (not-yet-delivered) jobs, so delete() can cancel them. */\n private readonly pending = new Map<string, ReturnType<typeof setTimeout>>();\n /** Sharpens the delivery-failure LOG: \"unreachable target\" vs a bare \"Failed to fetch\". */\n private readonly networkRejectClassifier = new NetworkRejectClassifier();\n\n constructor(\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @inject(GcpOidc) private readonly gcpOidc: GcpOidc,\n // @optional: only sharedSecret(...) task endpoints need it; the client sends its bound value.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @optional() @inject(SECRETS) private readonly secrets?: Secrets,\n ) {\n super();\n }\n\n override selectAuthentication(meta: AuthMeta): AuthMode {\n const selected = meta.methods.find((method: AuthMode) => method.kind === 'oidc' || method.kind === 'public' ||\n (method.kind === 'shared-secret' && this.secrets?.get(method.secretKey) !== undefined));\n if (!selected) throw new Error('No declared task credential has a configured delivery mechanism.');\n return selected;\n }\n\n override async enqueue(request: TaskRequest): Promise<JobReference> {\n this.counter += 1;\n const taskId =\n request.scheduleInfo.dedupName ?? `inmem-${request.queueName}-${this.counter}`;\n const delayMs = this.computeDelayMs(request.scheduleInfo.epochMsToRunAt);\n\n // Break from the caller: queue the HTTP delivery and return the reference NOW.\n const timer = setTimeout(() => {\n this.pending.delete(taskId);\n void this.deliver(request, taskId);\n }, delayMs);\n // A queued task must not keep the process alive on its own.\n timer.unref?.();\n this.pending.set(taskId, timer);\n\n log.debug(\n `queued local task ${taskId} -> ${request.targetUrl}${request.path} (delay ${delayMs}ms)`,\n );\n return new JobReference(taskId);\n }\n\n override async delete(ref: JobReference): Promise<void> {\n const timer = this.pending.get(ref.taskId);\n if (timer) {\n clearTimeout(timer);\n this.pending.delete(ref.taskId);\n }\n }\n\n /** ms until the scheduled run time (0 = as soon as possible / already due). */\n private computeDelayMs(epochMsToRunAt?: number): number {\n if (epochMsToRunAt === undefined) {\n return 0;\n }\n const delay = epochMsToRunAt - Date.now();\n return delay > 0 ? delay : 0;\n }\n\n /** Deliver the queued task over real HTTP, mirroring Cloud Tasks' HTTP callback. */\n private async deliver(request: TaskRequest, taskId: string): Promise<void> {\n const url = `${request.targetUrl}${request.path}`;\n const headers = await this.buildHeaders(request);\n log.info(`delivering local task ${taskId}: POST ${url}`);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- detached queue job: log delivery failure, never crash the queue\n try {\n // webpieces-disable no-fetch -- in-memory queue delivers over real HTTP like Cloud Tasks (no server to invoke in-process)\n const response = await fetch(url, {\n method: 'POST',\n headers: headers,\n body: JSON.stringify(request.body ?? {}),\n });\n if (!response.ok) {\n log.error(\n `local task ${taskId} delivery to ${url} failed: HTTP ${response.status}`,\n );\n return;\n }\n log.debug(`local task ${taskId} delivered: HTTP ${response.status}`);\n } catch (err: unknown) {\n const error = toError(err);\n // Detached job — we never rethrow, so classify only to sharpen the LOG: an operator reads\n // \"the target was unreachable\" (ApiConnectionError) instead of a bare \"Failed to fetch\".\n const deliveryError = this.networkRejectClassifier.toNetworkError(error, url);\n log.error(`local task ${taskId} delivery to ${url} threw: ${deliveryError.message}`);\n }\n }\n\n /** content-type + propagated context headers + synthesized delivery credential. */\n private async buildHeaders(request: TaskRequest): Promise<Record<string, string>> {\n const headers: Record<string, string> = { 'content-type': 'application/json' };\n for (const entry of request.contextHeaders.entries()) {\n headers[entry[0]] = entry[1];\n }\n await this.attachAuth(request, headers);\n return headers;\n }\n\n /** Synthesize the delivery credential Cloud Tasks would attach, per the auth mode. */\n private async attachAuth(request: TaskRequest, headers: Record<string, string>): Promise<void> {\n const mode = request.authMode;\n if (mode.kind === 'oidc') {\n headers['authorization'] =\n `Bearer ${await this.gcpOidc.mintIdToken(request.targetUrl)}`;\n return;\n }\n if (mode.kind === 'shared-secret') {\n const secret = this.secrets?.get(mode.secretKey);\n if (secret) {\n // One credential header; the 'Webpieces' scheme says a shared secret follows, not a token.\n headers['authorization'] = `Webpieces ${secret}`;\n }\n }\n // public / jwt → no service credential is synthesized.\n }\n}\n"]}
@@ -55,12 +55,12 @@ export declare class TaskProxyClient {
55
55
  *
56
56
  * No credential can appear here: `authorization` is read off the inbound HttpRequest and is not
57
57
  * a ContextKey, so it never enters the RequestContext to be transferred. The invoker mints the
58
- * task's own delivery auth per the endpoint's @WpAuthOidc / @WpAuthSharedSecret mode.
58
+ * task's own delivery auth per the endpoint's oidc(...) / sharedSecret(...) mode.
59
59
  *
60
60
  * TRUSTED keys DO flow on this path, and that is the point of deriving {@link DestinationTrust}
61
61
  * from `plan.authMode` rather than hardcoding it: a task is delivered under the OIDC token the
62
62
  * invoker mints, so the delivery endpoint authenticates its caller and its AuthFilter admits the
63
- * userId/orgId we vouched for. A @PubSub endpoint declared @WpAuthPublic or @WpAuthJwt would not, and it
63
+ * userId/orgId we vouched for. A @PubSub endpoint declared @WpAuthPublic or jwt() would not, and it
64
64
  * correctly stops receiving them — the enqueue keeps working, minus context the callee would
65
65
  * have 401'd on.
66
66
  */
@@ -127,7 +127,8 @@ let TaskProxyClient = class TaskProxyClient {
127
127
  const endpoints = (0, core_util_1.getEndpoints)(apiClass) ?? {};
128
128
  const plans = new Map();
129
129
  for (const methodName of Object.keys(endpoints)) {
130
- const authMode = (0, core_util_1.getAuthMode)(apiClass, methodName);
130
+ const authMeta = (0, core_util_1.getAuthMeta)(apiClass, methodName);
131
+ const authMode = authMeta ? this.invoker.selectAuthentication(authMeta) : undefined;
131
132
  if (!authMode) {
132
133
  throw new Error(`Endpoint '${methodName}' on ${this.apiName} has no auth mode`);
133
134
  }
@@ -143,12 +144,12 @@ let TaskProxyClient = class TaskProxyClient {
143
144
  *
144
145
  * No credential can appear here: `authorization` is read off the inbound HttpRequest and is not
145
146
  * a ContextKey, so it never enters the RequestContext to be transferred. The invoker mints the
146
- * task's own delivery auth per the endpoint's @WpAuthOidc / @WpAuthSharedSecret mode.
147
+ * task's own delivery auth per the endpoint's oidc(...) / sharedSecret(...) mode.
147
148
  *
148
149
  * TRUSTED keys DO flow on this path, and that is the point of deriving {@link DestinationTrust}
149
150
  * from `plan.authMode` rather than hardcoding it: a task is delivered under the OIDC token the
150
151
  * invoker mints, so the delivery endpoint authenticates its caller and its AuthFilter admits the
151
- * userId/orgId we vouched for. A @PubSub endpoint declared @WpAuthPublic or @WpAuthJwt would not, and it
152
+ * userId/orgId we vouched for. A @PubSub endpoint declared @WpAuthPublic or jwt() would not, and it
152
153
  * correctly stops receiving them — the enqueue keeps working, minus context the callee would
153
154
  * have 401'd on.
154
155
  */
@@ -1 +1 @@
1
- {"version":3,"file":"TaskProxyClient.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/TaskProxyClient.ts"],"names":[],"mappings":";;;;AAAA,yCAAmC;AACnC,oDAmB8B;AAC9B,0DAIiC;AACjC,2CAAqE;AACrE,uDAAyD;AAGzD,MAAM,GAAG,GAAG,sBAAU,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAC;AAEpD,8EAA8E;AAC9E,MAAM,YAAY;IACd,IAAI,CAAS;IACb,SAAS,CAAS;IAClB,QAAQ,CAAW;IACnB;uGACmG;IACnG,IAAI,CAAY;IAEhB,YAAY,IAAY,EAAE,SAAiB,EAAE,QAAkB,EAAE,IAAe;QAC5E,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACrB,CAAC;CACJ;AAED;;;;;;;;;;;;;;;;;;GAkBG;AAEI,IAAM,eAAe,GAArB,MAAM,eAAe;IAiBkB;IACU;IAjBpD,8EAA8E;IACtE,KAAK,CAA6B;IAClC,OAAO,CAAU;IACjB,QAAQ,CAAwB;IAChC,MAAM,CAAoB;IAElC;;;;;;OAMG;IACc,UAAU,GAAG,IAAI,0BAAc,CAAC,IAAI,2CAA4B,EAAE,CAAC,CAAC;IAErF,YAC0C,OAAoB,EACV,OAA8B;QADxC,YAAO,GAAP,OAAO,CAAa;QACV,YAAO,GAAP,OAAO,CAAuB;IAC/E,CAAC;IAEJ,yDAAyD;IACzD,IAAI,CAAC,QAA8B,EAAE,MAAwB;QACzD,IAAA,gCAAoB,EAAC,QAAQ,EAAE,oBAAoB,CAAC,CAAC;QACrD,IAAI,CAAC,IAAA,qBAAS,EAAC,QAAQ,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CACX,SAAS,QAAQ,CAAC,IAAI,IAAI,SAAS,oCAAoC,CAC1E,CAAC;QACN,CAAC;QACD,IAAA,mCAAuB,EAAC,QAAQ,CAAC,CAAC;QAClC,IAAA,0CAA8B,EAAC,QAAQ,CAAC,CAAC;QAEzC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,OAAO,GAAG,QAAQ,CAAC,IAAI,IAAI,YAAY,CAAC;QAC7C,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IAC3C,CAAC;IAED,oFAAoF;IACpF,WAAW,CAAC,UAAkB;QAC1B,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IACtC,CAAC;IAED;;;;OAIG;IACH,6FAA6F;IAC7F,KAAK,CAAC,OAAO,CAAC,UAAkB,EAAE,UAAmB;QACjD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACxC,IAAI,CAAC,IAAI,EAAE,CAAC;YACR,MAAM,IAAI,KAAK,CAAC,wBAAwB,UAAU,QAAQ,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QAC9E,CAAC;QAED,MAAM,KAAK,GAAG,IAAA,sCAAoB,GAAE,CAAC;QACrC,IAAI,CAAC,KAAK,EAAE,CAAC;YACT,MAAM,IAAI,KAAK,CACX,uEAAuE;gBACnE,oEAAoE,CAC3E,CAAC;QACN,CAAC;QAED,8FAA8F;QAC9F,gGAAgG;QAChG,wFAAwF;QACxF,0FAA0F;QAC1F,mDAAmD;QACnD,MAAM,IAAI,GAAG,IAAI,yBAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QACzF,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,IAAI,EAAE;YACvD,MAAM,MAAM,GAAG,MAAM,wBAAY,CAAC,OAAO,CACrC,IAAI,CAAC,QAAQ,EACb,UAAU,EACV,CAAC,SAAiB,EAAE,EAAE,CAClB,wBAAY,CAAC,GAAG,CACZ,SAAS,EACT,IAAI,uBAAW,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,EACzC,KAAK,EAAE,MAAmB,EAAE,EAAE;gBAC1B,uFAAuF;gBACvF,qFAAqF;gBACrF,wCAAwC;gBACxC,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;gBACjD,wBAAY,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;gBAEpC,MAAM,OAAO,GAAG,IAAI,uBAAW,CAC3B,SAAS,EACT,IAAI,CAAC,IAAI,EACT,IAAI,CAAC,SAAS,EACd,UAAU,EACV,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,QAAQ,CAAC,EACvC,IAAI,CAAC,QAAQ,EACb,KAAK,CAAC,IAAI,IAAI,IAAI,wBAAY,EAAE,CACnC,CAAC;gBAEF,gEAAgE;gBAChE,GAAG,CAAC,KAAK,CACL,gBAAgB,IAAI,CAAC,SAAS,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CACzE,CAAC;gBACF,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YACzC,CAAC,CACJ,EACL,MAAM,CACT,CAAC;YACF,0FAA0F;YAC1F,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC;YAEtB,4FAA4F;YAC5F,wFAAwF;YACxF,6BAA6B;YAC7B,OAAO,KAAK,CAAC,MAAM,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QAC9C,CAAC,CAAC,CAAC;IACP,CAAC;IAED,6FAA6F;IACrF,UAAU,CAAC,QAA8B;QAC7C,MAAM,QAAQ,GAAG,IAAA,sBAAU,EAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QAC5C,MAAM,SAAS,GAAG,IAAA,wBAAY,EAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QAC/C,MAAM,KAAK,GAAG,IAAI,GAAG,EAAwB,CAAC;QAE9C,KAAK,MAAM,UAAU,IAAI,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;YAC9C,MAAM,QAAQ,GAAG,IAAA,uBAAW,EAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;YACnD,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CAAC,aAAa,UAAU,QAAQ,IAAI,CAAC,OAAO,mBAAmB,CAAC,CAAC;YACpF,CAAC;YACD,MAAM,IAAI,GAAG,IAAI,YAAY,CACzB,QAAQ,GAAG,SAAS,CAAC,UAAU,CAAC,EAChC,IAAA,wBAAY,EAAC,QAAQ,EAAE,UAAU,CAAC,EAClC,QAAQ,EACR,IAAA,uBAAW,EAAC,QAAQ,EAAE,UAAU,CAAC,CACpC,CAAC;YACF,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;QAChC,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACK,mBAAmB,CAAC,QAAkB;QAC1C,OAAO,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,4BAAgB,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC;IACrF,CAAC;CACJ,CAAA;AA1JY,0CAAe;0BAAf,eAAe;IAD3B,IAAA,wCAAyB,GAAE;IAkBnB,mBAAA,IAAA,kBAAM,EAAC,uBAAW,CAAC,CAAA;IACnB,mBAAA,IAAA,kBAAM,EAAC,oCAAqB,CAAC,CAAA;6CADiB,uBAAW;QACD,oCAAqB;GAlBzE,eAAe,CA0J3B;AAED;;;;;;;GAOG;AACH,gGAAgG;AACnF,QAAA,0BAA0B,GAAG,MAAM,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC","sourcesContent":["import { inject } from 'inversify';\nimport {\n isApiPath,\n getApiPath,\n getEndpoints,\n getAuthMode,\n getQueueName,\n getMaskSpec,\n assertPubSubConventions,\n assertEveryEndpointHasAuthMode,\n assertNotInternalApi,\n AuthMode,\n DestinationTrust,\n MaskSpec,\n LogManager,\n LogApiCallImpl,\n ApiMethodInfo,\n CallRegistry,\n CallDeadline,\n CallContext,\n} from '@webpieces/core-util';\nimport {\n RequestContextApiCallContext,\n RequestContextHeaders,\n provideFrameworkTransient,\n} from '@webpieces/core-context';\nimport { TaskInvoker, TaskRequest, ScheduleInfo } from './TaskTypes';\nimport { currentScheduleFrame } from './ScheduleContext';\nimport { ApiPrototype, TaskClientConfig } from './TaskClientConfig';\n\nconst log = LogManager.getLogger('TaskProxyClient');\n\n/** Per-endpoint routing plan resolved once from the contract's decorators. */\nclass EndpointPlan {\n path: string;\n queueName: string;\n authMode: AuthMode;\n /** @MaskLog spec for this endpoint's request DTO, read once here so the enqueue log path masks\n * a secret on the task payload without a per-enqueue reflection hit. Undefined = log verbatim. */\n mask?: MaskSpec;\n\n constructor(path: string, queueName: string, authMode: AuthMode, mask?: MaskSpec) {\n this.path = path;\n this.queueName = queueName;\n this.authMode = authMode;\n this.mask = mask;\n }\n}\n\n/**\n * TaskProxyClient - the enqueue engine behind one @PubSub API contract's client proxy.\n *\n * The fire-and-forget twin of http-client-core's ProxyClient, and TWO-PHASE for the same reason:\n * its COLLABORATORS (invoker, headers) come from the container, while the PER-CLIENT state (which\n * contract, which target) arrives on {@link init}. That is what lets {@link ClientCloudTasksFactory}\n * hold a `Provider<TaskProxyClient>` and hand out a fresh, independently-configured client per\n * contract.\n *\n * Calling an endpoint ENQUEUES a task (it does not call remotely); the task is later delivered to\n * the same endpoint's controller through the full server filter chain.\n *\n * It owns:\n * - @ApiPath / @PubSub convention validation + the endpoint plans from the contract's decorators\n * - Resolving the callee's base URL from svcName through `ClientRegistry.resolve` — the SAME chain the\n * RPC client runs: a registered mapping, else the installed deriver, else throw\n * - Context propagation onto the task headers (a credential is never a context key, so none can ride along)\n * - Handing a fully-built TaskRequest to the bound {@link TaskInvoker}\n */\n@provideFrameworkTransient()\nexport class TaskProxyClient {\n // Assigned by init(), which the factory calls immediately after construction.\n private plans!: Map<string, EndpointPlan>;\n private apiName!: string;\n private apiClass!: ApiPrototype<object>;\n private config!: TaskClientConfig;\n\n /**\n * The api-call log seam, built around this node-only package's OWN RequestContext-backed\n * ApiCallContext. Constructed here rather than installed at startup, so a plain NestJS/Express\n * host needs no webpieces STARTUP INSTALL to enqueue — it only has to run the call inside a\n * `RequestContext.run(...)` scope, which such a host already opens per request. That is the defect\n * that made every enqueue throw `ApiCallContext is not installed`.\n */\n private readonly logApiCall = new LogApiCallImpl(new RequestContextApiCallContext());\n\n constructor(\n @inject(TaskInvoker) private readonly invoker: TaskInvoker,\n @inject(RequestContextHeaders) private readonly headers: RequestContextHeaders,\n ) {}\n\n /** Bind this client to one @PubSub contract + target. */\n init(apiClass: ApiPrototype<object>, config: TaskClientConfig): void {\n assertNotInternalApi(apiClass, 'Cloud Tasks client');\n if (!isApiPath(apiClass)) {\n throw new Error(\n `Class ${apiClass.name || 'Unknown'} must be decorated with @ApiPath()`,\n );\n }\n assertPubSubConventions(apiClass);\n assertEveryEndpointHasAuthMode(apiClass);\n\n this.config = config;\n this.apiClass = apiClass;\n this.apiName = apiClass.name || 'UnknownApi';\n this.plans = this.buildPlans(apiClass);\n }\n\n /** Check whether the contract declares a @PubSub endpoint with this method name. */\n hasEndpoint(methodName: string): boolean {\n return this.plans.has(methodName);\n }\n\n /**\n * Enqueue one task for the named endpoint. Must run inside a CloudTaskScheduler lambda\n * (which supplies the ScheduleInfo) within an active RequestContext, e.g.:\n * scheduler.addToQueue(() => taskClient.foo(req), { dedupName });\n */\n // webpieces-disable no-any-unknown -- the request DTO's type is erased at the proxy boundary\n async enqueue(methodName: string, requestDto: unknown): Promise<void> {\n const plan = this.plans.get(methodName);\n if (!plan) {\n throw new Error(`No @PubSub endpoint '${methodName}' on ${this.apiName}`);\n }\n\n const frame = currentScheduleFrame();\n if (!frame) {\n throw new Error(\n 'Cloud task enqueue must run inside a CloudTaskScheduler lambda, e.g. ' +\n 'scheduler.addToQueue(() => taskClient.method(req), { dedupName }).',\n );\n }\n\n // Route through LogApiCall exactly like ProxyClient does for http. TaskProxyClient.enqueue is\n // the ONE seam above BOTH GcpTaskInvoker (prod) and InMemoryTaskInvoker (localhost) — whichever\n // TaskInvoker is bound, this method sits above it — so the enqueue edge gets structured\n // `jsonPayload.api.*` logging (and, once recording is unified at LogApiCall, is recorded)\n // uniformly, without touching either invoker impl.\n const info = new ApiMethodInfo('client', this.apiName, methodName, undefined, plan.mask);\n await this.logApiCall.execute(info, requestDto, async () => {\n const jobRef = await CallRegistry.execute(\n this.apiClass,\n methodName,\n (timeoutMs: number) =>\n CallDeadline.run(\n timeoutMs,\n new CallContext(this.apiName, methodName),\n async (signal: AbortSignal) => {\n // Resolved lazily (not at client construction) so building a client stays synchronous.\n // Every metadata read beneath resolveUrl is memoized process-wide, so only the first\n // enqueue in the process pays a lookup.\n const targetUrl = await this.config.resolveUrl();\n CallDeadline.throwIfAborted(signal);\n\n const request = new TaskRequest(\n targetUrl,\n plan.path,\n plan.queueName,\n requestDto,\n this.buildContextHeaders(plan.authMode),\n plan.authMode,\n frame.info ?? new ScheduleInfo(),\n );\n\n // svcName, not the URL, is the stable name across demo/qa/prod.\n log.debug(\n `enqueue task ${plan.queueName} -> ${this.config.svcName}${plan.path}`,\n );\n return this.invoker.enqueue(request);\n },\n ),\n 30_000,\n );\n // Only the winning attempt may publish the reference. A late enqueue cannot overwrite it.\n frame.jobRef = jobRef;\n\n // Fire-and-forget: the enqueue ack is not a meaningful response. Return a non-null value so\n // LogApiCall's null-response guard passes; a recorded test asserts the enqueued REQUEST\n // (payload), never this ack.\n return frame.jobRef ?? { enqueued: true };\n });\n }\n\n /** Endpoint name -> its resolved path / queue / auth mode, read once from the decorators. */\n private buildPlans(apiClass: ApiPrototype<object>): Map<string, EndpointPlan> {\n const basePath = getApiPath(apiClass) ?? '';\n const endpoints = getEndpoints(apiClass) ?? {};\n const plans = new Map<string, EndpointPlan>();\n\n for (const methodName of Object.keys(endpoints)) {\n const authMode = getAuthMode(apiClass, methodName);\n if (!authMode) {\n throw new Error(`Endpoint '${methodName}' on ${this.apiName} has no auth mode`);\n }\n const plan = new EndpointPlan(\n basePath + endpoints[methodName],\n getQueueName(apiClass, methodName),\n authMode,\n getMaskSpec(apiClass, methodName),\n );\n plans.set(methodName, plan);\n }\n return plans;\n }\n\n /**\n * Every transferred context key (txId/requestId/tenant…) the DELIVERY endpoint may receive,\n * request-id chained. Throws if there is no active RequestContext — an enqueue with no trace is\n * a bug.\n *\n * No credential can appear here: `authorization` is read off the inbound HttpRequest and is not\n * a ContextKey, so it never enters the RequestContext to be transferred. The invoker mints the\n * task's own delivery auth per the endpoint's @WpAuthOidc / @WpAuthSharedSecret mode.\n *\n * TRUSTED keys DO flow on this path, and that is the point of deriving {@link DestinationTrust}\n * from `plan.authMode` rather than hardcoding it: a task is delivered under the OIDC token the\n * invoker mints, so the delivery endpoint authenticates its caller and its AuthFilter admits the\n * userId/orgId we vouched for. A @PubSub endpoint declared @WpAuthPublic or @WpAuthJwt would not, and it\n * correctly stops receiving them — the enqueue keeps working, minus context the callee would\n * have 401'd on.\n */\n private buildContextHeaders(authMode: AuthMode): Map<string, string> {\n return this.headers.buildOutboundHeaders(DestinationTrust.forAuthMode(authMode));\n }\n}\n\n/**\n * DI token for the `Provider<TaskProxyClient>` that hands out enqueue clients — one per @PubSub\n * contract. `Provider<T>` is erased at runtime, so it cannot be its own token; this Symbol names T.\n *\n * Because TaskProxyClient is bound TRANSIENT, every `get()` constructs a new one. (Were it bound\n * `@provideFrameworkSingleton`, the very same Provider would hand back one lazily-created instance\n * instead — the provider caches nothing, so the target's scope decides.)\n */\n// webpieces-disable no-symbol-di-tokens -- Provider<T> is erased at runtime; the Symbol names T\nexport const TASK_PROXY_CLIENT_PROVIDER = Symbol.for('Provider<TaskProxyClient>');\n"]}
1
+ {"version":3,"file":"TaskProxyClient.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/TaskProxyClient.ts"],"names":[],"mappings":";;;;AAAA,yCAAmC;AACnC,oDAmB8B;AAC9B,0DAIiC;AACjC,2CAAqE;AACrE,uDAAyD;AAGzD,MAAM,GAAG,GAAG,sBAAU,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAC;AAEpD,8EAA8E;AAC9E,MAAM,YAAY;IACd,IAAI,CAAS;IACb,SAAS,CAAS;IAClB,QAAQ,CAAW;IACnB;uGACmG;IACnG,IAAI,CAAY;IAEhB,YAAY,IAAY,EAAE,SAAiB,EAAE,QAAkB,EAAE,IAAe;QAC5E,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACrB,CAAC;CACJ;AAED;;;;;;;;;;;;;;;;;;GAkBG;AAEI,IAAM,eAAe,GAArB,MAAM,eAAe;IAiBkB;IACU;IAjBpD,8EAA8E;IACtE,KAAK,CAA6B;IAClC,OAAO,CAAU;IACjB,QAAQ,CAAwB;IAChC,MAAM,CAAoB;IAElC;;;;;;OAMG;IACc,UAAU,GAAG,IAAI,0BAAc,CAAC,IAAI,2CAA4B,EAAE,CAAC,CAAC;IAErF,YAC0C,OAAoB,EACV,OAA8B;QADxC,YAAO,GAAP,OAAO,CAAa;QACV,YAAO,GAAP,OAAO,CAAuB;IAC/E,CAAC;IAEJ,yDAAyD;IACzD,IAAI,CAAC,QAA8B,EAAE,MAAwB;QACzD,IAAA,gCAAoB,EAAC,QAAQ,EAAE,oBAAoB,CAAC,CAAC;QACrD,IAAI,CAAC,IAAA,qBAAS,EAAC,QAAQ,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CACX,SAAS,QAAQ,CAAC,IAAI,IAAI,SAAS,oCAAoC,CAC1E,CAAC;QACN,CAAC;QACD,IAAA,mCAAuB,EAAC,QAAQ,CAAC,CAAC;QAClC,IAAA,0CAA8B,EAAC,QAAQ,CAAC,CAAC;QAEzC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,OAAO,GAAG,QAAQ,CAAC,IAAI,IAAI,YAAY,CAAC;QAC7C,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IAC3C,CAAC;IAED,oFAAoF;IACpF,WAAW,CAAC,UAAkB;QAC1B,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IACtC,CAAC;IAED;;;;OAIG;IACH,6FAA6F;IAC7F,KAAK,CAAC,OAAO,CAAC,UAAkB,EAAE,UAAmB;QACjD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACxC,IAAI,CAAC,IAAI,EAAE,CAAC;YACR,MAAM,IAAI,KAAK,CAAC,wBAAwB,UAAU,QAAQ,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QAC9E,CAAC;QAED,MAAM,KAAK,GAAG,IAAA,sCAAoB,GAAE,CAAC;QACrC,IAAI,CAAC,KAAK,EAAE,CAAC;YACT,MAAM,IAAI,KAAK,CACX,uEAAuE;gBACnE,oEAAoE,CAC3E,CAAC;QACN,CAAC;QAED,8FAA8F;QAC9F,gGAAgG;QAChG,wFAAwF;QACxF,0FAA0F;QAC1F,mDAAmD;QACnD,MAAM,IAAI,GAAG,IAAI,yBAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QACzF,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,IAAI,EAAE;YACvD,MAAM,MAAM,GAAG,MAAM,wBAAY,CAAC,OAAO,CACrC,IAAI,CAAC,QAAQ,EACb,UAAU,EACV,CAAC,SAAiB,EAAE,EAAE,CAClB,wBAAY,CAAC,GAAG,CACZ,SAAS,EACT,IAAI,uBAAW,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,EACzC,KAAK,EAAE,MAAmB,EAAE,EAAE;gBAC1B,uFAAuF;gBACvF,qFAAqF;gBACrF,wCAAwC;gBACxC,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;gBACjD,wBAAY,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;gBAEpC,MAAM,OAAO,GAAG,IAAI,uBAAW,CAC3B,SAAS,EACT,IAAI,CAAC,IAAI,EACT,IAAI,CAAC,SAAS,EACd,UAAU,EACV,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,QAAQ,CAAC,EACvC,IAAI,CAAC,QAAQ,EACb,KAAK,CAAC,IAAI,IAAI,IAAI,wBAAY,EAAE,CACnC,CAAC;gBAEF,gEAAgE;gBAChE,GAAG,CAAC,KAAK,CACL,gBAAgB,IAAI,CAAC,SAAS,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CACzE,CAAC;gBACF,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YACzC,CAAC,CACJ,EACL,MAAM,CACT,CAAC;YACF,0FAA0F;YAC1F,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC;YAEtB,4FAA4F;YAC5F,wFAAwF;YACxF,6BAA6B;YAC7B,OAAO,KAAK,CAAC,MAAM,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QAC9C,CAAC,CAAC,CAAC;IACP,CAAC;IAED,6FAA6F;IACrF,UAAU,CAAC,QAA8B;QAC7C,MAAM,QAAQ,GAAG,IAAA,sBAAU,EAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QAC5C,MAAM,SAAS,GAAG,IAAA,wBAAY,EAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QAC/C,MAAM,KAAK,GAAG,IAAI,GAAG,EAAwB,CAAC;QAE9C,KAAK,MAAM,UAAU,IAAI,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;YAC9C,MAAM,QAAQ,GAAG,IAAA,uBAAW,EAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;YACnD,MAAM,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YACpF,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CAAC,aAAa,UAAU,QAAQ,IAAI,CAAC,OAAO,mBAAmB,CAAC,CAAC;YACpF,CAAC;YACD,MAAM,IAAI,GAAG,IAAI,YAAY,CACzB,QAAQ,GAAG,SAAS,CAAC,UAAU,CAAC,EAChC,IAAA,wBAAY,EAAC,QAAQ,EAAE,UAAU,CAAC,EAClC,QAAQ,EACR,IAAA,uBAAW,EAAC,QAAQ,EAAE,UAAU,CAAC,CACpC,CAAC;YACF,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;QAChC,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACK,mBAAmB,CAAC,QAAkB;QAC1C,OAAO,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,4BAAgB,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC;IACrF,CAAC;CACJ,CAAA;AA3JY,0CAAe;0BAAf,eAAe;IAD3B,IAAA,wCAAyB,GAAE;IAkBnB,mBAAA,IAAA,kBAAM,EAAC,uBAAW,CAAC,CAAA;IACnB,mBAAA,IAAA,kBAAM,EAAC,oCAAqB,CAAC,CAAA;6CADiB,uBAAW;QACD,oCAAqB;GAlBzE,eAAe,CA2J3B;AAED;;;;;;;GAOG;AACH,gGAAgG;AACnF,QAAA,0BAA0B,GAAG,MAAM,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC","sourcesContent":["import { inject } from 'inversify';\nimport {\n isApiPath,\n getApiPath,\n getEndpoints,\n getAuthMeta,\n getQueueName,\n getMaskSpec,\n assertPubSubConventions,\n assertEveryEndpointHasAuthMode,\n assertNotInternalApi,\n AuthMode,\n DestinationTrust,\n MaskSpec,\n LogManager,\n LogApiCallImpl,\n ApiMethodInfo,\n CallRegistry,\n CallDeadline,\n CallContext,\n} from '@webpieces/core-util';\nimport {\n RequestContextApiCallContext,\n RequestContextHeaders,\n provideFrameworkTransient,\n} from '@webpieces/core-context';\nimport { TaskInvoker, TaskRequest, ScheduleInfo } from './TaskTypes';\nimport { currentScheduleFrame } from './ScheduleContext';\nimport { ApiPrototype, TaskClientConfig } from './TaskClientConfig';\n\nconst log = LogManager.getLogger('TaskProxyClient');\n\n/** Per-endpoint routing plan resolved once from the contract's decorators. */\nclass EndpointPlan {\n path: string;\n queueName: string;\n authMode: AuthMode;\n /** @MaskLog spec for this endpoint's request DTO, read once here so the enqueue log path masks\n * a secret on the task payload without a per-enqueue reflection hit. Undefined = log verbatim. */\n mask?: MaskSpec;\n\n constructor(path: string, queueName: string, authMode: AuthMode, mask?: MaskSpec) {\n this.path = path;\n this.queueName = queueName;\n this.authMode = authMode;\n this.mask = mask;\n }\n}\n\n/**\n * TaskProxyClient - the enqueue engine behind one @PubSub API contract's client proxy.\n *\n * The fire-and-forget twin of http-client-core's ProxyClient, and TWO-PHASE for the same reason:\n * its COLLABORATORS (invoker, headers) come from the container, while the PER-CLIENT state (which\n * contract, which target) arrives on {@link init}. That is what lets {@link ClientCloudTasksFactory}\n * hold a `Provider<TaskProxyClient>` and hand out a fresh, independently-configured client per\n * contract.\n *\n * Calling an endpoint ENQUEUES a task (it does not call remotely); the task is later delivered to\n * the same endpoint's controller through the full server filter chain.\n *\n * It owns:\n * - @ApiPath / @PubSub convention validation + the endpoint plans from the contract's decorators\n * - Resolving the callee's base URL from svcName through `ClientRegistry.resolve` — the SAME chain the\n * RPC client runs: a registered mapping, else the installed deriver, else throw\n * - Context propagation onto the task headers (a credential is never a context key, so none can ride along)\n * - Handing a fully-built TaskRequest to the bound {@link TaskInvoker}\n */\n@provideFrameworkTransient()\nexport class TaskProxyClient {\n // Assigned by init(), which the factory calls immediately after construction.\n private plans!: Map<string, EndpointPlan>;\n private apiName!: string;\n private apiClass!: ApiPrototype<object>;\n private config!: TaskClientConfig;\n\n /**\n * The api-call log seam, built around this node-only package's OWN RequestContext-backed\n * ApiCallContext. Constructed here rather than installed at startup, so a plain NestJS/Express\n * host needs no webpieces STARTUP INSTALL to enqueue — it only has to run the call inside a\n * `RequestContext.run(...)` scope, which such a host already opens per request. That is the defect\n * that made every enqueue throw `ApiCallContext is not installed`.\n */\n private readonly logApiCall = new LogApiCallImpl(new RequestContextApiCallContext());\n\n constructor(\n @inject(TaskInvoker) private readonly invoker: TaskInvoker,\n @inject(RequestContextHeaders) private readonly headers: RequestContextHeaders,\n ) {}\n\n /** Bind this client to one @PubSub contract + target. */\n init(apiClass: ApiPrototype<object>, config: TaskClientConfig): void {\n assertNotInternalApi(apiClass, 'Cloud Tasks client');\n if (!isApiPath(apiClass)) {\n throw new Error(\n `Class ${apiClass.name || 'Unknown'} must be decorated with @ApiPath()`,\n );\n }\n assertPubSubConventions(apiClass);\n assertEveryEndpointHasAuthMode(apiClass);\n\n this.config = config;\n this.apiClass = apiClass;\n this.apiName = apiClass.name || 'UnknownApi';\n this.plans = this.buildPlans(apiClass);\n }\n\n /** Check whether the contract declares a @PubSub endpoint with this method name. */\n hasEndpoint(methodName: string): boolean {\n return this.plans.has(methodName);\n }\n\n /**\n * Enqueue one task for the named endpoint. Must run inside a CloudTaskScheduler lambda\n * (which supplies the ScheduleInfo) within an active RequestContext, e.g.:\n * scheduler.addToQueue(() => taskClient.foo(req), { dedupName });\n */\n // webpieces-disable no-any-unknown -- the request DTO's type is erased at the proxy boundary\n async enqueue(methodName: string, requestDto: unknown): Promise<void> {\n const plan = this.plans.get(methodName);\n if (!plan) {\n throw new Error(`No @PubSub endpoint '${methodName}' on ${this.apiName}`);\n }\n\n const frame = currentScheduleFrame();\n if (!frame) {\n throw new Error(\n 'Cloud task enqueue must run inside a CloudTaskScheduler lambda, e.g. ' +\n 'scheduler.addToQueue(() => taskClient.method(req), { dedupName }).',\n );\n }\n\n // Route through LogApiCall exactly like ProxyClient does for http. TaskProxyClient.enqueue is\n // the ONE seam above BOTH GcpTaskInvoker (prod) and InMemoryTaskInvoker (localhost) — whichever\n // TaskInvoker is bound, this method sits above it — so the enqueue edge gets structured\n // `jsonPayload.api.*` logging (and, once recording is unified at LogApiCall, is recorded)\n // uniformly, without touching either invoker impl.\n const info = new ApiMethodInfo('client', this.apiName, methodName, undefined, plan.mask);\n await this.logApiCall.execute(info, requestDto, async () => {\n const jobRef = await CallRegistry.execute(\n this.apiClass,\n methodName,\n (timeoutMs: number) =>\n CallDeadline.run(\n timeoutMs,\n new CallContext(this.apiName, methodName),\n async (signal: AbortSignal) => {\n // Resolved lazily (not at client construction) so building a client stays synchronous.\n // Every metadata read beneath resolveUrl is memoized process-wide, so only the first\n // enqueue in the process pays a lookup.\n const targetUrl = await this.config.resolveUrl();\n CallDeadline.throwIfAborted(signal);\n\n const request = new TaskRequest(\n targetUrl,\n plan.path,\n plan.queueName,\n requestDto,\n this.buildContextHeaders(plan.authMode),\n plan.authMode,\n frame.info ?? new ScheduleInfo(),\n );\n\n // svcName, not the URL, is the stable name across demo/qa/prod.\n log.debug(\n `enqueue task ${plan.queueName} -> ${this.config.svcName}${plan.path}`,\n );\n return this.invoker.enqueue(request);\n },\n ),\n 30_000,\n );\n // Only the winning attempt may publish the reference. A late enqueue cannot overwrite it.\n frame.jobRef = jobRef;\n\n // Fire-and-forget: the enqueue ack is not a meaningful response. Return a non-null value so\n // LogApiCall's null-response guard passes; a recorded test asserts the enqueued REQUEST\n // (payload), never this ack.\n return frame.jobRef ?? { enqueued: true };\n });\n }\n\n /** Endpoint name -> its resolved path / queue / auth mode, read once from the decorators. */\n private buildPlans(apiClass: ApiPrototype<object>): Map<string, EndpointPlan> {\n const basePath = getApiPath(apiClass) ?? '';\n const endpoints = getEndpoints(apiClass) ?? {};\n const plans = new Map<string, EndpointPlan>();\n\n for (const methodName of Object.keys(endpoints)) {\n const authMeta = getAuthMeta(apiClass, methodName);\n const authMode = authMeta ? this.invoker.selectAuthentication(authMeta) : undefined;\n if (!authMode) {\n throw new Error(`Endpoint '${methodName}' on ${this.apiName} has no auth mode`);\n }\n const plan = new EndpointPlan(\n basePath + endpoints[methodName],\n getQueueName(apiClass, methodName),\n authMode,\n getMaskSpec(apiClass, methodName),\n );\n plans.set(methodName, plan);\n }\n return plans;\n }\n\n /**\n * Every transferred context key (txId/requestId/tenant…) the DELIVERY endpoint may receive,\n * request-id chained. Throws if there is no active RequestContext — an enqueue with no trace is\n * a bug.\n *\n * No credential can appear here: `authorization` is read off the inbound HttpRequest and is not\n * a ContextKey, so it never enters the RequestContext to be transferred. The invoker mints the\n * task's own delivery auth per the endpoint's oidc(...) / sharedSecret(...) mode.\n *\n * TRUSTED keys DO flow on this path, and that is the point of deriving {@link DestinationTrust}\n * from `plan.authMode` rather than hardcoding it: a task is delivered under the OIDC token the\n * invoker mints, so the delivery endpoint authenticates its caller and its AuthFilter admits the\n * userId/orgId we vouched for. A @PubSub endpoint declared @WpAuthPublic or jwt() would not, and it\n * correctly stops receiving them — the enqueue keeps working, minus context the callee would\n * have 401'd on.\n */\n private buildContextHeaders(authMode: AuthMode): Map<string, string> {\n return this.headers.buildOutboundHeaders(DestinationTrust.forAuthMode(authMode));\n }\n}\n\n/**\n * DI token for the `Provider<TaskProxyClient>` that hands out enqueue clients — one per @PubSub\n * contract. `Provider<T>` is erased at runtime, so it cannot be its own token; this Symbol names T.\n *\n * Because TaskProxyClient is bound TRANSIENT, every `get()` constructs a new one. (Were it bound\n * `@provideFrameworkSingleton`, the very same Provider would hand back one lazily-created instance\n * instead — the provider caches nothing, so the target's scope decides.)\n */\n// webpieces-disable no-symbol-di-tokens -- Provider<T> is erased at runtime; the Symbol names T\nexport const TASK_PROXY_CLIENT_PROVIDER = Symbol.for('Provider<TaskProxyClient>');\n"]}
@@ -50,6 +50,7 @@ export declare class TaskRequest {
50
50
  * InMemoryTaskInvoker via an in-process queue that fetches the target directly.
51
51
  */
52
52
  export declare abstract class TaskInvoker {
53
+ selectAuthentication(meta: import('@webpieces/core-util').AuthMeta): AuthMode;
53
54
  abstract enqueue(request: TaskRequest): Promise<JobReference>;
54
55
  abstract delete(ref: JobReference): Promise<void>;
55
56
  }
package/src/TaskTypes.js CHANGED
@@ -72,6 +72,14 @@ exports.TaskRequest = TaskRequest;
72
72
  * InMemoryTaskInvoker via an in-process queue that fetches the target directly.
73
73
  */
74
74
  class TaskInvoker {
75
+ selectAuthentication(meta) {
76
+ const selected = meta.methods.find((method) => method.kind === 'oidc' || method.kind === 'public');
77
+ if (selected)
78
+ return selected;
79
+ if (meta.methods.length === 1)
80
+ return meta.methods[0];
81
+ throw new Error('No configured Cloud Tasks credential satisfies the declared alternatives.');
82
+ }
75
83
  }
76
84
  exports.TaskInvoker = TaskInvoker;
77
85
  //# sourceMappingURL=TaskTypes.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"TaskTypes.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/TaskTypes.ts"],"names":[],"mappings":";;;AAEA;;;;GAIG;AACH,MAAa,YAAY;IACrB,0EAA0E;IAC1E,cAAc,CAAU;IACxB,6CAA6C;IAC7C,kBAAkB,CAAU;IAC5B;;;OAGG;IACH,SAAS,CAAU;IAEnB,YAAY,cAAuB,EAAE,kBAA2B,EAAE,SAAkB;QAChF,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;QACrC,IAAI,CAAC,kBAAkB,GAAG,kBAAkB,CAAC;QAC7C,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,CAAC;CACJ;AAhBD,oCAgBC;AAED,kFAAkF;AAClF,MAAa,YAAY;IACrB,MAAM,CAAS;IAEf,YAAY,MAAc;QACtB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACzB,CAAC;CACJ;AAND,oCAMC;AAED;;;GAGG;AACH,MAAa,WAAW;IACpB,wEAAwE;IACxE,SAAS,CAAS;IAClB,sFAAsF;IACtF,IAAI,CAAS;IACb,oFAAoF;IACpF,SAAS,CAAS;IAClB,iEAAiE;IACjE,sFAAsF;IACtF,IAAI,CAAU;IACd,+EAA+E;IAC/E,cAAc,CAAsB;IACpC,yEAAyE;IACzE,QAAQ,CAAW;IACnB,wDAAwD;IACxD,YAAY,CAAe;IAE3B,YACI,SAAiB,EACjB,IAAY,EACZ,SAAiB;IACjB,sFAAsF;IACtF,IAAa,EACb,cAAmC,EACnC,QAAkB,EAClB,YAA0B;QAE1B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;QACrC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;IACrC,CAAC;CACJ;AAnCD,kCAmCC;AAED;;;;;;GAMG;AACH,MAAsB,WAAW;CAGhC;AAHD,kCAGC","sourcesContent":["import { AuthMode } from '@webpieces/core-util';\n\n/**\n * Scheduling options for an enqueued task. Travels out-of-band (via the scheduler\n * frame in RequestContext) so the shared API method signature stays `foo(req)` on\n * both the client and the controller.\n */\nexport class ScheduleInfo {\n /** Absolute epoch-millis to run at; omitted = run as soon as possible. */\n epochMsToRunAt?: number;\n /** Per-task dispatch deadline in seconds. */\n taskTimeoutSeconds?: number;\n /**\n * Deterministic dedup name → the Cloud Task resource name. A second enqueue with\n * the same name is rejected ALREADY_EXISTS (treated as success = idempotent).\n */\n dedupName?: string;\n\n constructor(epochMsToRunAt?: number, taskTimeoutSeconds?: number, dedupName?: string) {\n this.epochMsToRunAt = epochMsToRunAt;\n this.taskTimeoutSeconds = taskTimeoutSeconds;\n this.dedupName = dedupName;\n }\n}\n\n/** Handle to an enqueued task (its Cloud Tasks id), returned by the scheduler. */\nexport class JobReference {\n taskId: string;\n\n constructor(taskId: string) {\n this.taskId = taskId;\n }\n}\n\n/**\n * Everything an invoker needs to enqueue one task. Built by the enqueue proxy from\n * the shared @PubSub contract's decorators + the ambient RequestContext.\n */\nexport class TaskRequest {\n /** Callee base URL (e.g. https://email-svc-123.us-central1.run.app). */\n targetUrl: string;\n /** Endpoint path the task is delivered to (basePath + endpoint, e.g. /email/send). */\n path: string;\n /** Cloud Tasks queue name (getQueueName: @Queue override or `${Api}-${method}`). */\n queueName: string;\n /** The request DTO (serialized to the POST body on delivery). */\n // webpieces-disable no-any-unknown -- request DTO type is erased at the task boundary\n body: unknown;\n /** Context headers to propagate (txId/requestId/tenant…), already resolved. */\n contextHeaders: Map<string, string>;\n /** The endpoint's auth mode — how the invoker authenticates delivery. */\n authMode: AuthMode;\n /** Scheduling options (dedup name, run-at, timeout). */\n scheduleInfo: ScheduleInfo;\n\n constructor(\n targetUrl: string,\n path: string,\n queueName: string,\n // webpieces-disable no-any-unknown -- request DTO type is erased at the task boundary\n body: unknown,\n contextHeaders: Map<string, string>,\n authMode: AuthMode,\n scheduleInfo: ScheduleInfo,\n ) {\n this.targetUrl = targetUrl;\n this.path = path;\n this.queueName = queueName;\n this.body = body;\n this.contextHeaders = contextHeaders;\n this.authMode = authMode;\n this.scheduleInfo = scheduleInfo;\n }\n}\n\n/**\n * The transport that actually enqueues a task. Abstract class so it doubles as the\n * inversify DI token (inject by type, no Symbol). Bind GcpTaskInvoker in prod or\n * InMemoryTaskInvoker in tests/local dev. Both deliver a task the same way — a plain\n * HTTP POST to `targetUrl + path` — GcpTaskInvoker via Google Cloud Tasks, and\n * InMemoryTaskInvoker via an in-process queue that fetches the target directly.\n */\nexport abstract class TaskInvoker {\n abstract enqueue(request: TaskRequest): Promise<JobReference>;\n abstract delete(ref: JobReference): Promise<void>;\n}\n"]}
1
+ {"version":3,"file":"TaskTypes.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/TaskTypes.ts"],"names":[],"mappings":";;;AAEA;;;;GAIG;AACH,MAAa,YAAY;IACrB,0EAA0E;IAC1E,cAAc,CAAU;IACxB,6CAA6C;IAC7C,kBAAkB,CAAU;IAC5B;;;OAGG;IACH,SAAS,CAAU;IAEnB,YAAY,cAAuB,EAAE,kBAA2B,EAAE,SAAkB;QAChF,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;QACrC,IAAI,CAAC,kBAAkB,GAAG,kBAAkB,CAAC;QAC7C,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,CAAC;CACJ;AAhBD,oCAgBC;AAED,kFAAkF;AAClF,MAAa,YAAY;IACrB,MAAM,CAAS;IAEf,YAAY,MAAc;QACtB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACzB,CAAC;CACJ;AAND,oCAMC;AAED;;;GAGG;AACH,MAAa,WAAW;IACpB,wEAAwE;IACxE,SAAS,CAAS;IAClB,sFAAsF;IACtF,IAAI,CAAS;IACb,oFAAoF;IACpF,SAAS,CAAS;IAClB,iEAAiE;IACjE,sFAAsF;IACtF,IAAI,CAAU;IACd,+EAA+E;IAC/E,cAAc,CAAsB;IACpC,yEAAyE;IACzE,QAAQ,CAAW;IACnB,wDAAwD;IACxD,YAAY,CAAe;IAE3B,YACI,SAAiB,EACjB,IAAY,EACZ,SAAiB;IACjB,sFAAsF;IACtF,IAAa,EACb,cAAmC,EACnC,QAAkB,EAClB,YAA0B;QAE1B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;QACrC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;IACrC,CAAC;CACJ;AAnCD,kCAmCC;AAED;;;;;;GAMG;AACH,MAAsB,WAAW;IAC7B,oBAAoB,CAAC,IAA6C;QAC9D,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,MAAgB,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,MAAM,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC;QAC7G,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAC9B,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QACtD,MAAM,IAAI,KAAK,CAAC,2EAA2E,CAAC,CAAC;IACjG,CAAC;CAGJ;AATD,kCASC","sourcesContent":["import { AuthMode } from '@webpieces/core-util';\n\n/**\n * Scheduling options for an enqueued task. Travels out-of-band (via the scheduler\n * frame in RequestContext) so the shared API method signature stays `foo(req)` on\n * both the client and the controller.\n */\nexport class ScheduleInfo {\n /** Absolute epoch-millis to run at; omitted = run as soon as possible. */\n epochMsToRunAt?: number;\n /** Per-task dispatch deadline in seconds. */\n taskTimeoutSeconds?: number;\n /**\n * Deterministic dedup name → the Cloud Task resource name. A second enqueue with\n * the same name is rejected ALREADY_EXISTS (treated as success = idempotent).\n */\n dedupName?: string;\n\n constructor(epochMsToRunAt?: number, taskTimeoutSeconds?: number, dedupName?: string) {\n this.epochMsToRunAt = epochMsToRunAt;\n this.taskTimeoutSeconds = taskTimeoutSeconds;\n this.dedupName = dedupName;\n }\n}\n\n/** Handle to an enqueued task (its Cloud Tasks id), returned by the scheduler. */\nexport class JobReference {\n taskId: string;\n\n constructor(taskId: string) {\n this.taskId = taskId;\n }\n}\n\n/**\n * Everything an invoker needs to enqueue one task. Built by the enqueue proxy from\n * the shared @PubSub contract's decorators + the ambient RequestContext.\n */\nexport class TaskRequest {\n /** Callee base URL (e.g. https://email-svc-123.us-central1.run.app). */\n targetUrl: string;\n /** Endpoint path the task is delivered to (basePath + endpoint, e.g. /email/send). */\n path: string;\n /** Cloud Tasks queue name (getQueueName: @Queue override or `${Api}-${method}`). */\n queueName: string;\n /** The request DTO (serialized to the POST body on delivery). */\n // webpieces-disable no-any-unknown -- request DTO type is erased at the task boundary\n body: unknown;\n /** Context headers to propagate (txId/requestId/tenant…), already resolved. */\n contextHeaders: Map<string, string>;\n /** The endpoint's auth mode — how the invoker authenticates delivery. */\n authMode: AuthMode;\n /** Scheduling options (dedup name, run-at, timeout). */\n scheduleInfo: ScheduleInfo;\n\n constructor(\n targetUrl: string,\n path: string,\n queueName: string,\n // webpieces-disable no-any-unknown -- request DTO type is erased at the task boundary\n body: unknown,\n contextHeaders: Map<string, string>,\n authMode: AuthMode,\n scheduleInfo: ScheduleInfo,\n ) {\n this.targetUrl = targetUrl;\n this.path = path;\n this.queueName = queueName;\n this.body = body;\n this.contextHeaders = contextHeaders;\n this.authMode = authMode;\n this.scheduleInfo = scheduleInfo;\n }\n}\n\n/**\n * The transport that actually enqueues a task. Abstract class so it doubles as the\n * inversify DI token (inject by type, no Symbol). Bind GcpTaskInvoker in prod or\n * InMemoryTaskInvoker in tests/local dev. Both deliver a task the same way — a plain\n * HTTP POST to `targetUrl + path` — GcpTaskInvoker via Google Cloud Tasks, and\n * InMemoryTaskInvoker via an in-process queue that fetches the target directly.\n */\nexport abstract class TaskInvoker {\n selectAuthentication(meta: import('@webpieces/core-util').AuthMeta): AuthMode {\n const selected = meta.methods.find((method: AuthMode) => method.kind === 'oidc' || method.kind === 'public');\n if (selected) return selected;\n if (meta.methods.length === 1) return meta.methods[0];\n throw new Error('No configured Cloud Tasks credential satisfies the declared alternatives.');\n }\n abstract enqueue(request: TaskRequest): Promise<JobReference>;\n abstract delete(ref: JobReference): Promise<void>;\n}\n"]}