@originalvoices/job-echo-client 0.3.5 → 0.4.0

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/dist/index.cjs CHANGED
@@ -529,7 +529,31 @@ function createWorker(cfg) {
529
529
  const echo = createClient(cfg);
530
530
  const prefix = cfg.prefix ?? "/jobs";
531
531
  const app = new import_hono.Hono();
532
+ app.onError((err, c) => {
533
+ console.error(`[job-echo] ${c.req.method} ${c.req.path} error:`, err);
534
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
535
+ });
536
+ app.get("/health", (c) => c.text("ok"));
532
537
  app.get("/healthz", (c) => c.text("ok"));
538
+ if (cfg.log !== false) {
539
+ app.use(`${prefix}/*`, async (c, next) => {
540
+ const start = Date.now();
541
+ console.log(`[job-echo] --> ${c.req.method} ${c.req.path}`);
542
+ await next();
543
+ console.log(
544
+ `[job-echo] <-- ${c.req.method} ${c.req.path} ${c.res.status} (${Date.now() - start}ms)`
545
+ );
546
+ });
547
+ }
548
+ if (cfg.workerSecret) {
549
+ const secret = cfg.workerSecret;
550
+ app.use(`${prefix}/*`, async (c, next) => {
551
+ if (c.req.header("X-Worker-Secret") !== secret) {
552
+ return c.json({ error: "unauthorized" }, 401);
553
+ }
554
+ return next();
555
+ });
556
+ }
533
557
  for (const job of cfg.jobs) {
534
558
  app.post(`${prefix}/${job.name}`, async (c) => {
535
559
  const input = await c.req.json().catch(() => ({}));
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/auth.ts","../src/firestore-rest.ts","../src/store.ts","../src/client.ts","../src/dispatcher.ts","../src/job.ts","../src/worker.ts","../src/sync.ts"],"sourcesContent":["export { createClient } from './client'\nexport type { JobEchoClient } from './client'\nexport { defineJob } from './job'\nexport type { Job, JobDefinition, JobContext, JobInput } from './job'\nexport {\n createWorker,\n runScheduleSyncIfRequested,\n startWorker,\n} from './worker'\nexport type { WorkerConfig } from './worker'\nexport { syncSchedules } from './sync'\nexport type { SyncReport } from './sync'\nexport type { ClientConfig, JobEvent, JobStatus } from './types'\n","const METADATA_URL =\n 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token'\n\ninterface TokenResponse {\n access_token: string\n expires_in: number\n token_type: string\n}\n\nlet cached: { token: string; expiresAt: number } | null = null\n\n/**\n * Fetch an OAuth2 access token from GCP's metadata server. The token is\n * cached in-process until 60s before expiry.\n *\n * Only works on GCP compute (Cloud Run, GCE, GKE, Cloud Functions, etc.).\n * For non-GCP environments the package's production code path is not\n * expected to run — dev mode short-circuits before this is ever called.\n */\nexport async function getAccessToken(): Promise<string> {\n const now = Date.now()\n if (cached && cached.expiresAt > now + 60_000) return cached.token\n\n const res = await fetch(METADATA_URL, { headers: { 'Metadata-Flavor': 'Google' } })\n if (!res.ok) {\n throw new Error(\n `@originalvoices/job-echo-client: failed to fetch GCP access token (status ${res.status}). Is this running on GCP compute?`,\n )\n }\n const body = (await res.json()) as TokenResponse\n cached = {\n token: body.access_token,\n expiresAt: now + body.expires_in * 1000,\n }\n return cached.token\n}\n","import { getAccessToken } from './auth'\n\n/**\n * Minimal Firestore REST client — just enough to create/patch/query documents.\n * Uses the metadata-server access token; no SDK, no native deps.\n *\n * Docs: https://firestore.googleapis.com/$discovery/rest?version=v1\n */\n\nconst BASE = 'https://firestore.googleapis.com/v1'\n\nfunction baseUrl(projectId: string) {\n return `${BASE}/projects/${projectId}/databases/(default)/documents`\n}\n\nasync function authed(url: string, init?: RequestInit): Promise<Response> {\n const token = await getAccessToken()\n const res = await fetch(url, {\n ...init,\n headers: {\n ...(init?.headers ?? {}),\n authorization: `Bearer ${token}`,\n 'content-type': 'application/json',\n },\n })\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new Error(`Firestore REST ${res.status}: ${body}`)\n }\n return res\n}\n\n// ── Typed value encoding / decoding ───────────────────────────────────────────\n\ntype FsValue =\n | { stringValue: string }\n | { integerValue: string }\n | { doubleValue: number }\n | { booleanValue: boolean }\n | { nullValue: null }\n | { timestampValue: string }\n | { mapValue: { fields: Record<string, FsValue> } }\n | { arrayValue: { values: FsValue[] } }\n\nexport function encodeValue(v: unknown): FsValue {\n if (v === null || v === undefined) return { nullValue: null }\n if (typeof v === 'string') return { stringValue: v }\n if (typeof v === 'boolean') return { booleanValue: v }\n if (typeof v === 'number') {\n return Number.isInteger(v)\n ? { integerValue: String(v) }\n : { doubleValue: v }\n }\n if (Array.isArray(v)) {\n return { arrayValue: { values: v.map(encodeValue) } }\n }\n if (typeof v === 'object') {\n return { mapValue: { fields: encodeFields(v as Record<string, unknown>) } }\n }\n // Fallback — coerce to string\n return { stringValue: String(v) }\n}\n\nexport function encodeFields(obj: Record<string, unknown>): Record<string, FsValue> {\n const out: Record<string, FsValue> = {}\n for (const [k, v] of Object.entries(obj)) out[k] = encodeValue(v)\n return out\n}\n\nexport function decodeValue(v: FsValue): unknown {\n if ('stringValue' in v) return v.stringValue\n if ('integerValue' in v) return Number(v.integerValue)\n if ('doubleValue' in v) return v.doubleValue\n if ('booleanValue' in v) return v.booleanValue\n if ('nullValue' in v) return null\n if ('timestampValue' in v) return v.timestampValue\n if ('mapValue' in v) return decodeFields(v.mapValue.fields ?? {})\n if ('arrayValue' in v) return (v.arrayValue.values ?? []).map(decodeValue)\n return null\n}\n\nexport function decodeFields(\n fields: Record<string, FsValue>,\n): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(fields)) out[k] = decodeValue(v)\n return out\n}\n\n// ── Public operations ─────────────────────────────────────────────────────────\n\nfunction docIdFromName(name: string): string {\n // name looks like \"projects/xxx/databases/(default)/documents/collection/docId\"\n return name.split('/').pop() ?? name\n}\n\nexport interface FirestoreDoc {\n id: string\n data: Record<string, unknown>\n}\n\nexport async function createDoc(\n projectId: string,\n collection: string,\n data: Record<string, unknown>,\n): Promise<FirestoreDoc> {\n const res = await authed(`${baseUrl(projectId)}/${collection}`, {\n method: 'POST',\n body: JSON.stringify({ fields: encodeFields(data) }),\n })\n const body = (await res.json()) as { name: string; fields?: Record<string, FsValue> }\n return { id: docIdFromName(body.name), data: decodeFields(body.fields ?? {}) }\n}\n\nexport async function patchDoc(\n projectId: string,\n collection: string,\n id: string,\n patch: Record<string, unknown>,\n): Promise<void> {\n const params = Object.keys(patch)\n .map((k) => `updateMask.fieldPaths=${encodeURIComponent(k)}`)\n .join('&')\n await authed(`${baseUrl(projectId)}/${collection}/${id}?${params}`, {\n method: 'PATCH',\n body: JSON.stringify({ fields: encodeFields(patch) }),\n })\n}\n\n/**\n * Run a simple equality-filter query against a single collection.\n * Returns at most `limit` docs.\n */\nexport async function queryDocs(\n projectId: string,\n collection: string,\n filters: Array<{ field: string; value: string }>,\n limit = 1,\n): Promise<FirestoreDoc[]> {\n const compositeFilter = {\n compositeFilter: {\n op: 'AND',\n filters: filters.map((f) => ({\n fieldFilter: {\n field: { fieldPath: f.field },\n op: 'EQUAL',\n value: { stringValue: f.value },\n },\n })),\n },\n }\n\n const structuredQuery = {\n from: [{ collectionId: collection }],\n where: filters.length ? compositeFilter : undefined,\n limit,\n }\n\n const res = await authed(`${baseUrl(projectId)}:runQuery`, {\n method: 'POST',\n body: JSON.stringify({ structuredQuery }),\n })\n const rows = (await res.json()) as Array<{\n document?: { name: string; fields?: Record<string, FsValue> }\n }>\n return rows\n .filter((r) => r.document)\n .map((r) => ({\n id: docIdFromName(r.document!.name),\n data: decodeFields(r.document!.fields ?? {}),\n }))\n}\n","import { createDoc, patchDoc, queryDocs } from './firestore-rest'\nimport type { JobEvent, JobStatus } from './types'\n\nconst PROJECTS = 'projects'\nconst ENVIRONMENTS = 'environments'\nconst EVENTS = 'events'\n\nconst now = () => Date.now()\n\nexport async function findOrCreateContext(\n gcpProjectId: string,\n projectName: string,\n envName: string,\n): Promise<{ projectId: string; environmentId: string }> {\n const projMatches = await queryDocs(\n gcpProjectId,\n PROJECTS,\n [{ field: 'name', value: projectName }],\n 1,\n )\n const projectId = projMatches[0]\n ? projMatches[0].id\n : (await createDoc(gcpProjectId, PROJECTS, { name: projectName, createdAt: now() })).id\n\n const envMatches = await queryDocs(\n gcpProjectId,\n ENVIRONMENTS,\n [\n { field: 'projectId', value: projectId },\n { field: 'name', value: envName },\n ],\n 1,\n )\n const environmentId = envMatches[0]\n ? envMatches[0].id\n : (\n await createDoc(gcpProjectId, ENVIRONMENTS, {\n projectId,\n name: envName,\n createdAt: now(),\n })\n ).id\n\n return { projectId, environmentId }\n}\n\nexport async function createEvent(\n gcpProjectId: string,\n args: {\n projectId: string\n environmentId: string\n name: string\n input?: unknown\n status?: JobStatus\n },\n): Promise<JobEvent> {\n const ts = now()\n const data: Omit<JobEvent, 'id'> = {\n projectId: args.projectId,\n environmentId: args.environmentId,\n name: args.name,\n status: args.status ?? 'in_progress',\n input: args.input ?? null,\n output: null,\n error: null,\n createdAt: ts,\n updatedAt: ts,\n completedAt: null,\n }\n const doc = await createDoc(gcpProjectId, EVENTS, data)\n return { id: doc.id, ...data }\n}\n\nexport async function patchEvent(\n gcpProjectId: string,\n id: string,\n patch: { status?: JobStatus; output?: unknown; error?: string | null },\n): Promise<void> {\n const ts = now()\n const update: Record<string, unknown> = { updatedAt: ts }\n if (patch.status !== undefined) {\n update.status = patch.status\n if (patch.status === 'completed' || patch.status === 'failed') {\n update.completedAt = ts\n }\n }\n if (patch.output !== undefined) update.output = patch.output\n if (patch.error !== undefined) update.error = patch.error\n\n await patchDoc(gcpProjectId, EVENTS, id, update)\n}\n","import { createEvent, findOrCreateContext, patchEvent } from './store'\nimport type { ClientConfig, JobEvent } from './types'\n\nexport interface JobEchoClient {\n /** Record a new job event as `in_progress`. Returns the created event. */\n start(name: string, input?: unknown): Promise<JobEvent>\n /** Mark an existing event `completed`. Optionally attach a return value as `output`. */\n complete(id: string, output?: unknown): Promise<void>\n /** Mark an existing event `failed` with an error message. */\n fail(id: string, error: string, output?: unknown): Promise<void>\n}\n\nexport function createClient(cfg: ClientConfig): JobEchoClient {\n // Only write to Firestore in production. Local/dev runs are a no-op —\n // handlers still execute, just no lifecycle events recorded.\n if (process.env.NODE_ENV !== 'production') return disabledClient()\n\n const gcpProjectId =\n cfg.collectorProjectId ??\n process.env.JOB_ECHO_GCP_PROJECT ??\n process.env.FIREBASE_PROJECT_ID\n\n // No collector project configured → run as no-op. Lets services adopt the\n // worker pattern before the dashboard's Firestore is provisioned.\n if (!gcpProjectId) {\n console.warn(\n '[job-echo] no collectorProjectId / JOB_ECHO_GCP_PROJECT / FIREBASE_PROJECT_ID set — Firestore writes disabled',\n )\n return disabledClient()\n }\n\n let contextPromise: Promise<{ projectId: string; environmentId: string }> | null = null\n const context = () => {\n if (!contextPromise) {\n contextPromise = findOrCreateContext(gcpProjectId, cfg.project, cfg.environment)\n }\n return contextPromise\n }\n\n return {\n async start(name, input) {\n const { projectId, environmentId } = await context()\n return createEvent(gcpProjectId, { projectId, environmentId, name, input })\n },\n async complete(id, output) {\n await patchEvent(gcpProjectId, id, { status: 'completed', output })\n },\n async fail(id, error, output) {\n await patchEvent(gcpProjectId, id, { status: 'failed', error, output })\n },\n }\n}\n\nfunction disabledClient(): JobEchoClient {\n const ts = Date.now()\n return {\n async start(name, input) {\n return {\n id: 'disabled',\n projectId: 'disabled',\n environmentId: 'disabled',\n name,\n status: 'in_progress',\n input,\n output: null,\n error: null,\n createdAt: ts,\n updatedAt: ts,\n completedAt: null,\n }\n },\n async complete() {},\n async fail() {},\n }\n}\n","import { getAccessToken } from './auth'\n\nexport interface Dispatcher {\n dispatch(jobName: string, payload: unknown): Promise<void>\n}\n\nexport interface HttpDispatcherConfig {\n baseUrl: string\n headers?: Record<string, string>\n prefix?: string\n}\n\nexport interface CloudTasksDispatcherConfig {\n projectId: string\n location: string\n queue: string\n workerUrl: string\n serviceAccountEmail: string\n prefix?: string\n audience?: string\n}\n\n/**\n * HTTP dispatcher: plain POST to `{baseUrl}{prefix}/{jobName}`.\n */\nexport function createHttpDispatcher(cfg: HttpDispatcherConfig): Dispatcher {\n const prefix = cfg.prefix ?? '/jobs'\n return {\n async dispatch(jobName, payload) {\n const res = await fetch(`${cfg.baseUrl}${prefix}/${jobName}`, {\n method: 'POST',\n headers: { 'content-type': 'application/json', ...(cfg.headers ?? {}) },\n body: JSON.stringify(payload),\n })\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new Error(\n `job-echo dispatch '${jobName}' failed: ${res.status} ${body}`.trim(),\n )\n }\n },\n }\n}\n\n/**\n * Cloud Tasks dispatcher via the REST API — no @google-cloud/tasks SDK.\n * Enqueues an HTTP task with an OIDC token so the worker can verify\n * the caller identity.\n */\nexport function createCloudTasksDispatcher(\n cfg: CloudTasksDispatcherConfig,\n): Dispatcher {\n const prefix = cfg.prefix ?? '/jobs'\n const audience = cfg.audience ?? cfg.workerUrl\n const url = `https://cloudtasks.googleapis.com/v2/projects/${cfg.projectId}/locations/${cfg.location}/queues/${cfg.queue}/tasks`\n\n return {\n async dispatch(jobName, payload) {\n const token = await getAccessToken()\n const body = {\n task: {\n httpRequest: {\n httpMethod: 'POST',\n url: `${cfg.workerUrl}${prefix}/${jobName}`,\n headers: { 'content-type': 'application/json' },\n body: Buffer.from(JSON.stringify(payload)).toString('base64'),\n oidcToken: {\n serviceAccountEmail: cfg.serviceAccountEmail,\n audience,\n },\n },\n },\n }\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n authorization: `Bearer ${token}`,\n 'content-type': 'application/json',\n },\n body: JSON.stringify(body),\n })\n if (!res.ok) {\n const respBody = await res.text().catch(() => '')\n throw new Error(\n `job-echo Cloud Tasks enqueue '${jobName}' failed: ${res.status} ${respBody}`.trim(),\n )\n }\n },\n }\n}\n\nlet active: Dispatcher | null = null\n\n/**\n * Returns the dispatcher, auto-detected from env on first call:\n * - `NODE_ENV=production`: Cloud Tasks (requires `JOB_ECHO_TASKS_*` env vars)\n * - otherwise: HTTP to `JOB_ECHO_WORKER_URL` (default `http://localhost:8080`)\n */\nexport function getDispatcher(): Dispatcher {\n if (!active) active = autoDetectDispatcher()\n return active\n}\n\n/**\n * Parse a Cloud Tasks queue resource path:\n * `projects/{project}/locations/{location}/queues/{queue}`\n */\nfunction parseQueuePath(\n path: string,\n): { projectId: string; location: string; queue: string } {\n const m = path.match(\n /^projects\\/([^/]+)\\/locations\\/([^/]+)\\/queues\\/([^/]+)$/,\n )\n if (!m) {\n throw new Error(\n `@originalvoices/job-echo-client: JOB_ECHO_TASKS_QUEUE must be a full resource path (projects/{p}/locations/{l}/queues/{q}), got \"${path}\"`,\n )\n }\n return { projectId: m[1]!, location: m[2]!, queue: m[3]! }\n}\n\nfunction autoDetectDispatcher(): Dispatcher {\n if (process.env.NODE_ENV === 'production') {\n const required = {\n JOB_ECHO_TASKS_QUEUE: process.env.JOB_ECHO_TASKS_QUEUE,\n JOB_ECHO_WORKER_URL: process.env.JOB_ECHO_WORKER_URL,\n JOB_ECHO_TASKS_SA_EMAIL: process.env.JOB_ECHO_TASKS_SA_EMAIL,\n }\n const missing = Object.entries(required)\n .filter(([, v]) => !v)\n .map(([k]) => k)\n if (missing.length) {\n throw new Error(\n `@originalvoices/job-echo-client: Cloud Tasks dispatcher needs env vars: ${missing.join(', ')}`,\n )\n }\n const { projectId, location, queue } = parseQueuePath(\n required.JOB_ECHO_TASKS_QUEUE!,\n )\n return createCloudTasksDispatcher({\n projectId,\n location,\n queue,\n workerUrl: required.JOB_ECHO_WORKER_URL!,\n serviceAccountEmail: required.JOB_ECHO_TASKS_SA_EMAIL!,\n })\n }\n\n const baseUrl = process.env.JOB_ECHO_WORKER_URL ?? 'http://localhost:8080'\n return createHttpDispatcher({ baseUrl })\n}\n","import { getDispatcher } from './dispatcher'\nimport type { JobEchoClient } from './client'\n\nexport interface JobContext {\n /** Firestore event id for this invocation (status=in_progress). */\n eventId: string\n /** The raw job-echo client — use for nested events or custom updates. */\n echo: JobEchoClient\n}\n\nexport interface JobDefinition<Input> {\n name: string\n handler: (input: Input, ctx: JobContext) => Promise<unknown>\n /**\n * Optional cron expression. When set, `syncSchedules` will provision a\n * Cloud Scheduler job that fires this handler on the given cadence.\n */\n schedule?: string\n /** IANA timezone for `schedule`. Defaults to \"Etc/UTC\". */\n timezone?: string\n}\n\nexport interface Job<Input> extends JobDefinition<Input> {\n /**\n * Send `input` to the configured dispatcher (defaults to HTTP POST to the\n * worker). Requires `configureDispatcher({ baseUrl })` at startup.\n */\n emit(input: Input): Promise<void>\n /** Type marker — not present at runtime. */\n _input?: Input\n}\n\n/**\n * Define a typed job. Returns an object with a bound `.emit(input)` so call\n * sites don't have to know about the worker URL or shape of the HTTP call.\n *\n * ```ts\n * export const sendMessage = defineJob({\n * name: 'send-message',\n * async handler(input: { threadId: string }) { ... },\n * })\n *\n * // elsewhere, fully typed:\n * await sendMessage.emit({ threadId: 't1' })\n * ```\n */\nexport function defineJob<Input>(def: JobDefinition<Input>): Job<Input> {\n return {\n ...def,\n emit: (input) => getDispatcher().dispatch(def.name, input),\n }\n}\n\n/** Extract the input type from a Job. */\nexport type JobInput<J> = J extends Job<infer I> ? I : never\n","import { Hono } from 'hono'\nimport { serve } from '@hono/node-server'\nimport { createClient, type JobEchoClient } from './client'\nimport type { Job } from './job'\nimport { syncSchedules } from './sync'\nimport type { ClientConfig } from './types'\n\nexport interface WorkerConfig extends ClientConfig {\n jobs: Job<any>[]\n /** Route prefix. Defaults to \"/jobs\". */\n prefix?: string\n}\n\n/**\n * Build a Hono app that exposes POST {prefix}/{jobName} for each job and\n * wraps each handler with job-echo lifecycle tracking.\n */\nexport function createWorker(cfg: WorkerConfig): Hono {\n const echo = createClient(cfg)\n const prefix = cfg.prefix ?? '/jobs'\n const app = new Hono()\n\n app.get('/healthz', (c) => c.text('ok'))\n\n for (const job of cfg.jobs) {\n app.post(`${prefix}/${job.name}`, async (c) => {\n const input = (await c.req.json().catch(() => ({}))) as unknown\n const event = await echo.start(job.name, input)\n try {\n const result = await job.handler(input as any, { eventId: event.id, echo })\n await echo.complete(event.id, result ?? undefined)\n return c.json({ ok: true, eventId: event.id, result: result ?? null })\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n await echo.fail(event.id, message)\n return c.json({ ok: false, eventId: event.id, error: message }, 500)\n }\n })\n }\n\n return app\n}\n\n/**\n * If `JOB_ECHO_SYNC_MODE=1` is set, fire schedule sync (which will\n * `process.exit(0)` on success / `1` on failure) and return `true` so\n * the caller knows to skip normal startup. Otherwise returns `false`.\n *\n * Synchronous return so it composes naturally in CJS entrypoints\n * (no top-level `await` needed).\n *\n * ```ts\n * if (runScheduleSyncIfRequested(allJobs)) {\n * // sync running; don't start server\n * } else {\n * startServer()\n * }\n * ```\n */\nexport function runScheduleSyncIfRequested<\n J extends { name: string; schedule?: string; timezone?: string },\n>(jobs: J[]): boolean {\n if (process.env.JOB_ECHO_SYNC_MODE !== '1') return false\n\n syncSchedules(jobs)\n .then((report) => {\n console.log(\n '[job-echo] schedule sync complete:\\n' + JSON.stringify(report, null, 2),\n )\n process.exit(0)\n })\n .catch((err) => {\n console.error('[job-echo] schedule sync failed:', err)\n process.exit(1)\n })\n\n return true\n}\n\n/**\n * Convenience: build a worker and start the HTTP server on `port`\n * (defaults to `PORT` env or 8080). Also handles `JOB_ECHO_SYNC_MODE=1`\n * via `runScheduleSyncIfRequested` — same binary, two modes.\n */\nexport function startWorker(\n cfg: WorkerConfig & { port?: number },\n): ReturnType<typeof serve> | void {\n if (process.env.JOB_ECHO_SYNC_MODE === '1') {\n runScheduleSyncIfRequested(cfg.jobs)\n return\n }\n\n const app = createWorker(cfg)\n const port = cfg.port ?? Number(process.env.PORT ?? 8080)\n console.log(`[job-echo] worker listening on :${port}`)\n return serve({ fetch: app.fetch, port })\n}\n","import { getAccessToken } from './auth'\n\nconst BASE = 'https://cloudscheduler.googleapis.com/v1'\n\nexport interface SyncReport {\n created: string[]\n updated: string[]\n deleted: string[]\n unchanged: string[]\n}\n\ninterface ParsedQueue {\n projectId: string\n location: string\n queue: string\n fullPath: string\n}\n\nfunction parseQueuePath(path: string): ParsedQueue {\n const m = path.match(\n /^projects\\/([^/]+)\\/locations\\/([^/]+)\\/queues\\/([^/]+)$/,\n )\n if (!m) {\n throw new Error(\n `@originalvoices/job-echo-client: JOB_ECHO_TASKS_QUEUE must be a full resource path (projects/{p}/locations/{l}/queues/{q}), got \"${path}\"`,\n )\n }\n return { projectId: m[1]!, location: m[2]!, queue: m[3]!, fullPath: path }\n}\n\nfunction sanitizeName(name: string): string {\n return name.replace(/[^a-zA-Z0-9-]/g, '-').replace(/^-+|-+$/g, '')\n}\n\nasync function authed(url: string, init?: RequestInit): Promise<Response> {\n const token = await getAccessToken()\n const res = await fetch(url, {\n ...init,\n headers: {\n ...(init?.headers ?? {}),\n authorization: `Bearer ${token}`,\n 'content-type': 'application/json',\n },\n })\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new Error(`Cloud Scheduler ${res.status}: ${body}`)\n }\n return res\n}\n\ninterface SchedulerJob {\n name: string\n schedule: string\n timeZone: string\n httpTarget: {\n uri: string\n httpMethod: string\n headers?: Record<string, string>\n body?: string\n oauthToken?: {\n serviceAccountEmail: string\n scope?: string\n }\n }\n labels?: Record<string, string>\n}\n\nfunction buildSchedulerJob(args: {\n fullName: string\n cron: string\n timezone: string\n queue: ParsedQueue\n workerUrl: string\n invokerSa: string\n jobName: string\n appLabel: string\n}): SchedulerJob {\n const task = {\n httpRequest: {\n url: `${args.workerUrl}/jobs/${args.jobName}`,\n httpMethod: 'POST',\n headers: { 'content-type': 'application/json' },\n body: Buffer.from('{}').toString('base64'),\n oidcToken: {\n serviceAccountEmail: args.invokerSa,\n audience: args.workerUrl,\n },\n },\n }\n\n return {\n name: args.fullName,\n schedule: args.cron,\n timeZone: args.timezone,\n httpTarget: {\n uri: `https://cloudtasks.googleapis.com/v2/${args.queue.fullPath}/tasks`,\n httpMethod: 'POST',\n headers: { 'content-type': 'application/json' },\n body: Buffer.from(JSON.stringify({ task })).toString('base64'),\n oauthToken: {\n serviceAccountEmail: args.invokerSa,\n scope: 'https://www.googleapis.com/auth/cloud-platform',\n },\n },\n labels: {\n 'managed-by': 'job-echo',\n app: args.appLabel,\n job: args.jobName,\n },\n }\n}\n\nasync function listManaged(\n queue: ParsedQueue,\n appLabel: string,\n): Promise<SchedulerJob[]> {\n const filter = `labels.managed-by=\"job-echo\" AND labels.app=\"${appLabel}\"`\n const url = `${BASE}/projects/${queue.projectId}/locations/${queue.location}/jobs?filter=${encodeURIComponent(filter)}`\n const res = await authed(url)\n const body = (await res.json()) as { jobs?: SchedulerJob[] }\n return body.jobs ?? []\n}\n\nfunction differs(a: SchedulerJob, b: SchedulerJob): boolean {\n return (\n a.schedule !== b.schedule ||\n a.timeZone !== b.timeZone ||\n a.httpTarget.uri !== b.httpTarget.uri ||\n a.httpTarget.body !== b.httpTarget.body\n )\n}\n\n/**\n * Reconcile Cloud Scheduler jobs to match the schedules declared on the\n * given jobs. Only touches schedulers labelled `managed-by=job-echo` and\n * `app=<JOB_ECHO_APP_LABEL>` — manually created schedulers are untouched.\n *\n * Required env:\n * - JOB_ECHO_TASKS_QUEUE (full resource path: projects/.../queues/...)\n * - JOB_ECHO_WORKER_URL (https URL of the worker service)\n * - JOB_ECHO_TASKS_SA_EMAIL (SA the scheduler-fired tasks run as)\n * - JOB_ECHO_APP_LABEL (e.g. \"platform-prod\" — scopes the sync)\n */\nexport async function syncSchedules<\n J extends { name: string; schedule?: string; timezone?: string },\n>(jobs: J[]): Promise<SyncReport> {\n const env = {\n JOB_ECHO_TASKS_QUEUE: process.env.JOB_ECHO_TASKS_QUEUE,\n JOB_ECHO_WORKER_URL: process.env.JOB_ECHO_WORKER_URL,\n JOB_ECHO_TASKS_SA_EMAIL: process.env.JOB_ECHO_TASKS_SA_EMAIL,\n JOB_ECHO_APP_LABEL: process.env.JOB_ECHO_APP_LABEL,\n }\n const missing = Object.entries(env)\n .filter(([, v]) => !v)\n .map(([k]) => k)\n if (missing.length) {\n throw new Error(\n `@originalvoices/job-echo-client: syncSchedules needs env vars: ${missing.join(', ')}`,\n )\n }\n\n const queue = parseQueuePath(env.JOB_ECHO_TASKS_QUEUE!)\n const workerUrl = env.JOB_ECHO_WORKER_URL!\n const invokerSa = env.JOB_ECHO_TASKS_SA_EMAIL!\n const appLabel = env.JOB_ECHO_APP_LABEL!\n\n const scheduled = jobs.filter((j) => Boolean(j.schedule))\n\n const desired = new Map<string, SchedulerJob>()\n for (const job of scheduled) {\n const schedulerName = sanitizeName(`${appLabel}-${job.name}`)\n const fullName = `projects/${queue.projectId}/locations/${queue.location}/jobs/${schedulerName}`\n desired.set(\n fullName,\n buildSchedulerJob({\n fullName,\n cron: job.schedule!,\n timezone: job.timezone ?? 'Etc/UTC',\n queue,\n workerUrl,\n invokerSa,\n jobName: job.name,\n appLabel,\n }),\n )\n }\n\n const existing = await listManaged(queue, appLabel)\n const existingByName = new Map(existing.map((j) => [j.name, j]))\n\n const report: SyncReport = {\n created: [],\n updated: [],\n deleted: [],\n unchanged: [],\n }\n\n for (const [name, want] of desired) {\n const have = existingByName.get(name)\n if (!have) {\n const url = `${BASE}/projects/${queue.projectId}/locations/${queue.location}/jobs`\n await authed(url, { method: 'POST', body: JSON.stringify(want) })\n report.created.push(name)\n } else if (differs(have, want)) {\n const url = `${BASE}/${name}?updateMask=schedule,timeZone,httpTarget,labels`\n await authed(url, { method: 'PATCH', body: JSON.stringify(want) })\n report.updated.push(name)\n } else {\n report.unchanged.push(name)\n }\n }\n\n for (const name of existingByName.keys()) {\n if (!desired.has(name)) {\n await authed(`${BASE}/${name}`, { method: 'DELETE' })\n report.deleted.push(name)\n }\n }\n\n return report\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,IAAM,eACJ;AAQF,IAAI,SAAsD;AAU1D,eAAsB,iBAAkC;AACtD,QAAMA,OAAM,KAAK,IAAI;AACrB,MAAI,UAAU,OAAO,YAAYA,OAAM,IAAQ,QAAO,OAAO;AAE7D,QAAM,MAAM,MAAM,MAAM,cAAc,EAAE,SAAS,EAAE,mBAAmB,SAAS,EAAE,CAAC;AAClF,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,IAAI;AAAA,MACR,6EAA6E,IAAI,MAAM;AAAA,IACzF;AAAA,EACF;AACA,QAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,WAAS;AAAA,IACP,OAAO,KAAK;AAAA,IACZ,WAAWA,OAAM,KAAK,aAAa;AAAA,EACrC;AACA,SAAO,OAAO;AAChB;;;AC1BA,IAAM,OAAO;AAEb,SAAS,QAAQ,WAAmB;AAClC,SAAO,GAAG,IAAI,aAAa,SAAS;AACtC;AAEA,eAAe,OAAO,KAAa,MAAuC;AACxE,QAAM,QAAQ,MAAM,eAAe;AACnC,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B,GAAG;AAAA,IACH,SAAS;AAAA,MACP,GAAI,MAAM,WAAW,CAAC;AAAA,MACtB,eAAe,UAAU,KAAK;AAAA,MAC9B,gBAAgB;AAAA,IAClB;AAAA,EACF,CAAC;AACD,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,EAAE;AAC5C,UAAM,IAAI,MAAM,kBAAkB,IAAI,MAAM,KAAK,IAAI,EAAE;AAAA,EACzD;AACA,SAAO;AACT;AAcO,SAAS,YAAY,GAAqB;AAC/C,MAAI,MAAM,QAAQ,MAAM,OAAW,QAAO,EAAE,WAAW,KAAK;AAC5D,MAAI,OAAO,MAAM,SAAU,QAAO,EAAE,aAAa,EAAE;AACnD,MAAI,OAAO,MAAM,UAAW,QAAO,EAAE,cAAc,EAAE;AACrD,MAAI,OAAO,MAAM,UAAU;AACzB,WAAO,OAAO,UAAU,CAAC,IACrB,EAAE,cAAc,OAAO,CAAC,EAAE,IAC1B,EAAE,aAAa,EAAE;AAAA,EACvB;AACA,MAAI,MAAM,QAAQ,CAAC,GAAG;AACpB,WAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,WAAW,EAAE,EAAE;AAAA,EACtD;AACA,MAAI,OAAO,MAAM,UAAU;AACzB,WAAO,EAAE,UAAU,EAAE,QAAQ,aAAa,CAA4B,EAAE,EAAE;AAAA,EAC5E;AAEA,SAAO,EAAE,aAAa,OAAO,CAAC,EAAE;AAClC;AAEO,SAAS,aAAa,KAAuD;AAClF,QAAM,MAA+B,CAAC;AACtC,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,GAAG,EAAG,KAAI,CAAC,IAAI,YAAY,CAAC;AAChE,SAAO;AACT;AAEO,SAAS,YAAY,GAAqB;AAC/C,MAAI,iBAAiB,EAAG,QAAO,EAAE;AACjC,MAAI,kBAAkB,EAAG,QAAO,OAAO,EAAE,YAAY;AACrD,MAAI,iBAAiB,EAAG,QAAO,EAAE;AACjC,MAAI,kBAAkB,EAAG,QAAO,EAAE;AAClC,MAAI,eAAe,EAAG,QAAO;AAC7B,MAAI,oBAAoB,EAAG,QAAO,EAAE;AACpC,MAAI,cAAc,EAAG,QAAO,aAAa,EAAE,SAAS,UAAU,CAAC,CAAC;AAChE,MAAI,gBAAgB,EAAG,SAAQ,EAAE,WAAW,UAAU,CAAC,GAAG,IAAI,WAAW;AACzE,SAAO;AACT;AAEO,SAAS,aACd,QACyB;AACzB,QAAM,MAA+B,CAAC;AACtC,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,MAAM,EAAG,KAAI,CAAC,IAAI,YAAY,CAAC;AACnE,SAAO;AACT;AAIA,SAAS,cAAc,MAAsB;AAE3C,SAAO,KAAK,MAAM,GAAG,EAAE,IAAI,KAAK;AAClC;AAOA,eAAsB,UACpB,WACA,YACA,MACuB;AACvB,QAAM,MAAM,MAAM,OAAO,GAAG,QAAQ,SAAS,CAAC,IAAI,UAAU,IAAI;AAAA,IAC9D,QAAQ;AAAA,IACR,MAAM,KAAK,UAAU,EAAE,QAAQ,aAAa,IAAI,EAAE,CAAC;AAAA,EACrD,CAAC;AACD,QAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,SAAO,EAAE,IAAI,cAAc,KAAK,IAAI,GAAG,MAAM,aAAa,KAAK,UAAU,CAAC,CAAC,EAAE;AAC/E;AAEA,eAAsB,SACpB,WACA,YACA,IACA,OACe;AACf,QAAM,SAAS,OAAO,KAAK,KAAK,EAC7B,IAAI,CAAC,MAAM,yBAAyB,mBAAmB,CAAC,CAAC,EAAE,EAC3D,KAAK,GAAG;AACX,QAAM,OAAO,GAAG,QAAQ,SAAS,CAAC,IAAI,UAAU,IAAI,EAAE,IAAI,MAAM,IAAI;AAAA,IAClE,QAAQ;AAAA,IACR,MAAM,KAAK,UAAU,EAAE,QAAQ,aAAa,KAAK,EAAE,CAAC;AAAA,EACtD,CAAC;AACH;AAMA,eAAsB,UACpB,WACA,YACA,SACA,QAAQ,GACiB;AACzB,QAAM,kBAAkB;AAAA,IACtB,iBAAiB;AAAA,MACf,IAAI;AAAA,MACJ,SAAS,QAAQ,IAAI,CAAC,OAAO;AAAA,QAC3B,aAAa;AAAA,UACX,OAAO,EAAE,WAAW,EAAE,MAAM;AAAA,UAC5B,IAAI;AAAA,UACJ,OAAO,EAAE,aAAa,EAAE,MAAM;AAAA,QAChC;AAAA,MACF,EAAE;AAAA,IACJ;AAAA,EACF;AAEA,QAAM,kBAAkB;AAAA,IACtB,MAAM,CAAC,EAAE,cAAc,WAAW,CAAC;AAAA,IACnC,OAAO,QAAQ,SAAS,kBAAkB;AAAA,IAC1C;AAAA,EACF;AAEA,QAAM,MAAM,MAAM,OAAO,GAAG,QAAQ,SAAS,CAAC,aAAa;AAAA,IACzD,QAAQ;AAAA,IACR,MAAM,KAAK,UAAU,EAAE,gBAAgB,CAAC;AAAA,EAC1C,CAAC;AACD,QAAM,OAAQ,MAAM,IAAI,KAAK;AAG7B,SAAO,KACJ,OAAO,CAAC,MAAM,EAAE,QAAQ,EACxB,IAAI,CAAC,OAAO;AAAA,IACX,IAAI,cAAc,EAAE,SAAU,IAAI;AAAA,IAClC,MAAM,aAAa,EAAE,SAAU,UAAU,CAAC,CAAC;AAAA,EAC7C,EAAE;AACN;;;ACxKA,IAAM,WAAW;AACjB,IAAM,eAAe;AACrB,IAAM,SAAS;AAEf,IAAM,MAAM,MAAM,KAAK,IAAI;AAE3B,eAAsB,oBACpB,cACA,aACA,SACuD;AACvD,QAAM,cAAc,MAAM;AAAA,IACxB;AAAA,IACA;AAAA,IACA,CAAC,EAAE,OAAO,QAAQ,OAAO,YAAY,CAAC;AAAA,IACtC;AAAA,EACF;AACA,QAAM,YAAY,YAAY,CAAC,IAC3B,YAAY,CAAC,EAAE,MACd,MAAM,UAAU,cAAc,UAAU,EAAE,MAAM,aAAa,WAAW,IAAI,EAAE,CAAC,GAAG;AAEvF,QAAM,aAAa,MAAM;AAAA,IACvB;AAAA,IACA;AAAA,IACA;AAAA,MACE,EAAE,OAAO,aAAa,OAAO,UAAU;AAAA,MACvC,EAAE,OAAO,QAAQ,OAAO,QAAQ;AAAA,IAClC;AAAA,IACA;AAAA,EACF;AACA,QAAM,gBAAgB,WAAW,CAAC,IAC9B,WAAW,CAAC,EAAE,MAEZ,MAAM,UAAU,cAAc,cAAc;AAAA,IAC1C;AAAA,IACA,MAAM;AAAA,IACN,WAAW,IAAI;AAAA,EACjB,CAAC,GACD;AAEN,SAAO,EAAE,WAAW,cAAc;AACpC;AAEA,eAAsB,YACpB,cACA,MAOmB;AACnB,QAAM,KAAK,IAAI;AACf,QAAM,OAA6B;AAAA,IACjC,WAAW,KAAK;AAAA,IAChB,eAAe,KAAK;AAAA,IACpB,MAAM,KAAK;AAAA,IACX,QAAQ,KAAK,UAAU;AAAA,IACvB,OAAO,KAAK,SAAS;AAAA,IACrB,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,WAAW;AAAA,IACX,WAAW;AAAA,IACX,aAAa;AAAA,EACf;AACA,QAAM,MAAM,MAAM,UAAU,cAAc,QAAQ,IAAI;AACtD,SAAO,EAAE,IAAI,IAAI,IAAI,GAAG,KAAK;AAC/B;AAEA,eAAsB,WACpB,cACA,IACA,OACe;AACf,QAAM,KAAK,IAAI;AACf,QAAM,SAAkC,EAAE,WAAW,GAAG;AACxD,MAAI,MAAM,WAAW,QAAW;AAC9B,WAAO,SAAS,MAAM;AACtB,QAAI,MAAM,WAAW,eAAe,MAAM,WAAW,UAAU;AAC7D,aAAO,cAAc;AAAA,IACvB;AAAA,EACF;AACA,MAAI,MAAM,WAAW,OAAW,QAAO,SAAS,MAAM;AACtD,MAAI,MAAM,UAAU,OAAW,QAAO,QAAQ,MAAM;AAEpD,QAAM,SAAS,cAAc,QAAQ,IAAI,MAAM;AACjD;;;AC9EO,SAAS,aAAa,KAAkC;AAG7D,MAAI,QAAQ,IAAI,aAAa,aAAc,QAAO,eAAe;AAEjE,QAAM,eACJ,IAAI,sBACJ,QAAQ,IAAI,wBACZ,QAAQ,IAAI;AAId,MAAI,CAAC,cAAc;AACjB,YAAQ;AAAA,MACN;AAAA,IACF;AACA,WAAO,eAAe;AAAA,EACxB;AAEA,MAAI,iBAA+E;AACnF,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,gBAAgB;AACnB,uBAAiB,oBAAoB,cAAc,IAAI,SAAS,IAAI,WAAW;AAAA,IACjF;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,MAAM,MAAM,MAAM,OAAO;AACvB,YAAM,EAAE,WAAW,cAAc,IAAI,MAAM,QAAQ;AACnD,aAAO,YAAY,cAAc,EAAE,WAAW,eAAe,MAAM,MAAM,CAAC;AAAA,IAC5E;AAAA,IACA,MAAM,SAAS,IAAI,QAAQ;AACzB,YAAM,WAAW,cAAc,IAAI,EAAE,QAAQ,aAAa,OAAO,CAAC;AAAA,IACpE;AAAA,IACA,MAAM,KAAK,IAAI,OAAO,QAAQ;AAC5B,YAAM,WAAW,cAAc,IAAI,EAAE,QAAQ,UAAU,OAAO,OAAO,CAAC;AAAA,IACxE;AAAA,EACF;AACF;AAEA,SAAS,iBAAgC;AACvC,QAAM,KAAK,KAAK,IAAI;AACpB,SAAO;AAAA,IACL,MAAM,MAAM,MAAM,OAAO;AACvB,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,WAAW;AAAA,QACX,eAAe;AAAA,QACf;AAAA,QACA,QAAQ;AAAA,QACR;AAAA,QACA,QAAQ;AAAA,QACR,OAAO;AAAA,QACP,WAAW;AAAA,QACX,WAAW;AAAA,QACX,aAAa;AAAA,MACf;AAAA,IACF;AAAA,IACA,MAAM,WAAW;AAAA,IAAC;AAAA,IAClB,MAAM,OAAO;AAAA,IAAC;AAAA,EAChB;AACF;;;ACjDO,SAAS,qBAAqB,KAAuC;AAC1E,QAAM,SAAS,IAAI,UAAU;AAC7B,SAAO;AAAA,IACL,MAAM,SAAS,SAAS,SAAS;AAC/B,YAAM,MAAM,MAAM,MAAM,GAAG,IAAI,OAAO,GAAG,MAAM,IAAI,OAAO,IAAI;AAAA,QAC5D,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,oBAAoB,GAAI,IAAI,WAAW,CAAC,EAAG;AAAA,QACtE,MAAM,KAAK,UAAU,OAAO;AAAA,MAC9B,CAAC;AACD,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,EAAE;AAC5C,cAAM,IAAI;AAAA,UACR,sBAAsB,OAAO,aAAa,IAAI,MAAM,IAAI,IAAI,GAAG,KAAK;AAAA,QACtE;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAOO,SAAS,2BACd,KACY;AACZ,QAAM,SAAS,IAAI,UAAU;AAC7B,QAAM,WAAW,IAAI,YAAY,IAAI;AACrC,QAAM,MAAM,iDAAiD,IAAI,SAAS,cAAc,IAAI,QAAQ,WAAW,IAAI,KAAK;AAExH,SAAO;AAAA,IACL,MAAM,SAAS,SAAS,SAAS;AAC/B,YAAM,QAAQ,MAAM,eAAe;AACnC,YAAM,OAAO;AAAA,QACX,MAAM;AAAA,UACJ,aAAa;AAAA,YACX,YAAY;AAAA,YACZ,KAAK,GAAG,IAAI,SAAS,GAAG,MAAM,IAAI,OAAO;AAAA,YACzC,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,YAC9C,MAAM,OAAO,KAAK,KAAK,UAAU,OAAO,CAAC,EAAE,SAAS,QAAQ;AAAA,YAC5D,WAAW;AAAA,cACT,qBAAqB,IAAI;AAAA,cACzB;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AACA,YAAM,MAAM,MAAM,MAAM,KAAK;AAAA,QAC3B,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,eAAe,UAAU,KAAK;AAAA,UAC9B,gBAAgB;AAAA,QAClB;AAAA,QACA,MAAM,KAAK,UAAU,IAAI;AAAA,MAC3B,CAAC;AACD,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,WAAW,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,EAAE;AAChD,cAAM,IAAI;AAAA,UACR,iCAAiC,OAAO,aAAa,IAAI,MAAM,IAAI,QAAQ,GAAG,KAAK;AAAA,QACrF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAI,SAA4B;AAOzB,SAAS,gBAA4B;AAC1C,MAAI,CAAC,OAAQ,UAAS,qBAAqB;AAC3C,SAAO;AACT;AAMA,SAAS,eACP,MACwD;AACxD,QAAM,IAAI,KAAK;AAAA,IACb;AAAA,EACF;AACA,MAAI,CAAC,GAAG;AACN,UAAM,IAAI;AAAA,MACR,oIAAoI,IAAI;AAAA,IAC1I;AAAA,EACF;AACA,SAAO,EAAE,WAAW,EAAE,CAAC,GAAI,UAAU,EAAE,CAAC,GAAI,OAAO,EAAE,CAAC,EAAG;AAC3D;AAEA,SAAS,uBAAmC;AAC1C,MAAI,QAAQ,IAAI,aAAa,cAAc;AACzC,UAAM,WAAW;AAAA,MACf,sBAAsB,QAAQ,IAAI;AAAA,MAClC,qBAAqB,QAAQ,IAAI;AAAA,MACjC,yBAAyB,QAAQ,IAAI;AAAA,IACvC;AACA,UAAM,UAAU,OAAO,QAAQ,QAAQ,EACpC,OAAO,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,EACpB,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC;AACjB,QAAI,QAAQ,QAAQ;AAClB,YAAM,IAAI;AAAA,QACR,2EAA2E,QAAQ,KAAK,IAAI,CAAC;AAAA,MAC/F;AAAA,IACF;AACA,UAAM,EAAE,WAAW,UAAU,MAAM,IAAI;AAAA,MACrC,SAAS;AAAA,IACX;AACA,WAAO,2BAA2B;AAAA,MAChC;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW,SAAS;AAAA,MACpB,qBAAqB,SAAS;AAAA,IAChC,CAAC;AAAA,EACH;AAEA,QAAMC,WAAU,QAAQ,IAAI,uBAAuB;AACnD,SAAO,qBAAqB,EAAE,SAAAA,SAAQ,CAAC;AACzC;;;ACxGO,SAAS,UAAiB,KAAuC;AACtE,SAAO;AAAA,IACL,GAAG;AAAA,IACH,MAAM,CAAC,UAAU,cAAc,EAAE,SAAS,IAAI,MAAM,KAAK;AAAA,EAC3D;AACF;;;ACnDA,kBAAqB;AACrB,yBAAsB;;;ACCtB,IAAMC,QAAO;AAgBb,SAASC,gBAAe,MAA2B;AACjD,QAAM,IAAI,KAAK;AAAA,IACb;AAAA,EACF;AACA,MAAI,CAAC,GAAG;AACN,UAAM,IAAI;AAAA,MACR,oIAAoI,IAAI;AAAA,IAC1I;AAAA,EACF;AACA,SAAO,EAAE,WAAW,EAAE,CAAC,GAAI,UAAU,EAAE,CAAC,GAAI,OAAO,EAAE,CAAC,GAAI,UAAU,KAAK;AAC3E;AAEA,SAAS,aAAa,MAAsB;AAC1C,SAAO,KAAK,QAAQ,kBAAkB,GAAG,EAAE,QAAQ,YAAY,EAAE;AACnE;AAEA,eAAeC,QAAO,KAAa,MAAuC;AACxE,QAAM,QAAQ,MAAM,eAAe;AACnC,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B,GAAG;AAAA,IACH,SAAS;AAAA,MACP,GAAI,MAAM,WAAW,CAAC;AAAA,MACtB,eAAe,UAAU,KAAK;AAAA,MAC9B,gBAAgB;AAAA,IAClB;AAAA,EACF,CAAC;AACD,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,EAAE;AAC5C,UAAM,IAAI,MAAM,mBAAmB,IAAI,MAAM,KAAK,IAAI,EAAE;AAAA,EAC1D;AACA,SAAO;AACT;AAmBA,SAAS,kBAAkB,MASV;AACf,QAAM,OAAO;AAAA,IACX,aAAa;AAAA,MACX,KAAK,GAAG,KAAK,SAAS,SAAS,KAAK,OAAO;AAAA,MAC3C,YAAY;AAAA,MACZ,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,OAAO,KAAK,IAAI,EAAE,SAAS,QAAQ;AAAA,MACzC,WAAW;AAAA,QACT,qBAAqB,KAAK;AAAA,QAC1B,UAAU,KAAK;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM,KAAK;AAAA,IACX,UAAU,KAAK;AAAA,IACf,UAAU,KAAK;AAAA,IACf,YAAY;AAAA,MACV,KAAK,wCAAwC,KAAK,MAAM,QAAQ;AAAA,MAChE,YAAY;AAAA,MACZ,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,OAAO,KAAK,KAAK,UAAU,EAAE,KAAK,CAAC,CAAC,EAAE,SAAS,QAAQ;AAAA,MAC7D,YAAY;AAAA,QACV,qBAAqB,KAAK;AAAA,QAC1B,OAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA,QAAQ;AAAA,MACN,cAAc;AAAA,MACd,KAAK,KAAK;AAAA,MACV,KAAK,KAAK;AAAA,IACZ;AAAA,EACF;AACF;AAEA,eAAe,YACb,OACA,UACyB;AACzB,QAAM,SAAS,gDAAgD,QAAQ;AACvE,QAAM,MAAM,GAAGF,KAAI,aAAa,MAAM,SAAS,cAAc,MAAM,QAAQ,gBAAgB,mBAAmB,MAAM,CAAC;AACrH,QAAM,MAAM,MAAME,QAAO,GAAG;AAC5B,QAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,SAAO,KAAK,QAAQ,CAAC;AACvB;AAEA,SAAS,QAAQ,GAAiB,GAA0B;AAC1D,SACE,EAAE,aAAa,EAAE,YACjB,EAAE,aAAa,EAAE,YACjB,EAAE,WAAW,QAAQ,EAAE,WAAW,OAClC,EAAE,WAAW,SAAS,EAAE,WAAW;AAEvC;AAaA,eAAsB,cAEpB,MAAgC;AAChC,QAAM,MAAM;AAAA,IACV,sBAAsB,QAAQ,IAAI;AAAA,IAClC,qBAAqB,QAAQ,IAAI;AAAA,IACjC,yBAAyB,QAAQ,IAAI;AAAA,IACrC,oBAAoB,QAAQ,IAAI;AAAA,EAClC;AACA,QAAM,UAAU,OAAO,QAAQ,GAAG,EAC/B,OAAO,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,EACpB,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC;AACjB,MAAI,QAAQ,QAAQ;AAClB,UAAM,IAAI;AAAA,MACR,kEAAkE,QAAQ,KAAK,IAAI,CAAC;AAAA,IACtF;AAAA,EACF;AAEA,QAAM,QAAQD,gBAAe,IAAI,oBAAqB;AACtD,QAAM,YAAY,IAAI;AACtB,QAAM,YAAY,IAAI;AACtB,QAAM,WAAW,IAAI;AAErB,QAAM,YAAY,KAAK,OAAO,CAAC,MAAM,QAAQ,EAAE,QAAQ,CAAC;AAExD,QAAM,UAAU,oBAAI,IAA0B;AAC9C,aAAW,OAAO,WAAW;AAC3B,UAAM,gBAAgB,aAAa,GAAG,QAAQ,IAAI,IAAI,IAAI,EAAE;AAC5D,UAAM,WAAW,YAAY,MAAM,SAAS,cAAc,MAAM,QAAQ,SAAS,aAAa;AAC9F,YAAQ;AAAA,MACN;AAAA,MACA,kBAAkB;AAAA,QAChB;AAAA,QACA,MAAM,IAAI;AAAA,QACV,UAAU,IAAI,YAAY;AAAA,QAC1B;AAAA,QACA;AAAA,QACA;AAAA,QACA,SAAS,IAAI;AAAA,QACb;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAEA,QAAM,WAAW,MAAM,YAAY,OAAO,QAAQ;AAClD,QAAM,iBAAiB,IAAI,IAAI,SAAS,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AAE/D,QAAM,SAAqB;AAAA,IACzB,SAAS,CAAC;AAAA,IACV,SAAS,CAAC;AAAA,IACV,SAAS,CAAC;AAAA,IACV,WAAW,CAAC;AAAA,EACd;AAEA,aAAW,CAAC,MAAM,IAAI,KAAK,SAAS;AAClC,UAAM,OAAO,eAAe,IAAI,IAAI;AACpC,QAAI,CAAC,MAAM;AACT,YAAM,MAAM,GAAGD,KAAI,aAAa,MAAM,SAAS,cAAc,MAAM,QAAQ;AAC3E,YAAME,QAAO,KAAK,EAAE,QAAQ,QAAQ,MAAM,KAAK,UAAU,IAAI,EAAE,CAAC;AAChE,aAAO,QAAQ,KAAK,IAAI;AAAA,IAC1B,WAAW,QAAQ,MAAM,IAAI,GAAG;AAC9B,YAAM,MAAM,GAAGF,KAAI,IAAI,IAAI;AAC3B,YAAME,QAAO,KAAK,EAAE,QAAQ,SAAS,MAAM,KAAK,UAAU,IAAI,EAAE,CAAC;AACjE,aAAO,QAAQ,KAAK,IAAI;AAAA,IAC1B,OAAO;AACL,aAAO,UAAU,KAAK,IAAI;AAAA,IAC5B;AAAA,EACF;AAEA,aAAW,QAAQ,eAAe,KAAK,GAAG;AACxC,QAAI,CAAC,QAAQ,IAAI,IAAI,GAAG;AACtB,YAAMA,QAAO,GAAGF,KAAI,IAAI,IAAI,IAAI,EAAE,QAAQ,SAAS,CAAC;AACpD,aAAO,QAAQ,KAAK,IAAI;AAAA,IAC1B;AAAA,EACF;AAEA,SAAO;AACT;;;AD5MO,SAAS,aAAa,KAAyB;AACpD,QAAM,OAAO,aAAa,GAAG;AAC7B,QAAM,SAAS,IAAI,UAAU;AAC7B,QAAM,MAAM,IAAI,iBAAK;AAErB,MAAI,IAAI,YAAY,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC;AAEvC,aAAW,OAAO,IAAI,MAAM;AAC1B,QAAI,KAAK,GAAG,MAAM,IAAI,IAAI,IAAI,IAAI,OAAO,MAAM;AAC7C,YAAM,QAAS,MAAM,EAAE,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AAClD,YAAM,QAAQ,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK;AAC9C,UAAI;AACF,cAAM,SAAS,MAAM,IAAI,QAAQ,OAAc,EAAE,SAAS,MAAM,IAAI,KAAK,CAAC;AAC1E,cAAM,KAAK,SAAS,MAAM,IAAI,UAAU,MAAS;AACjD,eAAO,EAAE,KAAK,EAAE,IAAI,MAAM,SAAS,MAAM,IAAI,QAAQ,UAAU,KAAK,CAAC;AAAA,MACvE,SAAS,KAAK;AACZ,cAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,cAAM,KAAK,KAAK,MAAM,IAAI,OAAO;AACjC,eAAO,EAAE,KAAK,EAAE,IAAI,OAAO,SAAS,MAAM,IAAI,OAAO,QAAQ,GAAG,GAAG;AAAA,MACrE;AAAA,IACF,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAkBO,SAAS,2BAEd,MAAoB;AACpB,MAAI,QAAQ,IAAI,uBAAuB,IAAK,QAAO;AAEnD,gBAAc,IAAI,EACf,KAAK,CAAC,WAAW;AAChB,YAAQ;AAAA,MACN,yCAAyC,KAAK,UAAU,QAAQ,MAAM,CAAC;AAAA,IACzE;AACA,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC,EACA,MAAM,CAAC,QAAQ;AACd,YAAQ,MAAM,oCAAoC,GAAG;AACrD,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AAEH,SAAO;AACT;AAOO,SAAS,YACd,KACiC;AACjC,MAAI,QAAQ,IAAI,uBAAuB,KAAK;AAC1C,+BAA2B,IAAI,IAAI;AACnC;AAAA,EACF;AAEA,QAAM,MAAM,aAAa,GAAG;AAC5B,QAAM,OAAO,IAAI,QAAQ,OAAO,QAAQ,IAAI,QAAQ,IAAI;AACxD,UAAQ,IAAI,mCAAmC,IAAI,EAAE;AACrD,aAAO,0BAAM,EAAE,OAAO,IAAI,OAAO,KAAK,CAAC;AACzC;","names":["now","baseUrl","BASE","parseQueuePath","authed"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/auth.ts","../src/firestore-rest.ts","../src/store.ts","../src/client.ts","../src/dispatcher.ts","../src/job.ts","../src/worker.ts","../src/sync.ts"],"sourcesContent":["export { createClient } from './client'\nexport type { JobEchoClient } from './client'\nexport { defineJob } from './job'\nexport type { Job, JobDefinition, JobContext, JobInput } from './job'\nexport {\n createWorker,\n runScheduleSyncIfRequested,\n startWorker,\n} from './worker'\nexport type { WorkerConfig } from './worker'\nexport { syncSchedules } from './sync'\nexport type { SyncReport } from './sync'\nexport type { ClientConfig, JobEvent, JobStatus } from './types'\n","const METADATA_URL =\n 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token'\n\ninterface TokenResponse {\n access_token: string\n expires_in: number\n token_type: string\n}\n\nlet cached: { token: string; expiresAt: number } | null = null\n\n/**\n * Fetch an OAuth2 access token from GCP's metadata server. The token is\n * cached in-process until 60s before expiry.\n *\n * Only works on GCP compute (Cloud Run, GCE, GKE, Cloud Functions, etc.).\n * For non-GCP environments the package's production code path is not\n * expected to run — dev mode short-circuits before this is ever called.\n */\nexport async function getAccessToken(): Promise<string> {\n const now = Date.now()\n if (cached && cached.expiresAt > now + 60_000) return cached.token\n\n const res = await fetch(METADATA_URL, { headers: { 'Metadata-Flavor': 'Google' } })\n if (!res.ok) {\n throw new Error(\n `@originalvoices/job-echo-client: failed to fetch GCP access token (status ${res.status}). Is this running on GCP compute?`,\n )\n }\n const body = (await res.json()) as TokenResponse\n cached = {\n token: body.access_token,\n expiresAt: now + body.expires_in * 1000,\n }\n return cached.token\n}\n","import { getAccessToken } from './auth'\n\n/**\n * Minimal Firestore REST client — just enough to create/patch/query documents.\n * Uses the metadata-server access token; no SDK, no native deps.\n *\n * Docs: https://firestore.googleapis.com/$discovery/rest?version=v1\n */\n\nconst BASE = 'https://firestore.googleapis.com/v1'\n\nfunction baseUrl(projectId: string) {\n return `${BASE}/projects/${projectId}/databases/(default)/documents`\n}\n\nasync function authed(url: string, init?: RequestInit): Promise<Response> {\n const token = await getAccessToken()\n const res = await fetch(url, {\n ...init,\n headers: {\n ...(init?.headers ?? {}),\n authorization: `Bearer ${token}`,\n 'content-type': 'application/json',\n },\n })\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new Error(`Firestore REST ${res.status}: ${body}`)\n }\n return res\n}\n\n// ── Typed value encoding / decoding ───────────────────────────────────────────\n\ntype FsValue =\n | { stringValue: string }\n | { integerValue: string }\n | { doubleValue: number }\n | { booleanValue: boolean }\n | { nullValue: null }\n | { timestampValue: string }\n | { mapValue: { fields: Record<string, FsValue> } }\n | { arrayValue: { values: FsValue[] } }\n\nexport function encodeValue(v: unknown): FsValue {\n if (v === null || v === undefined) return { nullValue: null }\n if (typeof v === 'string') return { stringValue: v }\n if (typeof v === 'boolean') return { booleanValue: v }\n if (typeof v === 'number') {\n return Number.isInteger(v)\n ? { integerValue: String(v) }\n : { doubleValue: v }\n }\n if (Array.isArray(v)) {\n return { arrayValue: { values: v.map(encodeValue) } }\n }\n if (typeof v === 'object') {\n return { mapValue: { fields: encodeFields(v as Record<string, unknown>) } }\n }\n // Fallback — coerce to string\n return { stringValue: String(v) }\n}\n\nexport function encodeFields(obj: Record<string, unknown>): Record<string, FsValue> {\n const out: Record<string, FsValue> = {}\n for (const [k, v] of Object.entries(obj)) out[k] = encodeValue(v)\n return out\n}\n\nexport function decodeValue(v: FsValue): unknown {\n if ('stringValue' in v) return v.stringValue\n if ('integerValue' in v) return Number(v.integerValue)\n if ('doubleValue' in v) return v.doubleValue\n if ('booleanValue' in v) return v.booleanValue\n if ('nullValue' in v) return null\n if ('timestampValue' in v) return v.timestampValue\n if ('mapValue' in v) return decodeFields(v.mapValue.fields ?? {})\n if ('arrayValue' in v) return (v.arrayValue.values ?? []).map(decodeValue)\n return null\n}\n\nexport function decodeFields(\n fields: Record<string, FsValue>,\n): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(fields)) out[k] = decodeValue(v)\n return out\n}\n\n// ── Public operations ─────────────────────────────────────────────────────────\n\nfunction docIdFromName(name: string): string {\n // name looks like \"projects/xxx/databases/(default)/documents/collection/docId\"\n return name.split('/').pop() ?? name\n}\n\nexport interface FirestoreDoc {\n id: string\n data: Record<string, unknown>\n}\n\nexport async function createDoc(\n projectId: string,\n collection: string,\n data: Record<string, unknown>,\n): Promise<FirestoreDoc> {\n const res = await authed(`${baseUrl(projectId)}/${collection}`, {\n method: 'POST',\n body: JSON.stringify({ fields: encodeFields(data) }),\n })\n const body = (await res.json()) as { name: string; fields?: Record<string, FsValue> }\n return { id: docIdFromName(body.name), data: decodeFields(body.fields ?? {}) }\n}\n\nexport async function patchDoc(\n projectId: string,\n collection: string,\n id: string,\n patch: Record<string, unknown>,\n): Promise<void> {\n const params = Object.keys(patch)\n .map((k) => `updateMask.fieldPaths=${encodeURIComponent(k)}`)\n .join('&')\n await authed(`${baseUrl(projectId)}/${collection}/${id}?${params}`, {\n method: 'PATCH',\n body: JSON.stringify({ fields: encodeFields(patch) }),\n })\n}\n\n/**\n * Run a simple equality-filter query against a single collection.\n * Returns at most `limit` docs.\n */\nexport async function queryDocs(\n projectId: string,\n collection: string,\n filters: Array<{ field: string; value: string }>,\n limit = 1,\n): Promise<FirestoreDoc[]> {\n const compositeFilter = {\n compositeFilter: {\n op: 'AND',\n filters: filters.map((f) => ({\n fieldFilter: {\n field: { fieldPath: f.field },\n op: 'EQUAL',\n value: { stringValue: f.value },\n },\n })),\n },\n }\n\n const structuredQuery = {\n from: [{ collectionId: collection }],\n where: filters.length ? compositeFilter : undefined,\n limit,\n }\n\n const res = await authed(`${baseUrl(projectId)}:runQuery`, {\n method: 'POST',\n body: JSON.stringify({ structuredQuery }),\n })\n const rows = (await res.json()) as Array<{\n document?: { name: string; fields?: Record<string, FsValue> }\n }>\n return rows\n .filter((r) => r.document)\n .map((r) => ({\n id: docIdFromName(r.document!.name),\n data: decodeFields(r.document!.fields ?? {}),\n }))\n}\n","import { createDoc, patchDoc, queryDocs } from './firestore-rest'\nimport type { JobEvent, JobStatus } from './types'\n\nconst PROJECTS = 'projects'\nconst ENVIRONMENTS = 'environments'\nconst EVENTS = 'events'\n\nconst now = () => Date.now()\n\nexport async function findOrCreateContext(\n gcpProjectId: string,\n projectName: string,\n envName: string,\n): Promise<{ projectId: string; environmentId: string }> {\n const projMatches = await queryDocs(\n gcpProjectId,\n PROJECTS,\n [{ field: 'name', value: projectName }],\n 1,\n )\n const projectId = projMatches[0]\n ? projMatches[0].id\n : (await createDoc(gcpProjectId, PROJECTS, { name: projectName, createdAt: now() })).id\n\n const envMatches = await queryDocs(\n gcpProjectId,\n ENVIRONMENTS,\n [\n { field: 'projectId', value: projectId },\n { field: 'name', value: envName },\n ],\n 1,\n )\n const environmentId = envMatches[0]\n ? envMatches[0].id\n : (\n await createDoc(gcpProjectId, ENVIRONMENTS, {\n projectId,\n name: envName,\n createdAt: now(),\n })\n ).id\n\n return { projectId, environmentId }\n}\n\nexport async function createEvent(\n gcpProjectId: string,\n args: {\n projectId: string\n environmentId: string\n name: string\n input?: unknown\n status?: JobStatus\n },\n): Promise<JobEvent> {\n const ts = now()\n const data: Omit<JobEvent, 'id'> = {\n projectId: args.projectId,\n environmentId: args.environmentId,\n name: args.name,\n status: args.status ?? 'in_progress',\n input: args.input ?? null,\n output: null,\n error: null,\n createdAt: ts,\n updatedAt: ts,\n completedAt: null,\n }\n const doc = await createDoc(gcpProjectId, EVENTS, data)\n return { id: doc.id, ...data }\n}\n\nexport async function patchEvent(\n gcpProjectId: string,\n id: string,\n patch: { status?: JobStatus; output?: unknown; error?: string | null },\n): Promise<void> {\n const ts = now()\n const update: Record<string, unknown> = { updatedAt: ts }\n if (patch.status !== undefined) {\n update.status = patch.status\n if (patch.status === 'completed' || patch.status === 'failed') {\n update.completedAt = ts\n }\n }\n if (patch.output !== undefined) update.output = patch.output\n if (patch.error !== undefined) update.error = patch.error\n\n await patchDoc(gcpProjectId, EVENTS, id, update)\n}\n","import { createEvent, findOrCreateContext, patchEvent } from './store'\nimport type { ClientConfig, JobEvent } from './types'\n\nexport interface JobEchoClient {\n /** Record a new job event as `in_progress`. Returns the created event. */\n start(name: string, input?: unknown): Promise<JobEvent>\n /** Mark an existing event `completed`. Optionally attach a return value as `output`. */\n complete(id: string, output?: unknown): Promise<void>\n /** Mark an existing event `failed` with an error message. */\n fail(id: string, error: string, output?: unknown): Promise<void>\n}\n\nexport function createClient(cfg: ClientConfig): JobEchoClient {\n // Only write to Firestore in production. Local/dev runs are a no-op —\n // handlers still execute, just no lifecycle events recorded.\n if (process.env.NODE_ENV !== 'production') return disabledClient()\n\n const gcpProjectId =\n cfg.collectorProjectId ??\n process.env.JOB_ECHO_GCP_PROJECT ??\n process.env.FIREBASE_PROJECT_ID\n\n // No collector project configured → run as no-op. Lets services adopt the\n // worker pattern before the dashboard's Firestore is provisioned.\n if (!gcpProjectId) {\n console.warn(\n '[job-echo] no collectorProjectId / JOB_ECHO_GCP_PROJECT / FIREBASE_PROJECT_ID set — Firestore writes disabled',\n )\n return disabledClient()\n }\n\n let contextPromise: Promise<{ projectId: string; environmentId: string }> | null = null\n const context = () => {\n if (!contextPromise) {\n contextPromise = findOrCreateContext(gcpProjectId, cfg.project, cfg.environment)\n }\n return contextPromise\n }\n\n return {\n async start(name, input) {\n const { projectId, environmentId } = await context()\n return createEvent(gcpProjectId, { projectId, environmentId, name, input })\n },\n async complete(id, output) {\n await patchEvent(gcpProjectId, id, { status: 'completed', output })\n },\n async fail(id, error, output) {\n await patchEvent(gcpProjectId, id, { status: 'failed', error, output })\n },\n }\n}\n\nfunction disabledClient(): JobEchoClient {\n const ts = Date.now()\n return {\n async start(name, input) {\n return {\n id: 'disabled',\n projectId: 'disabled',\n environmentId: 'disabled',\n name,\n status: 'in_progress',\n input,\n output: null,\n error: null,\n createdAt: ts,\n updatedAt: ts,\n completedAt: null,\n }\n },\n async complete() {},\n async fail() {},\n }\n}\n","import { getAccessToken } from './auth'\n\nexport interface Dispatcher {\n dispatch(jobName: string, payload: unknown): Promise<void>\n}\n\nexport interface HttpDispatcherConfig {\n baseUrl: string\n headers?: Record<string, string>\n prefix?: string\n}\n\nexport interface CloudTasksDispatcherConfig {\n projectId: string\n location: string\n queue: string\n workerUrl: string\n serviceAccountEmail: string\n prefix?: string\n audience?: string\n}\n\n/**\n * HTTP dispatcher: plain POST to `{baseUrl}{prefix}/{jobName}`.\n */\nexport function createHttpDispatcher(cfg: HttpDispatcherConfig): Dispatcher {\n const prefix = cfg.prefix ?? '/jobs'\n return {\n async dispatch(jobName, payload) {\n const res = await fetch(`${cfg.baseUrl}${prefix}/${jobName}`, {\n method: 'POST',\n headers: { 'content-type': 'application/json', ...(cfg.headers ?? {}) },\n body: JSON.stringify(payload),\n })\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new Error(\n `job-echo dispatch '${jobName}' failed: ${res.status} ${body}`.trim(),\n )\n }\n },\n }\n}\n\n/**\n * Cloud Tasks dispatcher via the REST API — no @google-cloud/tasks SDK.\n * Enqueues an HTTP task with an OIDC token so the worker can verify\n * the caller identity.\n */\nexport function createCloudTasksDispatcher(\n cfg: CloudTasksDispatcherConfig,\n): Dispatcher {\n const prefix = cfg.prefix ?? '/jobs'\n const audience = cfg.audience ?? cfg.workerUrl\n const url = `https://cloudtasks.googleapis.com/v2/projects/${cfg.projectId}/locations/${cfg.location}/queues/${cfg.queue}/tasks`\n\n return {\n async dispatch(jobName, payload) {\n const token = await getAccessToken()\n const body = {\n task: {\n httpRequest: {\n httpMethod: 'POST',\n url: `${cfg.workerUrl}${prefix}/${jobName}`,\n headers: { 'content-type': 'application/json' },\n body: Buffer.from(JSON.stringify(payload)).toString('base64'),\n oidcToken: {\n serviceAccountEmail: cfg.serviceAccountEmail,\n audience,\n },\n },\n },\n }\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n authorization: `Bearer ${token}`,\n 'content-type': 'application/json',\n },\n body: JSON.stringify(body),\n })\n if (!res.ok) {\n const respBody = await res.text().catch(() => '')\n throw new Error(\n `job-echo Cloud Tasks enqueue '${jobName}' failed: ${res.status} ${respBody}`.trim(),\n )\n }\n },\n }\n}\n\nlet active: Dispatcher | null = null\n\n/**\n * Returns the dispatcher, auto-detected from env on first call:\n * - `NODE_ENV=production`: Cloud Tasks (requires `JOB_ECHO_TASKS_*` env vars)\n * - otherwise: HTTP to `JOB_ECHO_WORKER_URL` (default `http://localhost:8080`)\n */\nexport function getDispatcher(): Dispatcher {\n if (!active) active = autoDetectDispatcher()\n return active\n}\n\n/**\n * Parse a Cloud Tasks queue resource path:\n * `projects/{project}/locations/{location}/queues/{queue}`\n */\nfunction parseQueuePath(\n path: string,\n): { projectId: string; location: string; queue: string } {\n const m = path.match(\n /^projects\\/([^/]+)\\/locations\\/([^/]+)\\/queues\\/([^/]+)$/,\n )\n if (!m) {\n throw new Error(\n `@originalvoices/job-echo-client: JOB_ECHO_TASKS_QUEUE must be a full resource path (projects/{p}/locations/{l}/queues/{q}), got \"${path}\"`,\n )\n }\n return { projectId: m[1]!, location: m[2]!, queue: m[3]! }\n}\n\nfunction autoDetectDispatcher(): Dispatcher {\n if (process.env.NODE_ENV === 'production') {\n const required = {\n JOB_ECHO_TASKS_QUEUE: process.env.JOB_ECHO_TASKS_QUEUE,\n JOB_ECHO_WORKER_URL: process.env.JOB_ECHO_WORKER_URL,\n JOB_ECHO_TASKS_SA_EMAIL: process.env.JOB_ECHO_TASKS_SA_EMAIL,\n }\n const missing = Object.entries(required)\n .filter(([, v]) => !v)\n .map(([k]) => k)\n if (missing.length) {\n throw new Error(\n `@originalvoices/job-echo-client: Cloud Tasks dispatcher needs env vars: ${missing.join(', ')}`,\n )\n }\n const { projectId, location, queue } = parseQueuePath(\n required.JOB_ECHO_TASKS_QUEUE!,\n )\n return createCloudTasksDispatcher({\n projectId,\n location,\n queue,\n workerUrl: required.JOB_ECHO_WORKER_URL!,\n serviceAccountEmail: required.JOB_ECHO_TASKS_SA_EMAIL!,\n })\n }\n\n const baseUrl = process.env.JOB_ECHO_WORKER_URL ?? 'http://localhost:8080'\n return createHttpDispatcher({ baseUrl })\n}\n","import { getDispatcher } from './dispatcher'\nimport type { JobEchoClient } from './client'\n\nexport interface JobContext {\n /** Firestore event id for this invocation (status=in_progress). */\n eventId: string\n /** The raw job-echo client — use for nested events or custom updates. */\n echo: JobEchoClient\n}\n\nexport interface JobDefinition<Input> {\n name: string\n handler: (input: Input, ctx: JobContext) => Promise<unknown>\n /**\n * Optional cron expression. When set, `syncSchedules` will provision a\n * Cloud Scheduler job that fires this handler on the given cadence.\n */\n schedule?: string\n /** IANA timezone for `schedule`. Defaults to \"Etc/UTC\". */\n timezone?: string\n}\n\nexport interface Job<Input> extends JobDefinition<Input> {\n /**\n * Send `input` to the configured dispatcher (defaults to HTTP POST to the\n * worker). Requires `configureDispatcher({ baseUrl })` at startup.\n */\n emit(input: Input): Promise<void>\n /** Type marker — not present at runtime. */\n _input?: Input\n}\n\n/**\n * Define a typed job. Returns an object with a bound `.emit(input)` so call\n * sites don't have to know about the worker URL or shape of the HTTP call.\n *\n * ```ts\n * export const sendMessage = defineJob({\n * name: 'send-message',\n * async handler(input: { threadId: string }) { ... },\n * })\n *\n * // elsewhere, fully typed:\n * await sendMessage.emit({ threadId: 't1' })\n * ```\n */\nexport function defineJob<Input>(def: JobDefinition<Input>): Job<Input> {\n return {\n ...def,\n emit: (input) => getDispatcher().dispatch(def.name, input),\n }\n}\n\n/** Extract the input type from a Job. */\nexport type JobInput<J> = J extends Job<infer I> ? I : never\n","import { Hono } from 'hono'\nimport { serve } from '@hono/node-server'\nimport { createClient, type JobEchoClient } from './client'\nimport type { Job } from './job'\nimport { syncSchedules } from './sync'\nimport type { ClientConfig } from './types'\n\nexport interface WorkerConfig extends ClientConfig {\n jobs: Job<any>[]\n /** Route prefix for jobs. Defaults to \"/jobs\". */\n prefix?: string\n /**\n * If set, require requests to `/jobs/*` to include this exact value in\n * the `X-Worker-Secret` header. Returns 401 otherwise. In production,\n * leave unset and let Cloud Run IAM (run.invoker) gate access; this is\n * only useful for local dev or non-IAM-gated deployments.\n */\n workerSecret?: string\n /** Disable request logging on `/jobs/*`. Default: enabled. */\n log?: false\n}\n\n/**\n * Build a Hono app that:\n * - Exposes GET `/health` + `/healthz` (no auth, no logging)\n * - Optionally requires `X-Worker-Secret` on `/jobs/*` (when `workerSecret` set)\n * - Logs requests to `/jobs/*` with duration (unless `log: false`)\n * - Exposes POST `/jobs/{name}` for each job, wrapping handlers with\n * job-echo lifecycle tracking (in_progress → completed / failed)\n */\nexport function createWorker(cfg: WorkerConfig): Hono {\n const echo = createClient(cfg)\n const prefix = cfg.prefix ?? '/jobs'\n const app = new Hono()\n\n app.onError((err, c) => {\n console.error(`[job-echo] ${c.req.method} ${c.req.path} error:`, err)\n return c.json({ error: err instanceof Error ? err.message : String(err) }, 500)\n })\n\n // Health checks (before any middleware so they're always reachable)\n app.get('/health', (c) => c.text('ok'))\n app.get('/healthz', (c) => c.text('ok'))\n\n // Request logging (before auth so unauthorized hits are visible too)\n if (cfg.log !== false) {\n app.use(`${prefix}/*`, async (c, next) => {\n const start = Date.now()\n console.log(`[job-echo] --> ${c.req.method} ${c.req.path}`)\n await next()\n console.log(\n `[job-echo] <-- ${c.req.method} ${c.req.path} ${c.res.status} (${Date.now() - start}ms)`,\n )\n })\n }\n\n // Optional shared-secret auth\n if (cfg.workerSecret) {\n const secret = cfg.workerSecret\n app.use(`${prefix}/*`, async (c, next) => {\n if (c.req.header('X-Worker-Secret') !== secret) {\n return c.json({ error: 'unauthorized' }, 401)\n }\n return next()\n })\n }\n\n for (const job of cfg.jobs) {\n app.post(`${prefix}/${job.name}`, async (c) => {\n const input = (await c.req.json().catch(() => ({}))) as unknown\n const event = await echo.start(job.name, input)\n try {\n const result = await job.handler(input as any, { eventId: event.id, echo })\n await echo.complete(event.id, result ?? undefined)\n return c.json({ ok: true, eventId: event.id, result: result ?? null })\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n await echo.fail(event.id, message)\n return c.json({ ok: false, eventId: event.id, error: message }, 500)\n }\n })\n }\n\n return app\n}\n\n/**\n * If `JOB_ECHO_SYNC_MODE=1` is set, fire schedule sync (which will\n * `process.exit(0)` on success / `1` on failure) and return `true` so\n * the caller knows to skip normal startup. Otherwise returns `false`.\n *\n * Synchronous return so it composes naturally in CJS entrypoints\n * (no top-level `await` needed).\n *\n * ```ts\n * if (runScheduleSyncIfRequested(allJobs)) {\n * // sync running; don't start server\n * } else {\n * startServer()\n * }\n * ```\n */\nexport function runScheduleSyncIfRequested<\n J extends { name: string; schedule?: string; timezone?: string },\n>(jobs: J[]): boolean {\n if (process.env.JOB_ECHO_SYNC_MODE !== '1') return false\n\n syncSchedules(jobs)\n .then((report) => {\n console.log(\n '[job-echo] schedule sync complete:\\n' + JSON.stringify(report, null, 2),\n )\n process.exit(0)\n })\n .catch((err) => {\n console.error('[job-echo] schedule sync failed:', err)\n process.exit(1)\n })\n\n return true\n}\n\n/**\n * Convenience: build a worker and start the HTTP server on `port`\n * (defaults to `PORT` env or 8080). Also handles `JOB_ECHO_SYNC_MODE=1`\n * via `runScheduleSyncIfRequested` — same binary, two modes.\n */\nexport function startWorker(\n cfg: WorkerConfig & { port?: number },\n): ReturnType<typeof serve> | void {\n if (process.env.JOB_ECHO_SYNC_MODE === '1') {\n runScheduleSyncIfRequested(cfg.jobs)\n return\n }\n\n const app = createWorker(cfg)\n const port = cfg.port ?? Number(process.env.PORT ?? 8080)\n console.log(`[job-echo] worker listening on :${port}`)\n return serve({ fetch: app.fetch, port })\n}\n","import { getAccessToken } from './auth'\n\nconst BASE = 'https://cloudscheduler.googleapis.com/v1'\n\nexport interface SyncReport {\n created: string[]\n updated: string[]\n deleted: string[]\n unchanged: string[]\n}\n\ninterface ParsedQueue {\n projectId: string\n location: string\n queue: string\n fullPath: string\n}\n\nfunction parseQueuePath(path: string): ParsedQueue {\n const m = path.match(\n /^projects\\/([^/]+)\\/locations\\/([^/]+)\\/queues\\/([^/]+)$/,\n )\n if (!m) {\n throw new Error(\n `@originalvoices/job-echo-client: JOB_ECHO_TASKS_QUEUE must be a full resource path (projects/{p}/locations/{l}/queues/{q}), got \"${path}\"`,\n )\n }\n return { projectId: m[1]!, location: m[2]!, queue: m[3]!, fullPath: path }\n}\n\nfunction sanitizeName(name: string): string {\n return name.replace(/[^a-zA-Z0-9-]/g, '-').replace(/^-+|-+$/g, '')\n}\n\nasync function authed(url: string, init?: RequestInit): Promise<Response> {\n const token = await getAccessToken()\n const res = await fetch(url, {\n ...init,\n headers: {\n ...(init?.headers ?? {}),\n authorization: `Bearer ${token}`,\n 'content-type': 'application/json',\n },\n })\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new Error(`Cloud Scheduler ${res.status}: ${body}`)\n }\n return res\n}\n\ninterface SchedulerJob {\n name: string\n schedule: string\n timeZone: string\n httpTarget: {\n uri: string\n httpMethod: string\n headers?: Record<string, string>\n body?: string\n oauthToken?: {\n serviceAccountEmail: string\n scope?: string\n }\n }\n labels?: Record<string, string>\n}\n\nfunction buildSchedulerJob(args: {\n fullName: string\n cron: string\n timezone: string\n queue: ParsedQueue\n workerUrl: string\n invokerSa: string\n jobName: string\n appLabel: string\n}): SchedulerJob {\n const task = {\n httpRequest: {\n url: `${args.workerUrl}/jobs/${args.jobName}`,\n httpMethod: 'POST',\n headers: { 'content-type': 'application/json' },\n body: Buffer.from('{}').toString('base64'),\n oidcToken: {\n serviceAccountEmail: args.invokerSa,\n audience: args.workerUrl,\n },\n },\n }\n\n return {\n name: args.fullName,\n schedule: args.cron,\n timeZone: args.timezone,\n httpTarget: {\n uri: `https://cloudtasks.googleapis.com/v2/${args.queue.fullPath}/tasks`,\n httpMethod: 'POST',\n headers: { 'content-type': 'application/json' },\n body: Buffer.from(JSON.stringify({ task })).toString('base64'),\n oauthToken: {\n serviceAccountEmail: args.invokerSa,\n scope: 'https://www.googleapis.com/auth/cloud-platform',\n },\n },\n labels: {\n 'managed-by': 'job-echo',\n app: args.appLabel,\n job: args.jobName,\n },\n }\n}\n\nasync function listManaged(\n queue: ParsedQueue,\n appLabel: string,\n): Promise<SchedulerJob[]> {\n const filter = `labels.managed-by=\"job-echo\" AND labels.app=\"${appLabel}\"`\n const url = `${BASE}/projects/${queue.projectId}/locations/${queue.location}/jobs?filter=${encodeURIComponent(filter)}`\n const res = await authed(url)\n const body = (await res.json()) as { jobs?: SchedulerJob[] }\n return body.jobs ?? []\n}\n\nfunction differs(a: SchedulerJob, b: SchedulerJob): boolean {\n return (\n a.schedule !== b.schedule ||\n a.timeZone !== b.timeZone ||\n a.httpTarget.uri !== b.httpTarget.uri ||\n a.httpTarget.body !== b.httpTarget.body\n )\n}\n\n/**\n * Reconcile Cloud Scheduler jobs to match the schedules declared on the\n * given jobs. Only touches schedulers labelled `managed-by=job-echo` and\n * `app=<JOB_ECHO_APP_LABEL>` — manually created schedulers are untouched.\n *\n * Required env:\n * - JOB_ECHO_TASKS_QUEUE (full resource path: projects/.../queues/...)\n * - JOB_ECHO_WORKER_URL (https URL of the worker service)\n * - JOB_ECHO_TASKS_SA_EMAIL (SA the scheduler-fired tasks run as)\n * - JOB_ECHO_APP_LABEL (e.g. \"platform-prod\" — scopes the sync)\n */\nexport async function syncSchedules<\n J extends { name: string; schedule?: string; timezone?: string },\n>(jobs: J[]): Promise<SyncReport> {\n const env = {\n JOB_ECHO_TASKS_QUEUE: process.env.JOB_ECHO_TASKS_QUEUE,\n JOB_ECHO_WORKER_URL: process.env.JOB_ECHO_WORKER_URL,\n JOB_ECHO_TASKS_SA_EMAIL: process.env.JOB_ECHO_TASKS_SA_EMAIL,\n JOB_ECHO_APP_LABEL: process.env.JOB_ECHO_APP_LABEL,\n }\n const missing = Object.entries(env)\n .filter(([, v]) => !v)\n .map(([k]) => k)\n if (missing.length) {\n throw new Error(\n `@originalvoices/job-echo-client: syncSchedules needs env vars: ${missing.join(', ')}`,\n )\n }\n\n const queue = parseQueuePath(env.JOB_ECHO_TASKS_QUEUE!)\n const workerUrl = env.JOB_ECHO_WORKER_URL!\n const invokerSa = env.JOB_ECHO_TASKS_SA_EMAIL!\n const appLabel = env.JOB_ECHO_APP_LABEL!\n\n const scheduled = jobs.filter((j) => Boolean(j.schedule))\n\n const desired = new Map<string, SchedulerJob>()\n for (const job of scheduled) {\n const schedulerName = sanitizeName(`${appLabel}-${job.name}`)\n const fullName = `projects/${queue.projectId}/locations/${queue.location}/jobs/${schedulerName}`\n desired.set(\n fullName,\n buildSchedulerJob({\n fullName,\n cron: job.schedule!,\n timezone: job.timezone ?? 'Etc/UTC',\n queue,\n workerUrl,\n invokerSa,\n jobName: job.name,\n appLabel,\n }),\n )\n }\n\n const existing = await listManaged(queue, appLabel)\n const existingByName = new Map(existing.map((j) => [j.name, j]))\n\n const report: SyncReport = {\n created: [],\n updated: [],\n deleted: [],\n unchanged: [],\n }\n\n for (const [name, want] of desired) {\n const have = existingByName.get(name)\n if (!have) {\n const url = `${BASE}/projects/${queue.projectId}/locations/${queue.location}/jobs`\n await authed(url, { method: 'POST', body: JSON.stringify(want) })\n report.created.push(name)\n } else if (differs(have, want)) {\n const url = `${BASE}/${name}?updateMask=schedule,timeZone,httpTarget,labels`\n await authed(url, { method: 'PATCH', body: JSON.stringify(want) })\n report.updated.push(name)\n } else {\n report.unchanged.push(name)\n }\n }\n\n for (const name of existingByName.keys()) {\n if (!desired.has(name)) {\n await authed(`${BASE}/${name}`, { method: 'DELETE' })\n report.deleted.push(name)\n }\n }\n\n return report\n}\n"],"mappings":"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","names":["now","baseUrl","BASE","parseQueuePath","authed"]}
package/dist/index.d.cts CHANGED
@@ -85,12 +85,25 @@ type JobInput<J> = J extends Job<infer I> ? I : never;
85
85
 
86
86
  interface WorkerConfig extends ClientConfig {
87
87
  jobs: Job<any>[];
88
- /** Route prefix. Defaults to "/jobs". */
88
+ /** Route prefix for jobs. Defaults to "/jobs". */
89
89
  prefix?: string;
90
+ /**
91
+ * If set, require requests to `/jobs/*` to include this exact value in
92
+ * the `X-Worker-Secret` header. Returns 401 otherwise. In production,
93
+ * leave unset and let Cloud Run IAM (run.invoker) gate access; this is
94
+ * only useful for local dev or non-IAM-gated deployments.
95
+ */
96
+ workerSecret?: string;
97
+ /** Disable request logging on `/jobs/*`. Default: enabled. */
98
+ log?: false;
90
99
  }
91
100
  /**
92
- * Build a Hono app that exposes POST {prefix}/{jobName} for each job and
93
- * wraps each handler with job-echo lifecycle tracking.
101
+ * Build a Hono app that:
102
+ * - Exposes GET `/health` + `/healthz` (no auth, no logging)
103
+ * - Optionally requires `X-Worker-Secret` on `/jobs/*` (when `workerSecret` set)
104
+ * - Logs requests to `/jobs/*` with duration (unless `log: false`)
105
+ * - Exposes POST `/jobs/{name}` for each job, wrapping handlers with
106
+ * job-echo lifecycle tracking (in_progress → completed / failed)
94
107
  */
95
108
  declare function createWorker(cfg: WorkerConfig): Hono;
96
109
  /**
package/dist/index.d.ts CHANGED
@@ -85,12 +85,25 @@ type JobInput<J> = J extends Job<infer I> ? I : never;
85
85
 
86
86
  interface WorkerConfig extends ClientConfig {
87
87
  jobs: Job<any>[];
88
- /** Route prefix. Defaults to "/jobs". */
88
+ /** Route prefix for jobs. Defaults to "/jobs". */
89
89
  prefix?: string;
90
+ /**
91
+ * If set, require requests to `/jobs/*` to include this exact value in
92
+ * the `X-Worker-Secret` header. Returns 401 otherwise. In production,
93
+ * leave unset and let Cloud Run IAM (run.invoker) gate access; this is
94
+ * only useful for local dev or non-IAM-gated deployments.
95
+ */
96
+ workerSecret?: string;
97
+ /** Disable request logging on `/jobs/*`. Default: enabled. */
98
+ log?: false;
90
99
  }
91
100
  /**
92
- * Build a Hono app that exposes POST {prefix}/{jobName} for each job and
93
- * wraps each handler with job-echo lifecycle tracking.
101
+ * Build a Hono app that:
102
+ * - Exposes GET `/health` + `/healthz` (no auth, no logging)
103
+ * - Optionally requires `X-Worker-Secret` on `/jobs/*` (when `workerSecret` set)
104
+ * - Logs requests to `/jobs/*` with duration (unless `log: false`)
105
+ * - Exposes POST `/jobs/{name}` for each job, wrapping handlers with
106
+ * job-echo lifecycle tracking (in_progress → completed / failed)
94
107
  */
95
108
  declare function createWorker(cfg: WorkerConfig): Hono;
96
109
  /**
package/dist/index.js CHANGED
@@ -498,7 +498,31 @@ function createWorker(cfg) {
498
498
  const echo = createClient(cfg);
499
499
  const prefix = cfg.prefix ?? "/jobs";
500
500
  const app = new Hono();
501
+ app.onError((err, c) => {
502
+ console.error(`[job-echo] ${c.req.method} ${c.req.path} error:`, err);
503
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
504
+ });
505
+ app.get("/health", (c) => c.text("ok"));
501
506
  app.get("/healthz", (c) => c.text("ok"));
507
+ if (cfg.log !== false) {
508
+ app.use(`${prefix}/*`, async (c, next) => {
509
+ const start = Date.now();
510
+ console.log(`[job-echo] --> ${c.req.method} ${c.req.path}`);
511
+ await next();
512
+ console.log(
513
+ `[job-echo] <-- ${c.req.method} ${c.req.path} ${c.res.status} (${Date.now() - start}ms)`
514
+ );
515
+ });
516
+ }
517
+ if (cfg.workerSecret) {
518
+ const secret = cfg.workerSecret;
519
+ app.use(`${prefix}/*`, async (c, next) => {
520
+ if (c.req.header("X-Worker-Secret") !== secret) {
521
+ return c.json({ error: "unauthorized" }, 401);
522
+ }
523
+ return next();
524
+ });
525
+ }
502
526
  for (const job of cfg.jobs) {
503
527
  app.post(`${prefix}/${job.name}`, async (c) => {
504
528
  const input = await c.req.json().catch(() => ({}));
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/auth.ts","../src/firestore-rest.ts","../src/store.ts","../src/client.ts","../src/dispatcher.ts","../src/job.ts","../src/worker.ts","../src/sync.ts"],"sourcesContent":["const METADATA_URL =\n 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token'\n\ninterface TokenResponse {\n access_token: string\n expires_in: number\n token_type: string\n}\n\nlet cached: { token: string; expiresAt: number } | null = null\n\n/**\n * Fetch an OAuth2 access token from GCP's metadata server. The token is\n * cached in-process until 60s before expiry.\n *\n * Only works on GCP compute (Cloud Run, GCE, GKE, Cloud Functions, etc.).\n * For non-GCP environments the package's production code path is not\n * expected to run — dev mode short-circuits before this is ever called.\n */\nexport async function getAccessToken(): Promise<string> {\n const now = Date.now()\n if (cached && cached.expiresAt > now + 60_000) return cached.token\n\n const res = await fetch(METADATA_URL, { headers: { 'Metadata-Flavor': 'Google' } })\n if (!res.ok) {\n throw new Error(\n `@originalvoices/job-echo-client: failed to fetch GCP access token (status ${res.status}). Is this running on GCP compute?`,\n )\n }\n const body = (await res.json()) as TokenResponse\n cached = {\n token: body.access_token,\n expiresAt: now + body.expires_in * 1000,\n }\n return cached.token\n}\n","import { getAccessToken } from './auth'\n\n/**\n * Minimal Firestore REST client — just enough to create/patch/query documents.\n * Uses the metadata-server access token; no SDK, no native deps.\n *\n * Docs: https://firestore.googleapis.com/$discovery/rest?version=v1\n */\n\nconst BASE = 'https://firestore.googleapis.com/v1'\n\nfunction baseUrl(projectId: string) {\n return `${BASE}/projects/${projectId}/databases/(default)/documents`\n}\n\nasync function authed(url: string, init?: RequestInit): Promise<Response> {\n const token = await getAccessToken()\n const res = await fetch(url, {\n ...init,\n headers: {\n ...(init?.headers ?? {}),\n authorization: `Bearer ${token}`,\n 'content-type': 'application/json',\n },\n })\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new Error(`Firestore REST ${res.status}: ${body}`)\n }\n return res\n}\n\n// ── Typed value encoding / decoding ───────────────────────────────────────────\n\ntype FsValue =\n | { stringValue: string }\n | { integerValue: string }\n | { doubleValue: number }\n | { booleanValue: boolean }\n | { nullValue: null }\n | { timestampValue: string }\n | { mapValue: { fields: Record<string, FsValue> } }\n | { arrayValue: { values: FsValue[] } }\n\nexport function encodeValue(v: unknown): FsValue {\n if (v === null || v === undefined) return { nullValue: null }\n if (typeof v === 'string') return { stringValue: v }\n if (typeof v === 'boolean') return { booleanValue: v }\n if (typeof v === 'number') {\n return Number.isInteger(v)\n ? { integerValue: String(v) }\n : { doubleValue: v }\n }\n if (Array.isArray(v)) {\n return { arrayValue: { values: v.map(encodeValue) } }\n }\n if (typeof v === 'object') {\n return { mapValue: { fields: encodeFields(v as Record<string, unknown>) } }\n }\n // Fallback — coerce to string\n return { stringValue: String(v) }\n}\n\nexport function encodeFields(obj: Record<string, unknown>): Record<string, FsValue> {\n const out: Record<string, FsValue> = {}\n for (const [k, v] of Object.entries(obj)) out[k] = encodeValue(v)\n return out\n}\n\nexport function decodeValue(v: FsValue): unknown {\n if ('stringValue' in v) return v.stringValue\n if ('integerValue' in v) return Number(v.integerValue)\n if ('doubleValue' in v) return v.doubleValue\n if ('booleanValue' in v) return v.booleanValue\n if ('nullValue' in v) return null\n if ('timestampValue' in v) return v.timestampValue\n if ('mapValue' in v) return decodeFields(v.mapValue.fields ?? {})\n if ('arrayValue' in v) return (v.arrayValue.values ?? []).map(decodeValue)\n return null\n}\n\nexport function decodeFields(\n fields: Record<string, FsValue>,\n): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(fields)) out[k] = decodeValue(v)\n return out\n}\n\n// ── Public operations ─────────────────────────────────────────────────────────\n\nfunction docIdFromName(name: string): string {\n // name looks like \"projects/xxx/databases/(default)/documents/collection/docId\"\n return name.split('/').pop() ?? name\n}\n\nexport interface FirestoreDoc {\n id: string\n data: Record<string, unknown>\n}\n\nexport async function createDoc(\n projectId: string,\n collection: string,\n data: Record<string, unknown>,\n): Promise<FirestoreDoc> {\n const res = await authed(`${baseUrl(projectId)}/${collection}`, {\n method: 'POST',\n body: JSON.stringify({ fields: encodeFields(data) }),\n })\n const body = (await res.json()) as { name: string; fields?: Record<string, FsValue> }\n return { id: docIdFromName(body.name), data: decodeFields(body.fields ?? {}) }\n}\n\nexport async function patchDoc(\n projectId: string,\n collection: string,\n id: string,\n patch: Record<string, unknown>,\n): Promise<void> {\n const params = Object.keys(patch)\n .map((k) => `updateMask.fieldPaths=${encodeURIComponent(k)}`)\n .join('&')\n await authed(`${baseUrl(projectId)}/${collection}/${id}?${params}`, {\n method: 'PATCH',\n body: JSON.stringify({ fields: encodeFields(patch) }),\n })\n}\n\n/**\n * Run a simple equality-filter query against a single collection.\n * Returns at most `limit` docs.\n */\nexport async function queryDocs(\n projectId: string,\n collection: string,\n filters: Array<{ field: string; value: string }>,\n limit = 1,\n): Promise<FirestoreDoc[]> {\n const compositeFilter = {\n compositeFilter: {\n op: 'AND',\n filters: filters.map((f) => ({\n fieldFilter: {\n field: { fieldPath: f.field },\n op: 'EQUAL',\n value: { stringValue: f.value },\n },\n })),\n },\n }\n\n const structuredQuery = {\n from: [{ collectionId: collection }],\n where: filters.length ? compositeFilter : undefined,\n limit,\n }\n\n const res = await authed(`${baseUrl(projectId)}:runQuery`, {\n method: 'POST',\n body: JSON.stringify({ structuredQuery }),\n })\n const rows = (await res.json()) as Array<{\n document?: { name: string; fields?: Record<string, FsValue> }\n }>\n return rows\n .filter((r) => r.document)\n .map((r) => ({\n id: docIdFromName(r.document!.name),\n data: decodeFields(r.document!.fields ?? {}),\n }))\n}\n","import { createDoc, patchDoc, queryDocs } from './firestore-rest'\nimport type { JobEvent, JobStatus } from './types'\n\nconst PROJECTS = 'projects'\nconst ENVIRONMENTS = 'environments'\nconst EVENTS = 'events'\n\nconst now = () => Date.now()\n\nexport async function findOrCreateContext(\n gcpProjectId: string,\n projectName: string,\n envName: string,\n): Promise<{ projectId: string; environmentId: string }> {\n const projMatches = await queryDocs(\n gcpProjectId,\n PROJECTS,\n [{ field: 'name', value: projectName }],\n 1,\n )\n const projectId = projMatches[0]\n ? projMatches[0].id\n : (await createDoc(gcpProjectId, PROJECTS, { name: projectName, createdAt: now() })).id\n\n const envMatches = await queryDocs(\n gcpProjectId,\n ENVIRONMENTS,\n [\n { field: 'projectId', value: projectId },\n { field: 'name', value: envName },\n ],\n 1,\n )\n const environmentId = envMatches[0]\n ? envMatches[0].id\n : (\n await createDoc(gcpProjectId, ENVIRONMENTS, {\n projectId,\n name: envName,\n createdAt: now(),\n })\n ).id\n\n return { projectId, environmentId }\n}\n\nexport async function createEvent(\n gcpProjectId: string,\n args: {\n projectId: string\n environmentId: string\n name: string\n input?: unknown\n status?: JobStatus\n },\n): Promise<JobEvent> {\n const ts = now()\n const data: Omit<JobEvent, 'id'> = {\n projectId: args.projectId,\n environmentId: args.environmentId,\n name: args.name,\n status: args.status ?? 'in_progress',\n input: args.input ?? null,\n output: null,\n error: null,\n createdAt: ts,\n updatedAt: ts,\n completedAt: null,\n }\n const doc = await createDoc(gcpProjectId, EVENTS, data)\n return { id: doc.id, ...data }\n}\n\nexport async function patchEvent(\n gcpProjectId: string,\n id: string,\n patch: { status?: JobStatus; output?: unknown; error?: string | null },\n): Promise<void> {\n const ts = now()\n const update: Record<string, unknown> = { updatedAt: ts }\n if (patch.status !== undefined) {\n update.status = patch.status\n if (patch.status === 'completed' || patch.status === 'failed') {\n update.completedAt = ts\n }\n }\n if (patch.output !== undefined) update.output = patch.output\n if (patch.error !== undefined) update.error = patch.error\n\n await patchDoc(gcpProjectId, EVENTS, id, update)\n}\n","import { createEvent, findOrCreateContext, patchEvent } from './store'\nimport type { ClientConfig, JobEvent } from './types'\n\nexport interface JobEchoClient {\n /** Record a new job event as `in_progress`. Returns the created event. */\n start(name: string, input?: unknown): Promise<JobEvent>\n /** Mark an existing event `completed`. Optionally attach a return value as `output`. */\n complete(id: string, output?: unknown): Promise<void>\n /** Mark an existing event `failed` with an error message. */\n fail(id: string, error: string, output?: unknown): Promise<void>\n}\n\nexport function createClient(cfg: ClientConfig): JobEchoClient {\n // Only write to Firestore in production. Local/dev runs are a no-op —\n // handlers still execute, just no lifecycle events recorded.\n if (process.env.NODE_ENV !== 'production') return disabledClient()\n\n const gcpProjectId =\n cfg.collectorProjectId ??\n process.env.JOB_ECHO_GCP_PROJECT ??\n process.env.FIREBASE_PROJECT_ID\n\n // No collector project configured → run as no-op. Lets services adopt the\n // worker pattern before the dashboard's Firestore is provisioned.\n if (!gcpProjectId) {\n console.warn(\n '[job-echo] no collectorProjectId / JOB_ECHO_GCP_PROJECT / FIREBASE_PROJECT_ID set — Firestore writes disabled',\n )\n return disabledClient()\n }\n\n let contextPromise: Promise<{ projectId: string; environmentId: string }> | null = null\n const context = () => {\n if (!contextPromise) {\n contextPromise = findOrCreateContext(gcpProjectId, cfg.project, cfg.environment)\n }\n return contextPromise\n }\n\n return {\n async start(name, input) {\n const { projectId, environmentId } = await context()\n return createEvent(gcpProjectId, { projectId, environmentId, name, input })\n },\n async complete(id, output) {\n await patchEvent(gcpProjectId, id, { status: 'completed', output })\n },\n async fail(id, error, output) {\n await patchEvent(gcpProjectId, id, { status: 'failed', error, output })\n },\n }\n}\n\nfunction disabledClient(): JobEchoClient {\n const ts = Date.now()\n return {\n async start(name, input) {\n return {\n id: 'disabled',\n projectId: 'disabled',\n environmentId: 'disabled',\n name,\n status: 'in_progress',\n input,\n output: null,\n error: null,\n createdAt: ts,\n updatedAt: ts,\n completedAt: null,\n }\n },\n async complete() {},\n async fail() {},\n }\n}\n","import { getAccessToken } from './auth'\n\nexport interface Dispatcher {\n dispatch(jobName: string, payload: unknown): Promise<void>\n}\n\nexport interface HttpDispatcherConfig {\n baseUrl: string\n headers?: Record<string, string>\n prefix?: string\n}\n\nexport interface CloudTasksDispatcherConfig {\n projectId: string\n location: string\n queue: string\n workerUrl: string\n serviceAccountEmail: string\n prefix?: string\n audience?: string\n}\n\n/**\n * HTTP dispatcher: plain POST to `{baseUrl}{prefix}/{jobName}`.\n */\nexport function createHttpDispatcher(cfg: HttpDispatcherConfig): Dispatcher {\n const prefix = cfg.prefix ?? '/jobs'\n return {\n async dispatch(jobName, payload) {\n const res = await fetch(`${cfg.baseUrl}${prefix}/${jobName}`, {\n method: 'POST',\n headers: { 'content-type': 'application/json', ...(cfg.headers ?? {}) },\n body: JSON.stringify(payload),\n })\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new Error(\n `job-echo dispatch '${jobName}' failed: ${res.status} ${body}`.trim(),\n )\n }\n },\n }\n}\n\n/**\n * Cloud Tasks dispatcher via the REST API — no @google-cloud/tasks SDK.\n * Enqueues an HTTP task with an OIDC token so the worker can verify\n * the caller identity.\n */\nexport function createCloudTasksDispatcher(\n cfg: CloudTasksDispatcherConfig,\n): Dispatcher {\n const prefix = cfg.prefix ?? '/jobs'\n const audience = cfg.audience ?? cfg.workerUrl\n const url = `https://cloudtasks.googleapis.com/v2/projects/${cfg.projectId}/locations/${cfg.location}/queues/${cfg.queue}/tasks`\n\n return {\n async dispatch(jobName, payload) {\n const token = await getAccessToken()\n const body = {\n task: {\n httpRequest: {\n httpMethod: 'POST',\n url: `${cfg.workerUrl}${prefix}/${jobName}`,\n headers: { 'content-type': 'application/json' },\n body: Buffer.from(JSON.stringify(payload)).toString('base64'),\n oidcToken: {\n serviceAccountEmail: cfg.serviceAccountEmail,\n audience,\n },\n },\n },\n }\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n authorization: `Bearer ${token}`,\n 'content-type': 'application/json',\n },\n body: JSON.stringify(body),\n })\n if (!res.ok) {\n const respBody = await res.text().catch(() => '')\n throw new Error(\n `job-echo Cloud Tasks enqueue '${jobName}' failed: ${res.status} ${respBody}`.trim(),\n )\n }\n },\n }\n}\n\nlet active: Dispatcher | null = null\n\n/**\n * Returns the dispatcher, auto-detected from env on first call:\n * - `NODE_ENV=production`: Cloud Tasks (requires `JOB_ECHO_TASKS_*` env vars)\n * - otherwise: HTTP to `JOB_ECHO_WORKER_URL` (default `http://localhost:8080`)\n */\nexport function getDispatcher(): Dispatcher {\n if (!active) active = autoDetectDispatcher()\n return active\n}\n\n/**\n * Parse a Cloud Tasks queue resource path:\n * `projects/{project}/locations/{location}/queues/{queue}`\n */\nfunction parseQueuePath(\n path: string,\n): { projectId: string; location: string; queue: string } {\n const m = path.match(\n /^projects\\/([^/]+)\\/locations\\/([^/]+)\\/queues\\/([^/]+)$/,\n )\n if (!m) {\n throw new Error(\n `@originalvoices/job-echo-client: JOB_ECHO_TASKS_QUEUE must be a full resource path (projects/{p}/locations/{l}/queues/{q}), got \"${path}\"`,\n )\n }\n return { projectId: m[1]!, location: m[2]!, queue: m[3]! }\n}\n\nfunction autoDetectDispatcher(): Dispatcher {\n if (process.env.NODE_ENV === 'production') {\n const required = {\n JOB_ECHO_TASKS_QUEUE: process.env.JOB_ECHO_TASKS_QUEUE,\n JOB_ECHO_WORKER_URL: process.env.JOB_ECHO_WORKER_URL,\n JOB_ECHO_TASKS_SA_EMAIL: process.env.JOB_ECHO_TASKS_SA_EMAIL,\n }\n const missing = Object.entries(required)\n .filter(([, v]) => !v)\n .map(([k]) => k)\n if (missing.length) {\n throw new Error(\n `@originalvoices/job-echo-client: Cloud Tasks dispatcher needs env vars: ${missing.join(', ')}`,\n )\n }\n const { projectId, location, queue } = parseQueuePath(\n required.JOB_ECHO_TASKS_QUEUE!,\n )\n return createCloudTasksDispatcher({\n projectId,\n location,\n queue,\n workerUrl: required.JOB_ECHO_WORKER_URL!,\n serviceAccountEmail: required.JOB_ECHO_TASKS_SA_EMAIL!,\n })\n }\n\n const baseUrl = process.env.JOB_ECHO_WORKER_URL ?? 'http://localhost:8080'\n return createHttpDispatcher({ baseUrl })\n}\n","import { getDispatcher } from './dispatcher'\nimport type { JobEchoClient } from './client'\n\nexport interface JobContext {\n /** Firestore event id for this invocation (status=in_progress). */\n eventId: string\n /** The raw job-echo client — use for nested events or custom updates. */\n echo: JobEchoClient\n}\n\nexport interface JobDefinition<Input> {\n name: string\n handler: (input: Input, ctx: JobContext) => Promise<unknown>\n /**\n * Optional cron expression. When set, `syncSchedules` will provision a\n * Cloud Scheduler job that fires this handler on the given cadence.\n */\n schedule?: string\n /** IANA timezone for `schedule`. Defaults to \"Etc/UTC\". */\n timezone?: string\n}\n\nexport interface Job<Input> extends JobDefinition<Input> {\n /**\n * Send `input` to the configured dispatcher (defaults to HTTP POST to the\n * worker). Requires `configureDispatcher({ baseUrl })` at startup.\n */\n emit(input: Input): Promise<void>\n /** Type marker — not present at runtime. */\n _input?: Input\n}\n\n/**\n * Define a typed job. Returns an object with a bound `.emit(input)` so call\n * sites don't have to know about the worker URL or shape of the HTTP call.\n *\n * ```ts\n * export const sendMessage = defineJob({\n * name: 'send-message',\n * async handler(input: { threadId: string }) { ... },\n * })\n *\n * // elsewhere, fully typed:\n * await sendMessage.emit({ threadId: 't1' })\n * ```\n */\nexport function defineJob<Input>(def: JobDefinition<Input>): Job<Input> {\n return {\n ...def,\n emit: (input) => getDispatcher().dispatch(def.name, input),\n }\n}\n\n/** Extract the input type from a Job. */\nexport type JobInput<J> = J extends Job<infer I> ? I : never\n","import { Hono } from 'hono'\nimport { serve } from '@hono/node-server'\nimport { createClient, type JobEchoClient } from './client'\nimport type { Job } from './job'\nimport { syncSchedules } from './sync'\nimport type { ClientConfig } from './types'\n\nexport interface WorkerConfig extends ClientConfig {\n jobs: Job<any>[]\n /** Route prefix. Defaults to \"/jobs\". */\n prefix?: string\n}\n\n/**\n * Build a Hono app that exposes POST {prefix}/{jobName} for each job and\n * wraps each handler with job-echo lifecycle tracking.\n */\nexport function createWorker(cfg: WorkerConfig): Hono {\n const echo = createClient(cfg)\n const prefix = cfg.prefix ?? '/jobs'\n const app = new Hono()\n\n app.get('/healthz', (c) => c.text('ok'))\n\n for (const job of cfg.jobs) {\n app.post(`${prefix}/${job.name}`, async (c) => {\n const input = (await c.req.json().catch(() => ({}))) as unknown\n const event = await echo.start(job.name, input)\n try {\n const result = await job.handler(input as any, { eventId: event.id, echo })\n await echo.complete(event.id, result ?? undefined)\n return c.json({ ok: true, eventId: event.id, result: result ?? null })\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n await echo.fail(event.id, message)\n return c.json({ ok: false, eventId: event.id, error: message }, 500)\n }\n })\n }\n\n return app\n}\n\n/**\n * If `JOB_ECHO_SYNC_MODE=1` is set, fire schedule sync (which will\n * `process.exit(0)` on success / `1` on failure) and return `true` so\n * the caller knows to skip normal startup. Otherwise returns `false`.\n *\n * Synchronous return so it composes naturally in CJS entrypoints\n * (no top-level `await` needed).\n *\n * ```ts\n * if (runScheduleSyncIfRequested(allJobs)) {\n * // sync running; don't start server\n * } else {\n * startServer()\n * }\n * ```\n */\nexport function runScheduleSyncIfRequested<\n J extends { name: string; schedule?: string; timezone?: string },\n>(jobs: J[]): boolean {\n if (process.env.JOB_ECHO_SYNC_MODE !== '1') return false\n\n syncSchedules(jobs)\n .then((report) => {\n console.log(\n '[job-echo] schedule sync complete:\\n' + JSON.stringify(report, null, 2),\n )\n process.exit(0)\n })\n .catch((err) => {\n console.error('[job-echo] schedule sync failed:', err)\n process.exit(1)\n })\n\n return true\n}\n\n/**\n * Convenience: build a worker and start the HTTP server on `port`\n * (defaults to `PORT` env or 8080). Also handles `JOB_ECHO_SYNC_MODE=1`\n * via `runScheduleSyncIfRequested` — same binary, two modes.\n */\nexport function startWorker(\n cfg: WorkerConfig & { port?: number },\n): ReturnType<typeof serve> | void {\n if (process.env.JOB_ECHO_SYNC_MODE === '1') {\n runScheduleSyncIfRequested(cfg.jobs)\n return\n }\n\n const app = createWorker(cfg)\n const port = cfg.port ?? Number(process.env.PORT ?? 8080)\n console.log(`[job-echo] worker listening on :${port}`)\n return serve({ fetch: app.fetch, port })\n}\n","import { getAccessToken } from './auth'\n\nconst BASE = 'https://cloudscheduler.googleapis.com/v1'\n\nexport interface SyncReport {\n created: string[]\n updated: string[]\n deleted: string[]\n unchanged: string[]\n}\n\ninterface ParsedQueue {\n projectId: string\n location: string\n queue: string\n fullPath: string\n}\n\nfunction parseQueuePath(path: string): ParsedQueue {\n const m = path.match(\n /^projects\\/([^/]+)\\/locations\\/([^/]+)\\/queues\\/([^/]+)$/,\n )\n if (!m) {\n throw new Error(\n `@originalvoices/job-echo-client: JOB_ECHO_TASKS_QUEUE must be a full resource path (projects/{p}/locations/{l}/queues/{q}), got \"${path}\"`,\n )\n }\n return { projectId: m[1]!, location: m[2]!, queue: m[3]!, fullPath: path }\n}\n\nfunction sanitizeName(name: string): string {\n return name.replace(/[^a-zA-Z0-9-]/g, '-').replace(/^-+|-+$/g, '')\n}\n\nasync function authed(url: string, init?: RequestInit): Promise<Response> {\n const token = await getAccessToken()\n const res = await fetch(url, {\n ...init,\n headers: {\n ...(init?.headers ?? {}),\n authorization: `Bearer ${token}`,\n 'content-type': 'application/json',\n },\n })\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new Error(`Cloud Scheduler ${res.status}: ${body}`)\n }\n return res\n}\n\ninterface SchedulerJob {\n name: string\n schedule: string\n timeZone: string\n httpTarget: {\n uri: string\n httpMethod: string\n headers?: Record<string, string>\n body?: string\n oauthToken?: {\n serviceAccountEmail: string\n scope?: string\n }\n }\n labels?: Record<string, string>\n}\n\nfunction buildSchedulerJob(args: {\n fullName: string\n cron: string\n timezone: string\n queue: ParsedQueue\n workerUrl: string\n invokerSa: string\n jobName: string\n appLabel: string\n}): SchedulerJob {\n const task = {\n httpRequest: {\n url: `${args.workerUrl}/jobs/${args.jobName}`,\n httpMethod: 'POST',\n headers: { 'content-type': 'application/json' },\n body: Buffer.from('{}').toString('base64'),\n oidcToken: {\n serviceAccountEmail: args.invokerSa,\n audience: args.workerUrl,\n },\n },\n }\n\n return {\n name: args.fullName,\n schedule: args.cron,\n timeZone: args.timezone,\n httpTarget: {\n uri: `https://cloudtasks.googleapis.com/v2/${args.queue.fullPath}/tasks`,\n httpMethod: 'POST',\n headers: { 'content-type': 'application/json' },\n body: Buffer.from(JSON.stringify({ task })).toString('base64'),\n oauthToken: {\n serviceAccountEmail: args.invokerSa,\n scope: 'https://www.googleapis.com/auth/cloud-platform',\n },\n },\n labels: {\n 'managed-by': 'job-echo',\n app: args.appLabel,\n job: args.jobName,\n },\n }\n}\n\nasync function listManaged(\n queue: ParsedQueue,\n appLabel: string,\n): Promise<SchedulerJob[]> {\n const filter = `labels.managed-by=\"job-echo\" AND labels.app=\"${appLabel}\"`\n const url = `${BASE}/projects/${queue.projectId}/locations/${queue.location}/jobs?filter=${encodeURIComponent(filter)}`\n const res = await authed(url)\n const body = (await res.json()) as { jobs?: SchedulerJob[] }\n return body.jobs ?? []\n}\n\nfunction differs(a: SchedulerJob, b: SchedulerJob): boolean {\n return (\n a.schedule !== b.schedule ||\n a.timeZone !== b.timeZone ||\n a.httpTarget.uri !== b.httpTarget.uri ||\n a.httpTarget.body !== b.httpTarget.body\n )\n}\n\n/**\n * Reconcile Cloud Scheduler jobs to match the schedules declared on the\n * given jobs. Only touches schedulers labelled `managed-by=job-echo` and\n * `app=<JOB_ECHO_APP_LABEL>` — manually created schedulers are untouched.\n *\n * Required env:\n * - JOB_ECHO_TASKS_QUEUE (full resource path: projects/.../queues/...)\n * - JOB_ECHO_WORKER_URL (https URL of the worker service)\n * - JOB_ECHO_TASKS_SA_EMAIL (SA the scheduler-fired tasks run as)\n * - JOB_ECHO_APP_LABEL (e.g. \"platform-prod\" — scopes the sync)\n */\nexport async function syncSchedules<\n J extends { name: string; schedule?: string; timezone?: string },\n>(jobs: J[]): Promise<SyncReport> {\n const env = {\n JOB_ECHO_TASKS_QUEUE: process.env.JOB_ECHO_TASKS_QUEUE,\n JOB_ECHO_WORKER_URL: process.env.JOB_ECHO_WORKER_URL,\n JOB_ECHO_TASKS_SA_EMAIL: process.env.JOB_ECHO_TASKS_SA_EMAIL,\n JOB_ECHO_APP_LABEL: process.env.JOB_ECHO_APP_LABEL,\n }\n const missing = Object.entries(env)\n .filter(([, v]) => !v)\n .map(([k]) => k)\n if (missing.length) {\n throw new Error(\n `@originalvoices/job-echo-client: syncSchedules needs env vars: ${missing.join(', ')}`,\n )\n }\n\n const queue = parseQueuePath(env.JOB_ECHO_TASKS_QUEUE!)\n const workerUrl = env.JOB_ECHO_WORKER_URL!\n const invokerSa = env.JOB_ECHO_TASKS_SA_EMAIL!\n const appLabel = env.JOB_ECHO_APP_LABEL!\n\n const scheduled = jobs.filter((j) => Boolean(j.schedule))\n\n const desired = new Map<string, SchedulerJob>()\n for (const job of scheduled) {\n const schedulerName = sanitizeName(`${appLabel}-${job.name}`)\n const fullName = `projects/${queue.projectId}/locations/${queue.location}/jobs/${schedulerName}`\n desired.set(\n fullName,\n buildSchedulerJob({\n fullName,\n cron: job.schedule!,\n timezone: job.timezone ?? 'Etc/UTC',\n queue,\n workerUrl,\n invokerSa,\n jobName: job.name,\n appLabel,\n }),\n )\n }\n\n const existing = await listManaged(queue, appLabel)\n const existingByName = new Map(existing.map((j) => [j.name, j]))\n\n const report: SyncReport = {\n created: [],\n updated: [],\n deleted: [],\n unchanged: [],\n }\n\n for (const [name, want] of desired) {\n const have = existingByName.get(name)\n if (!have) {\n const url = `${BASE}/projects/${queue.projectId}/locations/${queue.location}/jobs`\n await authed(url, { method: 'POST', body: JSON.stringify(want) })\n report.created.push(name)\n } else if (differs(have, want)) {\n const url = `${BASE}/${name}?updateMask=schedule,timeZone,httpTarget,labels`\n await authed(url, { method: 'PATCH', body: JSON.stringify(want) })\n report.updated.push(name)\n } else {\n report.unchanged.push(name)\n }\n }\n\n for (const name of existingByName.keys()) {\n if (!desired.has(name)) {\n await authed(`${BASE}/${name}`, { method: 'DELETE' })\n report.deleted.push(name)\n }\n }\n\n return report\n}\n"],"mappings":"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","names":["now","baseUrl","BASE","parseQueuePath","authed"]}
1
+ {"version":3,"sources":["../src/auth.ts","../src/firestore-rest.ts","../src/store.ts","../src/client.ts","../src/dispatcher.ts","../src/job.ts","../src/worker.ts","../src/sync.ts"],"sourcesContent":["const METADATA_URL =\n 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token'\n\ninterface TokenResponse {\n access_token: string\n expires_in: number\n token_type: string\n}\n\nlet cached: { token: string; expiresAt: number } | null = null\n\n/**\n * Fetch an OAuth2 access token from GCP's metadata server. The token is\n * cached in-process until 60s before expiry.\n *\n * Only works on GCP compute (Cloud Run, GCE, GKE, Cloud Functions, etc.).\n * For non-GCP environments the package's production code path is not\n * expected to run — dev mode short-circuits before this is ever called.\n */\nexport async function getAccessToken(): Promise<string> {\n const now = Date.now()\n if (cached && cached.expiresAt > now + 60_000) return cached.token\n\n const res = await fetch(METADATA_URL, { headers: { 'Metadata-Flavor': 'Google' } })\n if (!res.ok) {\n throw new Error(\n `@originalvoices/job-echo-client: failed to fetch GCP access token (status ${res.status}). Is this running on GCP compute?`,\n )\n }\n const body = (await res.json()) as TokenResponse\n cached = {\n token: body.access_token,\n expiresAt: now + body.expires_in * 1000,\n }\n return cached.token\n}\n","import { getAccessToken } from './auth'\n\n/**\n * Minimal Firestore REST client — just enough to create/patch/query documents.\n * Uses the metadata-server access token; no SDK, no native deps.\n *\n * Docs: https://firestore.googleapis.com/$discovery/rest?version=v1\n */\n\nconst BASE = 'https://firestore.googleapis.com/v1'\n\nfunction baseUrl(projectId: string) {\n return `${BASE}/projects/${projectId}/databases/(default)/documents`\n}\n\nasync function authed(url: string, init?: RequestInit): Promise<Response> {\n const token = await getAccessToken()\n const res = await fetch(url, {\n ...init,\n headers: {\n ...(init?.headers ?? {}),\n authorization: `Bearer ${token}`,\n 'content-type': 'application/json',\n },\n })\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new Error(`Firestore REST ${res.status}: ${body}`)\n }\n return res\n}\n\n// ── Typed value encoding / decoding ───────────────────────────────────────────\n\ntype FsValue =\n | { stringValue: string }\n | { integerValue: string }\n | { doubleValue: number }\n | { booleanValue: boolean }\n | { nullValue: null }\n | { timestampValue: string }\n | { mapValue: { fields: Record<string, FsValue> } }\n | { arrayValue: { values: FsValue[] } }\n\nexport function encodeValue(v: unknown): FsValue {\n if (v === null || v === undefined) return { nullValue: null }\n if (typeof v === 'string') return { stringValue: v }\n if (typeof v === 'boolean') return { booleanValue: v }\n if (typeof v === 'number') {\n return Number.isInteger(v)\n ? { integerValue: String(v) }\n : { doubleValue: v }\n }\n if (Array.isArray(v)) {\n return { arrayValue: { values: v.map(encodeValue) } }\n }\n if (typeof v === 'object') {\n return { mapValue: { fields: encodeFields(v as Record<string, unknown>) } }\n }\n // Fallback — coerce to string\n return { stringValue: String(v) }\n}\n\nexport function encodeFields(obj: Record<string, unknown>): Record<string, FsValue> {\n const out: Record<string, FsValue> = {}\n for (const [k, v] of Object.entries(obj)) out[k] = encodeValue(v)\n return out\n}\n\nexport function decodeValue(v: FsValue): unknown {\n if ('stringValue' in v) return v.stringValue\n if ('integerValue' in v) return Number(v.integerValue)\n if ('doubleValue' in v) return v.doubleValue\n if ('booleanValue' in v) return v.booleanValue\n if ('nullValue' in v) return null\n if ('timestampValue' in v) return v.timestampValue\n if ('mapValue' in v) return decodeFields(v.mapValue.fields ?? {})\n if ('arrayValue' in v) return (v.arrayValue.values ?? []).map(decodeValue)\n return null\n}\n\nexport function decodeFields(\n fields: Record<string, FsValue>,\n): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(fields)) out[k] = decodeValue(v)\n return out\n}\n\n// ── Public operations ─────────────────────────────────────────────────────────\n\nfunction docIdFromName(name: string): string {\n // name looks like \"projects/xxx/databases/(default)/documents/collection/docId\"\n return name.split('/').pop() ?? name\n}\n\nexport interface FirestoreDoc {\n id: string\n data: Record<string, unknown>\n}\n\nexport async function createDoc(\n projectId: string,\n collection: string,\n data: Record<string, unknown>,\n): Promise<FirestoreDoc> {\n const res = await authed(`${baseUrl(projectId)}/${collection}`, {\n method: 'POST',\n body: JSON.stringify({ fields: encodeFields(data) }),\n })\n const body = (await res.json()) as { name: string; fields?: Record<string, FsValue> }\n return { id: docIdFromName(body.name), data: decodeFields(body.fields ?? {}) }\n}\n\nexport async function patchDoc(\n projectId: string,\n collection: string,\n id: string,\n patch: Record<string, unknown>,\n): Promise<void> {\n const params = Object.keys(patch)\n .map((k) => `updateMask.fieldPaths=${encodeURIComponent(k)}`)\n .join('&')\n await authed(`${baseUrl(projectId)}/${collection}/${id}?${params}`, {\n method: 'PATCH',\n body: JSON.stringify({ fields: encodeFields(patch) }),\n })\n}\n\n/**\n * Run a simple equality-filter query against a single collection.\n * Returns at most `limit` docs.\n */\nexport async function queryDocs(\n projectId: string,\n collection: string,\n filters: Array<{ field: string; value: string }>,\n limit = 1,\n): Promise<FirestoreDoc[]> {\n const compositeFilter = {\n compositeFilter: {\n op: 'AND',\n filters: filters.map((f) => ({\n fieldFilter: {\n field: { fieldPath: f.field },\n op: 'EQUAL',\n value: { stringValue: f.value },\n },\n })),\n },\n }\n\n const structuredQuery = {\n from: [{ collectionId: collection }],\n where: filters.length ? compositeFilter : undefined,\n limit,\n }\n\n const res = await authed(`${baseUrl(projectId)}:runQuery`, {\n method: 'POST',\n body: JSON.stringify({ structuredQuery }),\n })\n const rows = (await res.json()) as Array<{\n document?: { name: string; fields?: Record<string, FsValue> }\n }>\n return rows\n .filter((r) => r.document)\n .map((r) => ({\n id: docIdFromName(r.document!.name),\n data: decodeFields(r.document!.fields ?? {}),\n }))\n}\n","import { createDoc, patchDoc, queryDocs } from './firestore-rest'\nimport type { JobEvent, JobStatus } from './types'\n\nconst PROJECTS = 'projects'\nconst ENVIRONMENTS = 'environments'\nconst EVENTS = 'events'\n\nconst now = () => Date.now()\n\nexport async function findOrCreateContext(\n gcpProjectId: string,\n projectName: string,\n envName: string,\n): Promise<{ projectId: string; environmentId: string }> {\n const projMatches = await queryDocs(\n gcpProjectId,\n PROJECTS,\n [{ field: 'name', value: projectName }],\n 1,\n )\n const projectId = projMatches[0]\n ? projMatches[0].id\n : (await createDoc(gcpProjectId, PROJECTS, { name: projectName, createdAt: now() })).id\n\n const envMatches = await queryDocs(\n gcpProjectId,\n ENVIRONMENTS,\n [\n { field: 'projectId', value: projectId },\n { field: 'name', value: envName },\n ],\n 1,\n )\n const environmentId = envMatches[0]\n ? envMatches[0].id\n : (\n await createDoc(gcpProjectId, ENVIRONMENTS, {\n projectId,\n name: envName,\n createdAt: now(),\n })\n ).id\n\n return { projectId, environmentId }\n}\n\nexport async function createEvent(\n gcpProjectId: string,\n args: {\n projectId: string\n environmentId: string\n name: string\n input?: unknown\n status?: JobStatus\n },\n): Promise<JobEvent> {\n const ts = now()\n const data: Omit<JobEvent, 'id'> = {\n projectId: args.projectId,\n environmentId: args.environmentId,\n name: args.name,\n status: args.status ?? 'in_progress',\n input: args.input ?? null,\n output: null,\n error: null,\n createdAt: ts,\n updatedAt: ts,\n completedAt: null,\n }\n const doc = await createDoc(gcpProjectId, EVENTS, data)\n return { id: doc.id, ...data }\n}\n\nexport async function patchEvent(\n gcpProjectId: string,\n id: string,\n patch: { status?: JobStatus; output?: unknown; error?: string | null },\n): Promise<void> {\n const ts = now()\n const update: Record<string, unknown> = { updatedAt: ts }\n if (patch.status !== undefined) {\n update.status = patch.status\n if (patch.status === 'completed' || patch.status === 'failed') {\n update.completedAt = ts\n }\n }\n if (patch.output !== undefined) update.output = patch.output\n if (patch.error !== undefined) update.error = patch.error\n\n await patchDoc(gcpProjectId, EVENTS, id, update)\n}\n","import { createEvent, findOrCreateContext, patchEvent } from './store'\nimport type { ClientConfig, JobEvent } from './types'\n\nexport interface JobEchoClient {\n /** Record a new job event as `in_progress`. Returns the created event. */\n start(name: string, input?: unknown): Promise<JobEvent>\n /** Mark an existing event `completed`. Optionally attach a return value as `output`. */\n complete(id: string, output?: unknown): Promise<void>\n /** Mark an existing event `failed` with an error message. */\n fail(id: string, error: string, output?: unknown): Promise<void>\n}\n\nexport function createClient(cfg: ClientConfig): JobEchoClient {\n // Only write to Firestore in production. Local/dev runs are a no-op —\n // handlers still execute, just no lifecycle events recorded.\n if (process.env.NODE_ENV !== 'production') return disabledClient()\n\n const gcpProjectId =\n cfg.collectorProjectId ??\n process.env.JOB_ECHO_GCP_PROJECT ??\n process.env.FIREBASE_PROJECT_ID\n\n // No collector project configured → run as no-op. Lets services adopt the\n // worker pattern before the dashboard's Firestore is provisioned.\n if (!gcpProjectId) {\n console.warn(\n '[job-echo] no collectorProjectId / JOB_ECHO_GCP_PROJECT / FIREBASE_PROJECT_ID set — Firestore writes disabled',\n )\n return disabledClient()\n }\n\n let contextPromise: Promise<{ projectId: string; environmentId: string }> | null = null\n const context = () => {\n if (!contextPromise) {\n contextPromise = findOrCreateContext(gcpProjectId, cfg.project, cfg.environment)\n }\n return contextPromise\n }\n\n return {\n async start(name, input) {\n const { projectId, environmentId } = await context()\n return createEvent(gcpProjectId, { projectId, environmentId, name, input })\n },\n async complete(id, output) {\n await patchEvent(gcpProjectId, id, { status: 'completed', output })\n },\n async fail(id, error, output) {\n await patchEvent(gcpProjectId, id, { status: 'failed', error, output })\n },\n }\n}\n\nfunction disabledClient(): JobEchoClient {\n const ts = Date.now()\n return {\n async start(name, input) {\n return {\n id: 'disabled',\n projectId: 'disabled',\n environmentId: 'disabled',\n name,\n status: 'in_progress',\n input,\n output: null,\n error: null,\n createdAt: ts,\n updatedAt: ts,\n completedAt: null,\n }\n },\n async complete() {},\n async fail() {},\n }\n}\n","import { getAccessToken } from './auth'\n\nexport interface Dispatcher {\n dispatch(jobName: string, payload: unknown): Promise<void>\n}\n\nexport interface HttpDispatcherConfig {\n baseUrl: string\n headers?: Record<string, string>\n prefix?: string\n}\n\nexport interface CloudTasksDispatcherConfig {\n projectId: string\n location: string\n queue: string\n workerUrl: string\n serviceAccountEmail: string\n prefix?: string\n audience?: string\n}\n\n/**\n * HTTP dispatcher: plain POST to `{baseUrl}{prefix}/{jobName}`.\n */\nexport function createHttpDispatcher(cfg: HttpDispatcherConfig): Dispatcher {\n const prefix = cfg.prefix ?? '/jobs'\n return {\n async dispatch(jobName, payload) {\n const res = await fetch(`${cfg.baseUrl}${prefix}/${jobName}`, {\n method: 'POST',\n headers: { 'content-type': 'application/json', ...(cfg.headers ?? {}) },\n body: JSON.stringify(payload),\n })\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new Error(\n `job-echo dispatch '${jobName}' failed: ${res.status} ${body}`.trim(),\n )\n }\n },\n }\n}\n\n/**\n * Cloud Tasks dispatcher via the REST API — no @google-cloud/tasks SDK.\n * Enqueues an HTTP task with an OIDC token so the worker can verify\n * the caller identity.\n */\nexport function createCloudTasksDispatcher(\n cfg: CloudTasksDispatcherConfig,\n): Dispatcher {\n const prefix = cfg.prefix ?? '/jobs'\n const audience = cfg.audience ?? cfg.workerUrl\n const url = `https://cloudtasks.googleapis.com/v2/projects/${cfg.projectId}/locations/${cfg.location}/queues/${cfg.queue}/tasks`\n\n return {\n async dispatch(jobName, payload) {\n const token = await getAccessToken()\n const body = {\n task: {\n httpRequest: {\n httpMethod: 'POST',\n url: `${cfg.workerUrl}${prefix}/${jobName}`,\n headers: { 'content-type': 'application/json' },\n body: Buffer.from(JSON.stringify(payload)).toString('base64'),\n oidcToken: {\n serviceAccountEmail: cfg.serviceAccountEmail,\n audience,\n },\n },\n },\n }\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n authorization: `Bearer ${token}`,\n 'content-type': 'application/json',\n },\n body: JSON.stringify(body),\n })\n if (!res.ok) {\n const respBody = await res.text().catch(() => '')\n throw new Error(\n `job-echo Cloud Tasks enqueue '${jobName}' failed: ${res.status} ${respBody}`.trim(),\n )\n }\n },\n }\n}\n\nlet active: Dispatcher | null = null\n\n/**\n * Returns the dispatcher, auto-detected from env on first call:\n * - `NODE_ENV=production`: Cloud Tasks (requires `JOB_ECHO_TASKS_*` env vars)\n * - otherwise: HTTP to `JOB_ECHO_WORKER_URL` (default `http://localhost:8080`)\n */\nexport function getDispatcher(): Dispatcher {\n if (!active) active = autoDetectDispatcher()\n return active\n}\n\n/**\n * Parse a Cloud Tasks queue resource path:\n * `projects/{project}/locations/{location}/queues/{queue}`\n */\nfunction parseQueuePath(\n path: string,\n): { projectId: string; location: string; queue: string } {\n const m = path.match(\n /^projects\\/([^/]+)\\/locations\\/([^/]+)\\/queues\\/([^/]+)$/,\n )\n if (!m) {\n throw new Error(\n `@originalvoices/job-echo-client: JOB_ECHO_TASKS_QUEUE must be a full resource path (projects/{p}/locations/{l}/queues/{q}), got \"${path}\"`,\n )\n }\n return { projectId: m[1]!, location: m[2]!, queue: m[3]! }\n}\n\nfunction autoDetectDispatcher(): Dispatcher {\n if (process.env.NODE_ENV === 'production') {\n const required = {\n JOB_ECHO_TASKS_QUEUE: process.env.JOB_ECHO_TASKS_QUEUE,\n JOB_ECHO_WORKER_URL: process.env.JOB_ECHO_WORKER_URL,\n JOB_ECHO_TASKS_SA_EMAIL: process.env.JOB_ECHO_TASKS_SA_EMAIL,\n }\n const missing = Object.entries(required)\n .filter(([, v]) => !v)\n .map(([k]) => k)\n if (missing.length) {\n throw new Error(\n `@originalvoices/job-echo-client: Cloud Tasks dispatcher needs env vars: ${missing.join(', ')}`,\n )\n }\n const { projectId, location, queue } = parseQueuePath(\n required.JOB_ECHO_TASKS_QUEUE!,\n )\n return createCloudTasksDispatcher({\n projectId,\n location,\n queue,\n workerUrl: required.JOB_ECHO_WORKER_URL!,\n serviceAccountEmail: required.JOB_ECHO_TASKS_SA_EMAIL!,\n })\n }\n\n const baseUrl = process.env.JOB_ECHO_WORKER_URL ?? 'http://localhost:8080'\n return createHttpDispatcher({ baseUrl })\n}\n","import { getDispatcher } from './dispatcher'\nimport type { JobEchoClient } from './client'\n\nexport interface JobContext {\n /** Firestore event id for this invocation (status=in_progress). */\n eventId: string\n /** The raw job-echo client — use for nested events or custom updates. */\n echo: JobEchoClient\n}\n\nexport interface JobDefinition<Input> {\n name: string\n handler: (input: Input, ctx: JobContext) => Promise<unknown>\n /**\n * Optional cron expression. When set, `syncSchedules` will provision a\n * Cloud Scheduler job that fires this handler on the given cadence.\n */\n schedule?: string\n /** IANA timezone for `schedule`. Defaults to \"Etc/UTC\". */\n timezone?: string\n}\n\nexport interface Job<Input> extends JobDefinition<Input> {\n /**\n * Send `input` to the configured dispatcher (defaults to HTTP POST to the\n * worker). Requires `configureDispatcher({ baseUrl })` at startup.\n */\n emit(input: Input): Promise<void>\n /** Type marker — not present at runtime. */\n _input?: Input\n}\n\n/**\n * Define a typed job. Returns an object with a bound `.emit(input)` so call\n * sites don't have to know about the worker URL or shape of the HTTP call.\n *\n * ```ts\n * export const sendMessage = defineJob({\n * name: 'send-message',\n * async handler(input: { threadId: string }) { ... },\n * })\n *\n * // elsewhere, fully typed:\n * await sendMessage.emit({ threadId: 't1' })\n * ```\n */\nexport function defineJob<Input>(def: JobDefinition<Input>): Job<Input> {\n return {\n ...def,\n emit: (input) => getDispatcher().dispatch(def.name, input),\n }\n}\n\n/** Extract the input type from a Job. */\nexport type JobInput<J> = J extends Job<infer I> ? I : never\n","import { Hono } from 'hono'\nimport { serve } from '@hono/node-server'\nimport { createClient, type JobEchoClient } from './client'\nimport type { Job } from './job'\nimport { syncSchedules } from './sync'\nimport type { ClientConfig } from './types'\n\nexport interface WorkerConfig extends ClientConfig {\n jobs: Job<any>[]\n /** Route prefix for jobs. Defaults to \"/jobs\". */\n prefix?: string\n /**\n * If set, require requests to `/jobs/*` to include this exact value in\n * the `X-Worker-Secret` header. Returns 401 otherwise. In production,\n * leave unset and let Cloud Run IAM (run.invoker) gate access; this is\n * only useful for local dev or non-IAM-gated deployments.\n */\n workerSecret?: string\n /** Disable request logging on `/jobs/*`. Default: enabled. */\n log?: false\n}\n\n/**\n * Build a Hono app that:\n * - Exposes GET `/health` + `/healthz` (no auth, no logging)\n * - Optionally requires `X-Worker-Secret` on `/jobs/*` (when `workerSecret` set)\n * - Logs requests to `/jobs/*` with duration (unless `log: false`)\n * - Exposes POST `/jobs/{name}` for each job, wrapping handlers with\n * job-echo lifecycle tracking (in_progress → completed / failed)\n */\nexport function createWorker(cfg: WorkerConfig): Hono {\n const echo = createClient(cfg)\n const prefix = cfg.prefix ?? '/jobs'\n const app = new Hono()\n\n app.onError((err, c) => {\n console.error(`[job-echo] ${c.req.method} ${c.req.path} error:`, err)\n return c.json({ error: err instanceof Error ? err.message : String(err) }, 500)\n })\n\n // Health checks (before any middleware so they're always reachable)\n app.get('/health', (c) => c.text('ok'))\n app.get('/healthz', (c) => c.text('ok'))\n\n // Request logging (before auth so unauthorized hits are visible too)\n if (cfg.log !== false) {\n app.use(`${prefix}/*`, async (c, next) => {\n const start = Date.now()\n console.log(`[job-echo] --> ${c.req.method} ${c.req.path}`)\n await next()\n console.log(\n `[job-echo] <-- ${c.req.method} ${c.req.path} ${c.res.status} (${Date.now() - start}ms)`,\n )\n })\n }\n\n // Optional shared-secret auth\n if (cfg.workerSecret) {\n const secret = cfg.workerSecret\n app.use(`${prefix}/*`, async (c, next) => {\n if (c.req.header('X-Worker-Secret') !== secret) {\n return c.json({ error: 'unauthorized' }, 401)\n }\n return next()\n })\n }\n\n for (const job of cfg.jobs) {\n app.post(`${prefix}/${job.name}`, async (c) => {\n const input = (await c.req.json().catch(() => ({}))) as unknown\n const event = await echo.start(job.name, input)\n try {\n const result = await job.handler(input as any, { eventId: event.id, echo })\n await echo.complete(event.id, result ?? undefined)\n return c.json({ ok: true, eventId: event.id, result: result ?? null })\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n await echo.fail(event.id, message)\n return c.json({ ok: false, eventId: event.id, error: message }, 500)\n }\n })\n }\n\n return app\n}\n\n/**\n * If `JOB_ECHO_SYNC_MODE=1` is set, fire schedule sync (which will\n * `process.exit(0)` on success / `1` on failure) and return `true` so\n * the caller knows to skip normal startup. Otherwise returns `false`.\n *\n * Synchronous return so it composes naturally in CJS entrypoints\n * (no top-level `await` needed).\n *\n * ```ts\n * if (runScheduleSyncIfRequested(allJobs)) {\n * // sync running; don't start server\n * } else {\n * startServer()\n * }\n * ```\n */\nexport function runScheduleSyncIfRequested<\n J extends { name: string; schedule?: string; timezone?: string },\n>(jobs: J[]): boolean {\n if (process.env.JOB_ECHO_SYNC_MODE !== '1') return false\n\n syncSchedules(jobs)\n .then((report) => {\n console.log(\n '[job-echo] schedule sync complete:\\n' + JSON.stringify(report, null, 2),\n )\n process.exit(0)\n })\n .catch((err) => {\n console.error('[job-echo] schedule sync failed:', err)\n process.exit(1)\n })\n\n return true\n}\n\n/**\n * Convenience: build a worker and start the HTTP server on `port`\n * (defaults to `PORT` env or 8080). Also handles `JOB_ECHO_SYNC_MODE=1`\n * via `runScheduleSyncIfRequested` — same binary, two modes.\n */\nexport function startWorker(\n cfg: WorkerConfig & { port?: number },\n): ReturnType<typeof serve> | void {\n if (process.env.JOB_ECHO_SYNC_MODE === '1') {\n runScheduleSyncIfRequested(cfg.jobs)\n return\n }\n\n const app = createWorker(cfg)\n const port = cfg.port ?? Number(process.env.PORT ?? 8080)\n console.log(`[job-echo] worker listening on :${port}`)\n return serve({ fetch: app.fetch, port })\n}\n","import { getAccessToken } from './auth'\n\nconst BASE = 'https://cloudscheduler.googleapis.com/v1'\n\nexport interface SyncReport {\n created: string[]\n updated: string[]\n deleted: string[]\n unchanged: string[]\n}\n\ninterface ParsedQueue {\n projectId: string\n location: string\n queue: string\n fullPath: string\n}\n\nfunction parseQueuePath(path: string): ParsedQueue {\n const m = path.match(\n /^projects\\/([^/]+)\\/locations\\/([^/]+)\\/queues\\/([^/]+)$/,\n )\n if (!m) {\n throw new Error(\n `@originalvoices/job-echo-client: JOB_ECHO_TASKS_QUEUE must be a full resource path (projects/{p}/locations/{l}/queues/{q}), got \"${path}\"`,\n )\n }\n return { projectId: m[1]!, location: m[2]!, queue: m[3]!, fullPath: path }\n}\n\nfunction sanitizeName(name: string): string {\n return name.replace(/[^a-zA-Z0-9-]/g, '-').replace(/^-+|-+$/g, '')\n}\n\nasync function authed(url: string, init?: RequestInit): Promise<Response> {\n const token = await getAccessToken()\n const res = await fetch(url, {\n ...init,\n headers: {\n ...(init?.headers ?? {}),\n authorization: `Bearer ${token}`,\n 'content-type': 'application/json',\n },\n })\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new Error(`Cloud Scheduler ${res.status}: ${body}`)\n }\n return res\n}\n\ninterface SchedulerJob {\n name: string\n schedule: string\n timeZone: string\n httpTarget: {\n uri: string\n httpMethod: string\n headers?: Record<string, string>\n body?: string\n oauthToken?: {\n serviceAccountEmail: string\n scope?: string\n }\n }\n labels?: Record<string, string>\n}\n\nfunction buildSchedulerJob(args: {\n fullName: string\n cron: string\n timezone: string\n queue: ParsedQueue\n workerUrl: string\n invokerSa: string\n jobName: string\n appLabel: string\n}): SchedulerJob {\n const task = {\n httpRequest: {\n url: `${args.workerUrl}/jobs/${args.jobName}`,\n httpMethod: 'POST',\n headers: { 'content-type': 'application/json' },\n body: Buffer.from('{}').toString('base64'),\n oidcToken: {\n serviceAccountEmail: args.invokerSa,\n audience: args.workerUrl,\n },\n },\n }\n\n return {\n name: args.fullName,\n schedule: args.cron,\n timeZone: args.timezone,\n httpTarget: {\n uri: `https://cloudtasks.googleapis.com/v2/${args.queue.fullPath}/tasks`,\n httpMethod: 'POST',\n headers: { 'content-type': 'application/json' },\n body: Buffer.from(JSON.stringify({ task })).toString('base64'),\n oauthToken: {\n serviceAccountEmail: args.invokerSa,\n scope: 'https://www.googleapis.com/auth/cloud-platform',\n },\n },\n labels: {\n 'managed-by': 'job-echo',\n app: args.appLabel,\n job: args.jobName,\n },\n }\n}\n\nasync function listManaged(\n queue: ParsedQueue,\n appLabel: string,\n): Promise<SchedulerJob[]> {\n const filter = `labels.managed-by=\"job-echo\" AND labels.app=\"${appLabel}\"`\n const url = `${BASE}/projects/${queue.projectId}/locations/${queue.location}/jobs?filter=${encodeURIComponent(filter)}`\n const res = await authed(url)\n const body = (await res.json()) as { jobs?: SchedulerJob[] }\n return body.jobs ?? []\n}\n\nfunction differs(a: SchedulerJob, b: SchedulerJob): boolean {\n return (\n a.schedule !== b.schedule ||\n a.timeZone !== b.timeZone ||\n a.httpTarget.uri !== b.httpTarget.uri ||\n a.httpTarget.body !== b.httpTarget.body\n )\n}\n\n/**\n * Reconcile Cloud Scheduler jobs to match the schedules declared on the\n * given jobs. Only touches schedulers labelled `managed-by=job-echo` and\n * `app=<JOB_ECHO_APP_LABEL>` — manually created schedulers are untouched.\n *\n * Required env:\n * - JOB_ECHO_TASKS_QUEUE (full resource path: projects/.../queues/...)\n * - JOB_ECHO_WORKER_URL (https URL of the worker service)\n * - JOB_ECHO_TASKS_SA_EMAIL (SA the scheduler-fired tasks run as)\n * - JOB_ECHO_APP_LABEL (e.g. \"platform-prod\" — scopes the sync)\n */\nexport async function syncSchedules<\n J extends { name: string; schedule?: string; timezone?: string },\n>(jobs: J[]): Promise<SyncReport> {\n const env = {\n JOB_ECHO_TASKS_QUEUE: process.env.JOB_ECHO_TASKS_QUEUE,\n JOB_ECHO_WORKER_URL: process.env.JOB_ECHO_WORKER_URL,\n JOB_ECHO_TASKS_SA_EMAIL: process.env.JOB_ECHO_TASKS_SA_EMAIL,\n JOB_ECHO_APP_LABEL: process.env.JOB_ECHO_APP_LABEL,\n }\n const missing = Object.entries(env)\n .filter(([, v]) => !v)\n .map(([k]) => k)\n if (missing.length) {\n throw new Error(\n `@originalvoices/job-echo-client: syncSchedules needs env vars: ${missing.join(', ')}`,\n )\n }\n\n const queue = parseQueuePath(env.JOB_ECHO_TASKS_QUEUE!)\n const workerUrl = env.JOB_ECHO_WORKER_URL!\n const invokerSa = env.JOB_ECHO_TASKS_SA_EMAIL!\n const appLabel = env.JOB_ECHO_APP_LABEL!\n\n const scheduled = jobs.filter((j) => Boolean(j.schedule))\n\n const desired = new Map<string, SchedulerJob>()\n for (const job of scheduled) {\n const schedulerName = sanitizeName(`${appLabel}-${job.name}`)\n const fullName = `projects/${queue.projectId}/locations/${queue.location}/jobs/${schedulerName}`\n desired.set(\n fullName,\n buildSchedulerJob({\n fullName,\n cron: job.schedule!,\n timezone: job.timezone ?? 'Etc/UTC',\n queue,\n workerUrl,\n invokerSa,\n jobName: job.name,\n appLabel,\n }),\n )\n }\n\n const existing = await listManaged(queue, appLabel)\n const existingByName = new Map(existing.map((j) => [j.name, j]))\n\n const report: SyncReport = {\n created: [],\n updated: [],\n deleted: [],\n unchanged: [],\n }\n\n for (const [name, want] of desired) {\n const have = existingByName.get(name)\n if (!have) {\n const url = `${BASE}/projects/${queue.projectId}/locations/${queue.location}/jobs`\n await authed(url, { method: 'POST', body: JSON.stringify(want) })\n report.created.push(name)\n } else if (differs(have, want)) {\n const url = `${BASE}/${name}?updateMask=schedule,timeZone,httpTarget,labels`\n await authed(url, { method: 'PATCH', body: JSON.stringify(want) })\n report.updated.push(name)\n } else {\n report.unchanged.push(name)\n }\n }\n\n for (const name of existingByName.keys()) {\n if (!desired.has(name)) {\n await authed(`${BASE}/${name}`, { method: 'DELETE' })\n report.deleted.push(name)\n }\n }\n\n return report\n}\n"],"mappings":";AAAA,IAAM,eACJ;AAQF,IAAI,SAAsD;AAU1D,eAAsB,iBAAkC;AACtD,QAAMA,OAAM,KAAK,IAAI;AACrB,MAAI,UAAU,OAAO,YAAYA,OAAM,IAAQ,QAAO,OAAO;AAE7D,QAAM,MAAM,MAAM,MAAM,cAAc,EAAE,SAAS,EAAE,mBAAmB,SAAS,EAAE,CAAC;AAClF,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,IAAI;AAAA,MACR,6EAA6E,IAAI,MAAM;AAAA,IACzF;AAAA,EACF;AACA,QAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,WAAS;AAAA,IACP,OAAO,KAAK;AAAA,IACZ,WAAWA,OAAM,KAAK,aAAa;AAAA,EACrC;AACA,SAAO,OAAO;AAChB;;;AC1BA,IAAM,OAAO;AAEb,SAAS,QAAQ,WAAmB;AAClC,SAAO,GAAG,IAAI,aAAa,SAAS;AACtC;AAEA,eAAe,OAAO,KAAa,MAAuC;AACxE,QAAM,QAAQ,MAAM,eAAe;AACnC,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B,GAAG;AAAA,IACH,SAAS;AAAA,MACP,GAAI,MAAM,WAAW,CAAC;AAAA,MACtB,eAAe,UAAU,KAAK;AAAA,MAC9B,gBAAgB;AAAA,IAClB;AAAA,EACF,CAAC;AACD,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,EAAE;AAC5C,UAAM,IAAI,MAAM,kBAAkB,IAAI,MAAM,KAAK,IAAI,EAAE;AAAA,EACzD;AACA,SAAO;AACT;AAcO,SAAS,YAAY,GAAqB;AAC/C,MAAI,MAAM,QAAQ,MAAM,OAAW,QAAO,EAAE,WAAW,KAAK;AAC5D,MAAI,OAAO,MAAM,SAAU,QAAO,EAAE,aAAa,EAAE;AACnD,MAAI,OAAO,MAAM,UAAW,QAAO,EAAE,cAAc,EAAE;AACrD,MAAI,OAAO,MAAM,UAAU;AACzB,WAAO,OAAO,UAAU,CAAC,IACrB,EAAE,cAAc,OAAO,CAAC,EAAE,IAC1B,EAAE,aAAa,EAAE;AAAA,EACvB;AACA,MAAI,MAAM,QAAQ,CAAC,GAAG;AACpB,WAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,WAAW,EAAE,EAAE;AAAA,EACtD;AACA,MAAI,OAAO,MAAM,UAAU;AACzB,WAAO,EAAE,UAAU,EAAE,QAAQ,aAAa,CAA4B,EAAE,EAAE;AAAA,EAC5E;AAEA,SAAO,EAAE,aAAa,OAAO,CAAC,EAAE;AAClC;AAEO,SAAS,aAAa,KAAuD;AAClF,QAAM,MAA+B,CAAC;AACtC,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,GAAG,EAAG,KAAI,CAAC,IAAI,YAAY,CAAC;AAChE,SAAO;AACT;AAEO,SAAS,YAAY,GAAqB;AAC/C,MAAI,iBAAiB,EAAG,QAAO,EAAE;AACjC,MAAI,kBAAkB,EAAG,QAAO,OAAO,EAAE,YAAY;AACrD,MAAI,iBAAiB,EAAG,QAAO,EAAE;AACjC,MAAI,kBAAkB,EAAG,QAAO,EAAE;AAClC,MAAI,eAAe,EAAG,QAAO;AAC7B,MAAI,oBAAoB,EAAG,QAAO,EAAE;AACpC,MAAI,cAAc,EAAG,QAAO,aAAa,EAAE,SAAS,UAAU,CAAC,CAAC;AAChE,MAAI,gBAAgB,EAAG,SAAQ,EAAE,WAAW,UAAU,CAAC,GAAG,IAAI,WAAW;AACzE,SAAO;AACT;AAEO,SAAS,aACd,QACyB;AACzB,QAAM,MAA+B,CAAC;AACtC,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,MAAM,EAAG,KAAI,CAAC,IAAI,YAAY,CAAC;AACnE,SAAO;AACT;AAIA,SAAS,cAAc,MAAsB;AAE3C,SAAO,KAAK,MAAM,GAAG,EAAE,IAAI,KAAK;AAClC;AAOA,eAAsB,UACpB,WACA,YACA,MACuB;AACvB,QAAM,MAAM,MAAM,OAAO,GAAG,QAAQ,SAAS,CAAC,IAAI,UAAU,IAAI;AAAA,IAC9D,QAAQ;AAAA,IACR,MAAM,KAAK,UAAU,EAAE,QAAQ,aAAa,IAAI,EAAE,CAAC;AAAA,EACrD,CAAC;AACD,QAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,SAAO,EAAE,IAAI,cAAc,KAAK,IAAI,GAAG,MAAM,aAAa,KAAK,UAAU,CAAC,CAAC,EAAE;AAC/E;AAEA,eAAsB,SACpB,WACA,YACA,IACA,OACe;AACf,QAAM,SAAS,OAAO,KAAK,KAAK,EAC7B,IAAI,CAAC,MAAM,yBAAyB,mBAAmB,CAAC,CAAC,EAAE,EAC3D,KAAK,GAAG;AACX,QAAM,OAAO,GAAG,QAAQ,SAAS,CAAC,IAAI,UAAU,IAAI,EAAE,IAAI,MAAM,IAAI;AAAA,IAClE,QAAQ;AAAA,IACR,MAAM,KAAK,UAAU,EAAE,QAAQ,aAAa,KAAK,EAAE,CAAC;AAAA,EACtD,CAAC;AACH;AAMA,eAAsB,UACpB,WACA,YACA,SACA,QAAQ,GACiB;AACzB,QAAM,kBAAkB;AAAA,IACtB,iBAAiB;AAAA,MACf,IAAI;AAAA,MACJ,SAAS,QAAQ,IAAI,CAAC,OAAO;AAAA,QAC3B,aAAa;AAAA,UACX,OAAO,EAAE,WAAW,EAAE,MAAM;AAAA,UAC5B,IAAI;AAAA,UACJ,OAAO,EAAE,aAAa,EAAE,MAAM;AAAA,QAChC;AAAA,MACF,EAAE;AAAA,IACJ;AAAA,EACF;AAEA,QAAM,kBAAkB;AAAA,IACtB,MAAM,CAAC,EAAE,cAAc,WAAW,CAAC;AAAA,IACnC,OAAO,QAAQ,SAAS,kBAAkB;AAAA,IAC1C;AAAA,EACF;AAEA,QAAM,MAAM,MAAM,OAAO,GAAG,QAAQ,SAAS,CAAC,aAAa;AAAA,IACzD,QAAQ;AAAA,IACR,MAAM,KAAK,UAAU,EAAE,gBAAgB,CAAC;AAAA,EAC1C,CAAC;AACD,QAAM,OAAQ,MAAM,IAAI,KAAK;AAG7B,SAAO,KACJ,OAAO,CAAC,MAAM,EAAE,QAAQ,EACxB,IAAI,CAAC,OAAO;AAAA,IACX,IAAI,cAAc,EAAE,SAAU,IAAI;AAAA,IAClC,MAAM,aAAa,EAAE,SAAU,UAAU,CAAC,CAAC;AAAA,EAC7C,EAAE;AACN;;;ACxKA,IAAM,WAAW;AACjB,IAAM,eAAe;AACrB,IAAM,SAAS;AAEf,IAAM,MAAM,MAAM,KAAK,IAAI;AAE3B,eAAsB,oBACpB,cACA,aACA,SACuD;AACvD,QAAM,cAAc,MAAM;AAAA,IACxB;AAAA,IACA;AAAA,IACA,CAAC,EAAE,OAAO,QAAQ,OAAO,YAAY,CAAC;AAAA,IACtC;AAAA,EACF;AACA,QAAM,YAAY,YAAY,CAAC,IAC3B,YAAY,CAAC,EAAE,MACd,MAAM,UAAU,cAAc,UAAU,EAAE,MAAM,aAAa,WAAW,IAAI,EAAE,CAAC,GAAG;AAEvF,QAAM,aAAa,MAAM;AAAA,IACvB;AAAA,IACA;AAAA,IACA;AAAA,MACE,EAAE,OAAO,aAAa,OAAO,UAAU;AAAA,MACvC,EAAE,OAAO,QAAQ,OAAO,QAAQ;AAAA,IAClC;AAAA,IACA;AAAA,EACF;AACA,QAAM,gBAAgB,WAAW,CAAC,IAC9B,WAAW,CAAC,EAAE,MAEZ,MAAM,UAAU,cAAc,cAAc;AAAA,IAC1C;AAAA,IACA,MAAM;AAAA,IACN,WAAW,IAAI;AAAA,EACjB,CAAC,GACD;AAEN,SAAO,EAAE,WAAW,cAAc;AACpC;AAEA,eAAsB,YACpB,cACA,MAOmB;AACnB,QAAM,KAAK,IAAI;AACf,QAAM,OAA6B;AAAA,IACjC,WAAW,KAAK;AAAA,IAChB,eAAe,KAAK;AAAA,IACpB,MAAM,KAAK;AAAA,IACX,QAAQ,KAAK,UAAU;AAAA,IACvB,OAAO,KAAK,SAAS;AAAA,IACrB,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,WAAW;AAAA,IACX,WAAW;AAAA,IACX,aAAa;AAAA,EACf;AACA,QAAM,MAAM,MAAM,UAAU,cAAc,QAAQ,IAAI;AACtD,SAAO,EAAE,IAAI,IAAI,IAAI,GAAG,KAAK;AAC/B;AAEA,eAAsB,WACpB,cACA,IACA,OACe;AACf,QAAM,KAAK,IAAI;AACf,QAAM,SAAkC,EAAE,WAAW,GAAG;AACxD,MAAI,MAAM,WAAW,QAAW;AAC9B,WAAO,SAAS,MAAM;AACtB,QAAI,MAAM,WAAW,eAAe,MAAM,WAAW,UAAU;AAC7D,aAAO,cAAc;AAAA,IACvB;AAAA,EACF;AACA,MAAI,MAAM,WAAW,OAAW,QAAO,SAAS,MAAM;AACtD,MAAI,MAAM,UAAU,OAAW,QAAO,QAAQ,MAAM;AAEpD,QAAM,SAAS,cAAc,QAAQ,IAAI,MAAM;AACjD;;;AC9EO,SAAS,aAAa,KAAkC;AAG7D,MAAI,QAAQ,IAAI,aAAa,aAAc,QAAO,eAAe;AAEjE,QAAM,eACJ,IAAI,sBACJ,QAAQ,IAAI,wBACZ,QAAQ,IAAI;AAId,MAAI,CAAC,cAAc;AACjB,YAAQ;AAAA,MACN;AAAA,IACF;AACA,WAAO,eAAe;AAAA,EACxB;AAEA,MAAI,iBAA+E;AACnF,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,gBAAgB;AACnB,uBAAiB,oBAAoB,cAAc,IAAI,SAAS,IAAI,WAAW;AAAA,IACjF;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,MAAM,MAAM,MAAM,OAAO;AACvB,YAAM,EAAE,WAAW,cAAc,IAAI,MAAM,QAAQ;AACnD,aAAO,YAAY,cAAc,EAAE,WAAW,eAAe,MAAM,MAAM,CAAC;AAAA,IAC5E;AAAA,IACA,MAAM,SAAS,IAAI,QAAQ;AACzB,YAAM,WAAW,cAAc,IAAI,EAAE,QAAQ,aAAa,OAAO,CAAC;AAAA,IACpE;AAAA,IACA,MAAM,KAAK,IAAI,OAAO,QAAQ;AAC5B,YAAM,WAAW,cAAc,IAAI,EAAE,QAAQ,UAAU,OAAO,OAAO,CAAC;AAAA,IACxE;AAAA,EACF;AACF;AAEA,SAAS,iBAAgC;AACvC,QAAM,KAAK,KAAK,IAAI;AACpB,SAAO;AAAA,IACL,MAAM,MAAM,MAAM,OAAO;AACvB,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,WAAW;AAAA,QACX,eAAe;AAAA,QACf;AAAA,QACA,QAAQ;AAAA,QACR;AAAA,QACA,QAAQ;AAAA,QACR,OAAO;AAAA,QACP,WAAW;AAAA,QACX,WAAW;AAAA,QACX,aAAa;AAAA,MACf;AAAA,IACF;AAAA,IACA,MAAM,WAAW;AAAA,IAAC;AAAA,IAClB,MAAM,OAAO;AAAA,IAAC;AAAA,EAChB;AACF;;;ACjDO,SAAS,qBAAqB,KAAuC;AAC1E,QAAM,SAAS,IAAI,UAAU;AAC7B,SAAO;AAAA,IACL,MAAM,SAAS,SAAS,SAAS;AAC/B,YAAM,MAAM,MAAM,MAAM,GAAG,IAAI,OAAO,GAAG,MAAM,IAAI,OAAO,IAAI;AAAA,QAC5D,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,oBAAoB,GAAI,IAAI,WAAW,CAAC,EAAG;AAAA,QACtE,MAAM,KAAK,UAAU,OAAO;AAAA,MAC9B,CAAC;AACD,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,EAAE;AAC5C,cAAM,IAAI;AAAA,UACR,sBAAsB,OAAO,aAAa,IAAI,MAAM,IAAI,IAAI,GAAG,KAAK;AAAA,QACtE;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAOO,SAAS,2BACd,KACY;AACZ,QAAM,SAAS,IAAI,UAAU;AAC7B,QAAM,WAAW,IAAI,YAAY,IAAI;AACrC,QAAM,MAAM,iDAAiD,IAAI,SAAS,cAAc,IAAI,QAAQ,WAAW,IAAI,KAAK;AAExH,SAAO;AAAA,IACL,MAAM,SAAS,SAAS,SAAS;AAC/B,YAAM,QAAQ,MAAM,eAAe;AACnC,YAAM,OAAO;AAAA,QACX,MAAM;AAAA,UACJ,aAAa;AAAA,YACX,YAAY;AAAA,YACZ,KAAK,GAAG,IAAI,SAAS,GAAG,MAAM,IAAI,OAAO;AAAA,YACzC,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,YAC9C,MAAM,OAAO,KAAK,KAAK,UAAU,OAAO,CAAC,EAAE,SAAS,QAAQ;AAAA,YAC5D,WAAW;AAAA,cACT,qBAAqB,IAAI;AAAA,cACzB;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AACA,YAAM,MAAM,MAAM,MAAM,KAAK;AAAA,QAC3B,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,eAAe,UAAU,KAAK;AAAA,UAC9B,gBAAgB;AAAA,QAClB;AAAA,QACA,MAAM,KAAK,UAAU,IAAI;AAAA,MAC3B,CAAC;AACD,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,WAAW,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,EAAE;AAChD,cAAM,IAAI;AAAA,UACR,iCAAiC,OAAO,aAAa,IAAI,MAAM,IAAI,QAAQ,GAAG,KAAK;AAAA,QACrF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAI,SAA4B;AAOzB,SAAS,gBAA4B;AAC1C,MAAI,CAAC,OAAQ,UAAS,qBAAqB;AAC3C,SAAO;AACT;AAMA,SAAS,eACP,MACwD;AACxD,QAAM,IAAI,KAAK;AAAA,IACb;AAAA,EACF;AACA,MAAI,CAAC,GAAG;AACN,UAAM,IAAI;AAAA,MACR,oIAAoI,IAAI;AAAA,IAC1I;AAAA,EACF;AACA,SAAO,EAAE,WAAW,EAAE,CAAC,GAAI,UAAU,EAAE,CAAC,GAAI,OAAO,EAAE,CAAC,EAAG;AAC3D;AAEA,SAAS,uBAAmC;AAC1C,MAAI,QAAQ,IAAI,aAAa,cAAc;AACzC,UAAM,WAAW;AAAA,MACf,sBAAsB,QAAQ,IAAI;AAAA,MAClC,qBAAqB,QAAQ,IAAI;AAAA,MACjC,yBAAyB,QAAQ,IAAI;AAAA,IACvC;AACA,UAAM,UAAU,OAAO,QAAQ,QAAQ,EACpC,OAAO,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,EACpB,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC;AACjB,QAAI,QAAQ,QAAQ;AAClB,YAAM,IAAI;AAAA,QACR,2EAA2E,QAAQ,KAAK,IAAI,CAAC;AAAA,MAC/F;AAAA,IACF;AACA,UAAM,EAAE,WAAW,UAAU,MAAM,IAAI;AAAA,MACrC,SAAS;AAAA,IACX;AACA,WAAO,2BAA2B;AAAA,MAChC;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW,SAAS;AAAA,MACpB,qBAAqB,SAAS;AAAA,IAChC,CAAC;AAAA,EACH;AAEA,QAAMC,WAAU,QAAQ,IAAI,uBAAuB;AACnD,SAAO,qBAAqB,EAAE,SAAAA,SAAQ,CAAC;AACzC;;;ACxGO,SAAS,UAAiB,KAAuC;AACtE,SAAO;AAAA,IACL,GAAG;AAAA,IACH,MAAM,CAAC,UAAU,cAAc,EAAE,SAAS,IAAI,MAAM,KAAK;AAAA,EAC3D;AACF;;;ACnDA,SAAS,YAAY;AACrB,SAAS,aAAa;;;ACCtB,IAAMC,QAAO;AAgBb,SAASC,gBAAe,MAA2B;AACjD,QAAM,IAAI,KAAK;AAAA,IACb;AAAA,EACF;AACA,MAAI,CAAC,GAAG;AACN,UAAM,IAAI;AAAA,MACR,oIAAoI,IAAI;AAAA,IAC1I;AAAA,EACF;AACA,SAAO,EAAE,WAAW,EAAE,CAAC,GAAI,UAAU,EAAE,CAAC,GAAI,OAAO,EAAE,CAAC,GAAI,UAAU,KAAK;AAC3E;AAEA,SAAS,aAAa,MAAsB;AAC1C,SAAO,KAAK,QAAQ,kBAAkB,GAAG,EAAE,QAAQ,YAAY,EAAE;AACnE;AAEA,eAAeC,QAAO,KAAa,MAAuC;AACxE,QAAM,QAAQ,MAAM,eAAe;AACnC,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B,GAAG;AAAA,IACH,SAAS;AAAA,MACP,GAAI,MAAM,WAAW,CAAC;AAAA,MACtB,eAAe,UAAU,KAAK;AAAA,MAC9B,gBAAgB;AAAA,IAClB;AAAA,EACF,CAAC;AACD,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,EAAE;AAC5C,UAAM,IAAI,MAAM,mBAAmB,IAAI,MAAM,KAAK,IAAI,EAAE;AAAA,EAC1D;AACA,SAAO;AACT;AAmBA,SAAS,kBAAkB,MASV;AACf,QAAM,OAAO;AAAA,IACX,aAAa;AAAA,MACX,KAAK,GAAG,KAAK,SAAS,SAAS,KAAK,OAAO;AAAA,MAC3C,YAAY;AAAA,MACZ,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,OAAO,KAAK,IAAI,EAAE,SAAS,QAAQ;AAAA,MACzC,WAAW;AAAA,QACT,qBAAqB,KAAK;AAAA,QAC1B,UAAU,KAAK;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM,KAAK;AAAA,IACX,UAAU,KAAK;AAAA,IACf,UAAU,KAAK;AAAA,IACf,YAAY;AAAA,MACV,KAAK,wCAAwC,KAAK,MAAM,QAAQ;AAAA,MAChE,YAAY;AAAA,MACZ,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,OAAO,KAAK,KAAK,UAAU,EAAE,KAAK,CAAC,CAAC,EAAE,SAAS,QAAQ;AAAA,MAC7D,YAAY;AAAA,QACV,qBAAqB,KAAK;AAAA,QAC1B,OAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA,QAAQ;AAAA,MACN,cAAc;AAAA,MACd,KAAK,KAAK;AAAA,MACV,KAAK,KAAK;AAAA,IACZ;AAAA,EACF;AACF;AAEA,eAAe,YACb,OACA,UACyB;AACzB,QAAM,SAAS,gDAAgD,QAAQ;AACvE,QAAM,MAAM,GAAGF,KAAI,aAAa,MAAM,SAAS,cAAc,MAAM,QAAQ,gBAAgB,mBAAmB,MAAM,CAAC;AACrH,QAAM,MAAM,MAAME,QAAO,GAAG;AAC5B,QAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,SAAO,KAAK,QAAQ,CAAC;AACvB;AAEA,SAAS,QAAQ,GAAiB,GAA0B;AAC1D,SACE,EAAE,aAAa,EAAE,YACjB,EAAE,aAAa,EAAE,YACjB,EAAE,WAAW,QAAQ,EAAE,WAAW,OAClC,EAAE,WAAW,SAAS,EAAE,WAAW;AAEvC;AAaA,eAAsB,cAEpB,MAAgC;AAChC,QAAM,MAAM;AAAA,IACV,sBAAsB,QAAQ,IAAI;AAAA,IAClC,qBAAqB,QAAQ,IAAI;AAAA,IACjC,yBAAyB,QAAQ,IAAI;AAAA,IACrC,oBAAoB,QAAQ,IAAI;AAAA,EAClC;AACA,QAAM,UAAU,OAAO,QAAQ,GAAG,EAC/B,OAAO,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,EACpB,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC;AACjB,MAAI,QAAQ,QAAQ;AAClB,UAAM,IAAI;AAAA,MACR,kEAAkE,QAAQ,KAAK,IAAI,CAAC;AAAA,IACtF;AAAA,EACF;AAEA,QAAM,QAAQD,gBAAe,IAAI,oBAAqB;AACtD,QAAM,YAAY,IAAI;AACtB,QAAM,YAAY,IAAI;AACtB,QAAM,WAAW,IAAI;AAErB,QAAM,YAAY,KAAK,OAAO,CAAC,MAAM,QAAQ,EAAE,QAAQ,CAAC;AAExD,QAAM,UAAU,oBAAI,IAA0B;AAC9C,aAAW,OAAO,WAAW;AAC3B,UAAM,gBAAgB,aAAa,GAAG,QAAQ,IAAI,IAAI,IAAI,EAAE;AAC5D,UAAM,WAAW,YAAY,MAAM,SAAS,cAAc,MAAM,QAAQ,SAAS,aAAa;AAC9F,YAAQ;AAAA,MACN;AAAA,MACA,kBAAkB;AAAA,QAChB;AAAA,QACA,MAAM,IAAI;AAAA,QACV,UAAU,IAAI,YAAY;AAAA,QAC1B;AAAA,QACA;AAAA,QACA;AAAA,QACA,SAAS,IAAI;AAAA,QACb;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAEA,QAAM,WAAW,MAAM,YAAY,OAAO,QAAQ;AAClD,QAAM,iBAAiB,IAAI,IAAI,SAAS,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AAE/D,QAAM,SAAqB;AAAA,IACzB,SAAS,CAAC;AAAA,IACV,SAAS,CAAC;AAAA,IACV,SAAS,CAAC;AAAA,IACV,WAAW,CAAC;AAAA,EACd;AAEA,aAAW,CAAC,MAAM,IAAI,KAAK,SAAS;AAClC,UAAM,OAAO,eAAe,IAAI,IAAI;AACpC,QAAI,CAAC,MAAM;AACT,YAAM,MAAM,GAAGD,KAAI,aAAa,MAAM,SAAS,cAAc,MAAM,QAAQ;AAC3E,YAAME,QAAO,KAAK,EAAE,QAAQ,QAAQ,MAAM,KAAK,UAAU,IAAI,EAAE,CAAC;AAChE,aAAO,QAAQ,KAAK,IAAI;AAAA,IAC1B,WAAW,QAAQ,MAAM,IAAI,GAAG;AAC9B,YAAM,MAAM,GAAGF,KAAI,IAAI,IAAI;AAC3B,YAAME,QAAO,KAAK,EAAE,QAAQ,SAAS,MAAM,KAAK,UAAU,IAAI,EAAE,CAAC;AACjE,aAAO,QAAQ,KAAK,IAAI;AAAA,IAC1B,OAAO;AACL,aAAO,UAAU,KAAK,IAAI;AAAA,IAC5B;AAAA,EACF;AAEA,aAAW,QAAQ,eAAe,KAAK,GAAG;AACxC,QAAI,CAAC,QAAQ,IAAI,IAAI,GAAG;AACtB,YAAMA,QAAO,GAAGF,KAAI,IAAI,IAAI,IAAI,EAAE,QAAQ,SAAS,CAAC;AACpD,aAAO,QAAQ,KAAK,IAAI;AAAA,IAC1B;AAAA,EACF;AAEA,SAAO;AACT;;;AD/LO,SAAS,aAAa,KAAyB;AACpD,QAAM,OAAO,aAAa,GAAG;AAC7B,QAAM,SAAS,IAAI,UAAU;AAC7B,QAAM,MAAM,IAAI,KAAK;AAErB,MAAI,QAAQ,CAAC,KAAK,MAAM;AACtB,YAAQ,MAAM,cAAc,EAAE,IAAI,MAAM,IAAI,EAAE,IAAI,IAAI,WAAW,GAAG;AACpE,WAAO,EAAE,KAAK,EAAE,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,GAAG,GAAG;AAAA,EAChF,CAAC;AAGD,MAAI,IAAI,WAAW,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC;AACtC,MAAI,IAAI,YAAY,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC;AAGvC,MAAI,IAAI,QAAQ,OAAO;AACrB,QAAI,IAAI,GAAG,MAAM,MAAM,OAAO,GAAG,SAAS;AACxC,YAAM,QAAQ,KAAK,IAAI;AACvB,cAAQ,IAAI,kBAAkB,EAAE,IAAI,MAAM,IAAI,EAAE,IAAI,IAAI,EAAE;AAC1D,YAAM,KAAK;AACX,cAAQ;AAAA,QACN,kBAAkB,EAAE,IAAI,MAAM,IAAI,EAAE,IAAI,IAAI,IAAI,EAAE,IAAI,MAAM,KAAK,KAAK,IAAI,IAAI,KAAK;AAAA,MACrF;AAAA,IACF,CAAC;AAAA,EACH;AAGA,MAAI,IAAI,cAAc;AACpB,UAAM,SAAS,IAAI;AACnB,QAAI,IAAI,GAAG,MAAM,MAAM,OAAO,GAAG,SAAS;AACxC,UAAI,EAAE,IAAI,OAAO,iBAAiB,MAAM,QAAQ;AAC9C,eAAO,EAAE,KAAK,EAAE,OAAO,eAAe,GAAG,GAAG;AAAA,MAC9C;AACA,aAAO,KAAK;AAAA,IACd,CAAC;AAAA,EACH;AAEA,aAAW,OAAO,IAAI,MAAM;AAC1B,QAAI,KAAK,GAAG,MAAM,IAAI,IAAI,IAAI,IAAI,OAAO,MAAM;AAC7C,YAAM,QAAS,MAAM,EAAE,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AAClD,YAAM,QAAQ,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK;AAC9C,UAAI;AACF,cAAM,SAAS,MAAM,IAAI,QAAQ,OAAc,EAAE,SAAS,MAAM,IAAI,KAAK,CAAC;AAC1E,cAAM,KAAK,SAAS,MAAM,IAAI,UAAU,MAAS;AACjD,eAAO,EAAE,KAAK,EAAE,IAAI,MAAM,SAAS,MAAM,IAAI,QAAQ,UAAU,KAAK,CAAC;AAAA,MACvE,SAAS,KAAK;AACZ,cAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,cAAM,KAAK,KAAK,MAAM,IAAI,OAAO;AACjC,eAAO,EAAE,KAAK,EAAE,IAAI,OAAO,SAAS,MAAM,IAAI,OAAO,QAAQ,GAAG,GAAG;AAAA,MACrE;AAAA,IACF,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAkBO,SAAS,2BAEd,MAAoB;AACpB,MAAI,QAAQ,IAAI,uBAAuB,IAAK,QAAO;AAEnD,gBAAc,IAAI,EACf,KAAK,CAAC,WAAW;AAChB,YAAQ;AAAA,MACN,yCAAyC,KAAK,UAAU,QAAQ,MAAM,CAAC;AAAA,IACzE;AACA,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC,EACA,MAAM,CAAC,QAAQ;AACd,YAAQ,MAAM,oCAAoC,GAAG;AACrD,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AAEH,SAAO;AACT;AAOO,SAAS,YACd,KACiC;AACjC,MAAI,QAAQ,IAAI,uBAAuB,KAAK;AAC1C,+BAA2B,IAAI,IAAI;AACnC;AAAA,EACF;AAEA,QAAM,MAAM,aAAa,GAAG;AAC5B,QAAM,OAAO,IAAI,QAAQ,OAAO,QAAQ,IAAI,QAAQ,IAAI;AACxD,UAAQ,IAAI,mCAAmC,IAAI,EAAE;AACrD,SAAO,MAAM,EAAE,OAAO,IAAI,OAAO,KAAK,CAAC;AACzC;","names":["now","baseUrl","BASE","parseQueuePath","authed"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@originalvoices/job-echo-client",
3
- "version": "0.3.5",
3
+ "version": "0.4.0",
4
4
  "description": "Typed worker runtime + Firestore event client for job-echo-collector",
5
5
  "license": "MIT",
6
6
  "author": "originalvoices",