@originalvoices/job-echo-client 0.3.0 → 0.3.2

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
@@ -217,9 +217,10 @@ function createClient(cfg) {
217
217
  if (process.env.NODE_ENV !== "production") return disabledClient();
218
218
  const gcpProjectId = cfg.collectorProjectId ?? process.env.JOB_ECHO_GCP_PROJECT ?? process.env.FIREBASE_PROJECT_ID;
219
219
  if (!gcpProjectId) {
220
- throw new Error(
221
- "@originalvoices/job-echo-client: collectorProjectId not provided and neither JOB_ECHO_GCP_PROJECT nor FIREBASE_PROJECT_ID is set"
220
+ console.warn(
221
+ "[job-echo] no collectorProjectId / JOB_ECHO_GCP_PROJECT / FIREBASE_PROJECT_ID set \u2014 Firestore writes disabled"
222
222
  );
223
+ return disabledClient();
223
224
  }
224
225
  let contextPromise = null;
225
226
  const context = () => {
@@ -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 { createWorker, startWorker } 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 if (!gcpProjectId) {\n throw new Error(\n '@originalvoices/job-echo-client: collectorProjectId not provided and neither JOB_ECHO_GCP_PROJECT nor FIREBASE_PROJECT_ID is set',\n )\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 * Convenience: build a worker and start the HTTP server on `port`\n * (defaults to `PORT` env or 8080).\n *\n * If `JOB_ECHO_SYNC_MODE=1` is set, the function instead reconciles\n * Cloud Scheduler jobs from declared `.schedule` fields and exits.\n * Use this from a one-shot Cloud Run Job after deploy.\n */\nexport function startWorker(\n cfg: WorkerConfig & { port?: number },\n): ReturnType<typeof serve> | void {\n if (process.env.JOB_ECHO_SYNC_MODE === '1') {\n syncSchedules(cfg.jobs as Job<unknown>[])\n .then((report) => {\n console.log(\n '[job-echo] schedule sync complete:\\n' +\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 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'\nimport type { Job } from './job'\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 jobs: Job<unknown>[],\n): 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/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 { createWorker, startWorker } 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 * Convenience: build a worker and start the HTTP server on `port`\n * (defaults to `PORT` env or 8080).\n *\n * If `JOB_ECHO_SYNC_MODE=1` is set, the function instead reconciles\n * Cloud Scheduler jobs from declared `.schedule` fields and exits.\n * Use this from a one-shot Cloud Run Job after deploy.\n */\nexport function startWorker(\n cfg: WorkerConfig & { port?: number },\n): ReturnType<typeof serve> | void {\n if (process.env.JOB_ECHO_SYNC_MODE === '1') {\n syncSchedules(cfg.jobs)\n .then((report) => {\n console.log(\n '[job-echo] schedule sync complete:\\n' +\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 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'\nimport type { Job } from './job'\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 // eslint-disable-next-line @typescript-eslint/no-explicit-any\n jobs: Job<any>[],\n): 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
@@ -122,6 +122,6 @@ interface SyncReport {
122
122
  * - JOB_ECHO_TASKS_SA_EMAIL (SA the scheduler-fired tasks run as)
123
123
  * - JOB_ECHO_APP_LABEL (e.g. "platform-prod" — scopes the sync)
124
124
  */
125
- declare function syncSchedules(jobs: Job<unknown>[]): Promise<SyncReport>;
125
+ declare function syncSchedules(jobs: Job<any>[]): Promise<SyncReport>;
126
126
 
127
127
  export { type ClientConfig, type Job, type JobContext, type JobDefinition, type JobEchoClient, type JobEvent, type JobInput, type JobStatus, type SyncReport, type WorkerConfig, createClient, createWorker, defineJob, startWorker, syncSchedules };
package/dist/index.d.ts CHANGED
@@ -122,6 +122,6 @@ interface SyncReport {
122
122
  * - JOB_ECHO_TASKS_SA_EMAIL (SA the scheduler-fired tasks run as)
123
123
  * - JOB_ECHO_APP_LABEL (e.g. "platform-prod" — scopes the sync)
124
124
  */
125
- declare function syncSchedules(jobs: Job<unknown>[]): Promise<SyncReport>;
125
+ declare function syncSchedules(jobs: Job<any>[]): Promise<SyncReport>;
126
126
 
127
127
  export { type ClientConfig, type Job, type JobContext, type JobDefinition, type JobEchoClient, type JobEvent, type JobInput, type JobStatus, type SyncReport, type WorkerConfig, createClient, createWorker, defineJob, startWorker, syncSchedules };
package/dist/index.js CHANGED
@@ -187,9 +187,10 @@ function createClient(cfg) {
187
187
  if (process.env.NODE_ENV !== "production") return disabledClient();
188
188
  const gcpProjectId = cfg.collectorProjectId ?? process.env.JOB_ECHO_GCP_PROJECT ?? process.env.FIREBASE_PROJECT_ID;
189
189
  if (!gcpProjectId) {
190
- throw new Error(
191
- "@originalvoices/job-echo-client: collectorProjectId not provided and neither JOB_ECHO_GCP_PROJECT nor FIREBASE_PROJECT_ID is set"
190
+ console.warn(
191
+ "[job-echo] no collectorProjectId / JOB_ECHO_GCP_PROJECT / FIREBASE_PROJECT_ID set \u2014 Firestore writes disabled"
192
192
  );
193
+ return disabledClient();
193
194
  }
194
195
  let contextPromise = null;
195
196
  const context = () => {
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 if (!gcpProjectId) {\n throw new Error(\n '@originalvoices/job-echo-client: collectorProjectId not provided and neither JOB_ECHO_GCP_PROJECT nor FIREBASE_PROJECT_ID is set',\n )\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 * Convenience: build a worker and start the HTTP server on `port`\n * (defaults to `PORT` env or 8080).\n *\n * If `JOB_ECHO_SYNC_MODE=1` is set, the function instead reconciles\n * Cloud Scheduler jobs from declared `.schedule` fields and exits.\n * Use this from a one-shot Cloud Run Job after deploy.\n */\nexport function startWorker(\n cfg: WorkerConfig & { port?: number },\n): ReturnType<typeof serve> | void {\n if (process.env.JOB_ECHO_SYNC_MODE === '1') {\n syncSchedules(cfg.jobs as Job<unknown>[])\n .then((report) => {\n console.log(\n '[job-echo] schedule sync complete:\\n' +\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 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'\nimport type { Job } from './job'\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 jobs: Job<unknown>[],\n): 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. 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 * Convenience: build a worker and start the HTTP server on `port`\n * (defaults to `PORT` env or 8080).\n *\n * If `JOB_ECHO_SYNC_MODE=1` is set, the function instead reconciles\n * Cloud Scheduler jobs from declared `.schedule` fields and exits.\n * Use this from a one-shot Cloud Run Job after deploy.\n */\nexport function startWorker(\n cfg: WorkerConfig & { port?: number },\n): ReturnType<typeof serve> | void {\n if (process.env.JOB_ECHO_SYNC_MODE === '1') {\n syncSchedules(cfg.jobs)\n .then((report) => {\n console.log(\n '[job-echo] schedule sync complete:\\n' +\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 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'\nimport type { Job } from './job'\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 // eslint-disable-next-line @typescript-eslint/no-explicit-any\n jobs: Job<any>[],\n): 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@originalvoices/job-echo-client",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Typed worker runtime + Firestore event client for job-echo-collector",
5
5
  "license": "MIT",
6
6
  "author": "originalvoices",