@pikku/core 0.12.103 → 0.12.106

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/dev/module-runner.js +33 -1
  3. package/dist/middleware/index.d.ts +1 -0
  4. package/dist/middleware/index.js +1 -0
  5. package/dist/middleware/remote-jobs-secret.d.ts +14 -0
  6. package/dist/middleware/remote-jobs-secret.js +27 -0
  7. package/dist/services/credential-wire-service.d.ts +15 -0
  8. package/dist/services/credential-wire-service.js +30 -11
  9. package/dist/services/index.d.ts +1 -0
  10. package/dist/services/index.js +1 -0
  11. package/dist/services/remote-jobs.d.ts +27 -0
  12. package/dist/services/remote-jobs.js +59 -0
  13. package/dist/services/webhook-service.d.ts +8 -0
  14. package/dist/wirings/scheduler/index.d.ts +1 -1
  15. package/dist/wirings/scheduler/index.js +1 -1
  16. package/dist/wirings/scheduler/scheduler-runner.d.ts +4 -0
  17. package/dist/wirings/scheduler/scheduler-runner.js +1 -1
  18. package/package.json +1 -1
  19. package/src/dev/module-runner.test.ts +23 -0
  20. package/src/dev/module-runner.ts +39 -1
  21. package/src/middleware/index.ts +1 -0
  22. package/src/middleware/remote-jobs-secret.ts +35 -0
  23. package/src/public-surface.json +61 -14
  24. package/src/services/credential-wire-service.test.ts +63 -0
  25. package/src/services/credential-wire-service.ts +27 -9
  26. package/src/services/index.ts +10 -0
  27. package/src/services/remote-jobs.test.ts +106 -0
  28. package/src/services/remote-jobs.ts +92 -0
  29. package/src/services/webhook-service.ts +8 -0
  30. package/src/wirings/scheduler/index.ts +1 -0
  31. package/src/wirings/scheduler/scheduler-runner.ts +1 -1
  32. package/tsconfig.tsbuildinfo +1 -1
  33. package/dist/classification/data-lock.d.ts +0 -80
  34. package/dist/classification/data-lock.js +0 -146
  35. package/dist/column-form.d.ts +0 -32
  36. package/dist/column-form.js +0 -42
  37. package/dist/data-classification.d.ts +0 -64
  38. package/dist/data-classification.js +0 -1
  39. package/dist/ecosystem/agent-scorer.d.ts +0 -12
  40. package/dist/ecosystem/agent-scorer.js +0 -5
  41. package/dist/ecosystem/agent.d.ts +0 -21
  42. package/dist/ecosystem/agent.js +0 -6
  43. package/dist/ecosystem/channel/local.d.ts +0 -3
  44. package/dist/ecosystem/channel/local.js +0 -3
  45. package/dist/ecosystem/channel/serverless.d.ts +0 -9
  46. package/dist/ecosystem/channel/serverless.js +0 -1
  47. package/dist/ecosystem/channel.d.ts +0 -15
  48. package/dist/ecosystem/channel.js +0 -5
  49. package/dist/ecosystem/cli/channel.d.ts +0 -16
  50. package/dist/ecosystem/cli/channel.js +0 -2
  51. package/dist/ecosystem/cli.d.ts +0 -9
  52. package/dist/ecosystem/cli.js +0 -2
  53. package/dist/ecosystem/credential.d.ts +0 -10
  54. package/dist/ecosystem/credential.js +0 -1
  55. package/dist/ecosystem/crypto-utils.d.ts +0 -8
  56. package/dist/ecosystem/crypto-utils.js +0 -1
  57. package/dist/ecosystem/dev.d.ts +0 -9
  58. package/dist/ecosystem/dev.js +0 -2
  59. package/dist/ecosystem/errors.d.ts +0 -8
  60. package/dist/ecosystem/errors.js +0 -2
  61. package/dist/ecosystem/function.d.ts +0 -13
  62. package/dist/ecosystem/function.js +0 -2
  63. package/dist/ecosystem/gateway.d.ts +0 -1
  64. package/dist/ecosystem/gateway.js +0 -1
  65. package/dist/ecosystem/hmac.d.ts +0 -1
  66. package/dist/ecosystem/hmac.js +0 -1
  67. package/dist/ecosystem/http.d.ts +0 -13
  68. package/dist/ecosystem/http.js +0 -5
  69. package/dist/ecosystem/mcp.d.ts +0 -3
  70. package/dist/ecosystem/mcp.js +0 -2
  71. package/dist/ecosystem/middleware.d.ts +0 -18
  72. package/dist/ecosystem/middleware.js +0 -7
  73. package/dist/ecosystem/node-host-resolver.d.ts +0 -1
  74. package/dist/ecosystem/node-host-resolver.js +0 -1
  75. package/dist/ecosystem/node.d.ts +0 -1
  76. package/dist/ecosystem/node.js +0 -1
  77. package/dist/ecosystem/oauth2.d.ts +0 -1
  78. package/dist/ecosystem/oauth2.js +0 -1
  79. package/dist/ecosystem/persona.d.ts +0 -17
  80. package/dist/ecosystem/persona.js +0 -5
  81. package/dist/ecosystem/queue.d.ts +0 -13
  82. package/dist/ecosystem/queue.js +0 -4
  83. package/dist/ecosystem/remote.d.ts +0 -8
  84. package/dist/ecosystem/remote.js +0 -1
  85. package/dist/ecosystem/role.d.ts +0 -4
  86. package/dist/ecosystem/role.js +0 -2
  87. package/dist/ecosystem/rpc.d.ts +0 -6
  88. package/dist/ecosystem/rpc.js +0 -3
  89. package/dist/ecosystem/safe-fetch.d.ts +0 -7
  90. package/dist/ecosystem/safe-fetch.js +0 -1
  91. package/dist/ecosystem/scenario.d.ts +0 -21
  92. package/dist/ecosystem/scenario.js +0 -8
  93. package/dist/ecosystem/scheduler.d.ts +0 -3
  94. package/dist/ecosystem/scheduler.js +0 -2
  95. package/dist/ecosystem/schema.d.ts +0 -2
  96. package/dist/ecosystem/schema.js +0 -1
  97. package/dist/ecosystem/scope.d.ts +0 -5
  98. package/dist/ecosystem/scope.js +0 -3
  99. package/dist/ecosystem/secret.d.ts +0 -11
  100. package/dist/ecosystem/secret.js +0 -2
  101. package/dist/ecosystem/services/istanbul-coverage.d.ts +0 -1
  102. package/dist/ecosystem/services/istanbul-coverage.js +0 -1
  103. package/dist/ecosystem/services/local-content-request-handler.d.ts +0 -9
  104. package/dist/ecosystem/services/local-content-request-handler.js +0 -1
  105. package/dist/ecosystem/services/local-content.d.ts +0 -2
  106. package/dist/ecosystem/services/local-content.js +0 -1
  107. package/dist/ecosystem/services/local-meta.d.ts +0 -1
  108. package/dist/ecosystem/services/local-meta.js +0 -1
  109. package/dist/ecosystem/services/v8-coverage.d.ts +0 -1
  110. package/dist/ecosystem/services/v8-coverage.js +0 -1
  111. package/dist/ecosystem/services.d.ts +0 -36
  112. package/dist/ecosystem/services.js +0 -12
  113. package/dist/ecosystem/testing.d.ts +0 -7
  114. package/dist/ecosystem/testing.js +0 -1
  115. package/dist/ecosystem/trigger.d.ts +0 -2
  116. package/dist/ecosystem/trigger.js +0 -1
  117. package/dist/ecosystem/types.d.ts +0 -12
  118. package/dist/ecosystem/types.js +0 -6
  119. package/dist/ecosystem/variable.d.ts +0 -9
  120. package/dist/ecosystem/variable.js +0 -1
  121. package/dist/ecosystem/virtual-user.d.ts +0 -24
  122. package/dist/ecosystem/virtual-user.js +0 -8
  123. package/dist/ecosystem/workflow.d.ts +0 -20
  124. package/dist/ecosystem/workflow.js +0 -6
  125. package/dist/ecosystem.d.ts +0 -27
  126. package/dist/ecosystem.js +0 -26
  127. package/dist/index.d.ts +0 -60
  128. package/dist/index.js +0 -25
  129. package/dist/middleware/require-unlocked.d.ts +0 -23
  130. package/dist/middleware/require-unlocked.js +0 -21
  131. package/dist/pikku-request.d.ts +0 -8
  132. package/dist/pikku-request.js +0 -15
  133. package/dist/secret-value.d.ts +0 -56
  134. package/dist/secret-value.js +0 -46
  135. package/dist/wirings/data-lock/data-lock-wiring.d.ts +0 -40
  136. package/dist/wirings/data-lock/data-lock-wiring.js +0 -77
  137. package/dist/wirings/data-lock/index.d.ts +0 -9
  138. package/dist/wirings/data-lock/index.js +0 -8
  139. package/dist/wirings/rpc/addon-runner.d.ts +0 -17
  140. package/dist/wirings/rpc/addon-runner.js +0 -150
  141. package/dist/wirings/rpc/remote-addon-auth.d.ts +0 -16
  142. package/dist/wirings/rpc/remote-addon-auth.js +0 -39
  143. package/dist/wirings/rpc/wire-addon.d.ts +0 -62
  144. package/dist/wirings/rpc/wire-addon.js +0 -88
  145. package/dist/wirings/rpc/wire-remote-addon.d.ts +0 -22
  146. package/dist/wirings/rpc/wire-remote-addon.js +0 -11
@@ -16,6 +16,7 @@
16
16
  "pikkuMiddleware",
17
17
  "pikkuMiddlewareFactory",
18
18
  "pikkuRemoteAuthMiddleware",
19
+ "remoteJobsSecret",
19
20
  "requireOrigin",
20
21
  "runMiddleware",
21
22
  "telemetryInner",
@@ -98,7 +99,9 @@
98
99
  "reconstructStateAt"
99
100
  ],
100
101
  "./workflow/types": [],
101
- "./actor-flow": ["runConversation"],
102
+ "./actor-flow": [
103
+ "runConversation"
104
+ ],
102
105
  "./virtual-user": [
103
106
  "DEFAULT_MAX_INTERVAL_MS",
104
107
  "DEFAULT_MIN_INTERVAL_MS",
@@ -161,7 +164,9 @@
161
164
  "wireHTTP",
162
165
  "wireHTTPRoutes"
163
166
  ],
164
- "./remote": ["buildRemoteHeaders"],
167
+ "./remote": [
168
+ "buildRemoteHeaders"
169
+ ],
165
170
  "./queue": [
166
171
  "QueueJobDiscardedError",
167
172
  "QueueJobFailedError",
@@ -171,12 +176,17 @@
171
176
  "wireQueueWorker"
172
177
  ],
173
178
  "./scheduler": [
179
+ "ScheduledTaskNotFoundError",
174
180
  "getScheduledTasks",
175
181
  "logSchedulers",
176
182
  "runScheduledTask",
177
183
  "wireScheduler"
178
184
  ],
179
- "./trigger": ["PikkuTriggerService", "wireTrigger", "wireTriggerSource"],
185
+ "./trigger": [
186
+ "PikkuTriggerService",
187
+ "wireTrigger",
188
+ "wireTriggerSource"
189
+ ],
180
190
  "./rpc": [
181
191
  "PikkuRPCService",
182
192
  "RPCNotFoundError",
@@ -196,7 +206,10 @@
196
206
  "safeFetch",
197
207
  "setDefaultHostResolver"
198
208
  ],
199
- "./node-host-resolver": ["installNodeHostResolver", "nodeHostResolver"],
209
+ "./node-host-resolver": [
210
+ "installNodeHostResolver",
211
+ "nodeHostResolver"
212
+ ],
200
213
  "./mcp": [
201
214
  "MCPEndpointRegistry",
202
215
  "MCPError",
@@ -265,7 +278,10 @@
265
278
  "runCLICommand",
266
279
  "wireCLI"
267
280
  ],
268
- "./cli/command-parser": ["generateCommandHelp", "parseCLIArguments"],
281
+ "./cli/command-parser": [
282
+ "generateCommandHelp",
283
+ "parseCLIArguments"
284
+ ],
269
285
  "./cli/channel": [
270
286
  "executeCLIViaChannel",
271
287
  "executeRawCLIViaChannel",
@@ -318,8 +334,14 @@
318
334
  "verifyActorSecret",
319
335
  "verifyPersonaRoles"
320
336
  ],
321
- "./secret": ["defineSecret", "validateAndBuildSecretDefinitionsMeta"],
322
- "./variable": ["defineVariable", "validateAndBuildVariableDefinitionsMeta"],
337
+ "./secret": [
338
+ "defineSecret",
339
+ "validateAndBuildSecretDefinitionsMeta"
340
+ ],
341
+ "./variable": [
342
+ "defineVariable",
343
+ "validateAndBuildVariableDefinitionsMeta"
344
+ ],
323
345
  "./oauth2": [],
324
346
  "./errors": [
325
347
  "AIProviderAuthError",
@@ -394,6 +416,10 @@
394
416
  "PikkuCredentialWireService",
395
417
  "PikkuSessionService",
396
418
  "QueueWebhookService",
419
+ "REMOTE_JOBS_SECRET_HEADER",
420
+ "REMOTE_JOBS_SECRET_VARIABLE",
421
+ "REMOTE_QUEUE_JOB_PATH",
422
+ "REMOTE_SCHEDULER_JOB_PATH",
397
423
  "SchedulerService",
398
424
  "ScopedCredentialService",
399
425
  "ScopedSecretService",
@@ -412,6 +438,8 @@
412
438
  "deriveActorSecret",
413
439
  "getStubTracker",
414
440
  "isTestRun",
441
+ "pikkuRemoteQueueJobFunc",
442
+ "pikkuRemoteScheduledJobFunc",
415
443
  "pikkuWebhookWorkerFunc",
416
444
  "renderEmail",
417
445
  "scenarioArtifactContentType",
@@ -421,10 +449,19 @@
421
449
  "verifyActorSecret",
422
450
  "withoutSecrets"
423
451
  ],
424
- "./services/local-meta": ["LocalMetaService"],
425
- "./services/v8-coverage": ["V8CoverageService"],
426
- "./services/istanbul-coverage": ["IstanbulCoverageService"],
427
- "./services/local-content": ["LocalContent", "signedContentPath"],
452
+ "./services/local-meta": [
453
+ "LocalMetaService"
454
+ ],
455
+ "./services/v8-coverage": [
456
+ "V8CoverageService"
457
+ ],
458
+ "./services/istanbul-coverage": [
459
+ "IstanbulCoverageService"
460
+ ],
461
+ "./services/local-content": [
462
+ "LocalContent",
463
+ "signedContentPath"
464
+ ],
428
465
  "./services/local-content-request-handler": [
429
466
  "createLocalContentRequestHandler",
430
467
  "verifySignedContentRequest"
@@ -454,7 +491,10 @@
454
491
  "verifyWithKeyMaterial",
455
492
  "wrapDEK"
456
493
  ],
457
- "./hmac": ["hmacSha256Hex", "timingSafeStringEqual"],
494
+ "./hmac": [
495
+ "hmacSha256Hex",
496
+ "timingSafeStringEqual"
497
+ ],
458
498
  "./state": [
459
499
  "addPackageServiceFactories",
460
500
  "getAllPackageStates",
@@ -501,6 +541,13 @@
501
541
  "getSchema",
502
542
  "validateSchema"
503
543
  ],
504
- "./testing": ["clearPikkuRuntimeState", "defineServiceTests"],
505
- "./dev": ["pikkuDevReloader", "reconcileAddonRegistry", "reloadGeneratedMeta"]
544
+ "./testing": [
545
+ "clearPikkuRuntimeState",
546
+ "defineServiceTests"
547
+ ],
548
+ "./dev": [
549
+ "pikkuDevReloader",
550
+ "reconcileAddonRegistry",
551
+ "reloadGeneratedMeta"
552
+ ]
506
553
  }
@@ -122,6 +122,69 @@ describe('PikkuCredentialWireService', () => {
122
122
  assert.deepStrictEqual(r1, r2)
123
123
  })
124
124
 
125
+ /**
126
+ * The regression the `loaded` flag was written to avoid.
127
+ *
128
+ * `get` answers synchronously once `loaded` is set, so setting the flag
129
+ * before the fetch resolved gave every caller that arrived during the load a
130
+ * confident `null` for a credential that exists. `getAll` hid it: it hands
131
+ * back the live `credentials` object, which is filled in before anyone
132
+ * asserts on it, so only a reader that copies a VALUE out sees the gap.
133
+ */
134
+ test('should not answer null while a lazy load is still in flight', async () => {
135
+ const credService = mockCredentialService({ stripe: { key: 'sk_live' } })
136
+ const wire: PikkuWire = { session: { userId: 'user-1' } as any }
137
+ const service = new PikkuCredentialWireService(credService, wire)
138
+
139
+ // Issued in one tick, exactly as `Promise.all` over sibling RPC calls does.
140
+ const results = await Promise.all([
141
+ service.get('stripe'),
142
+ service.get('stripe'),
143
+ service.get('stripe'),
144
+ ])
145
+
146
+ for (const [index, result] of results.entries()) {
147
+ assert.deepStrictEqual(
148
+ result,
149
+ { key: 'sk_live' },
150
+ `concurrent reader ${index} lost the credential`
151
+ )
152
+ }
153
+ })
154
+
155
+ test('should not answer null during a slow lazy load', async () => {
156
+ const credService: CredentialService = {
157
+ get: async () => null,
158
+ set: async () => {},
159
+ delete: async () => {},
160
+ has: async () => true,
161
+ getAll: async () => {
162
+ await new Promise((resolve) => setTimeout(resolve, 10))
163
+ return { stripe: { key: 'sk_live' } }
164
+ },
165
+ }
166
+ const wire: PikkuWire = { session: { userId: 'user-1' } as any }
167
+ const service = new PikkuCredentialWireService(credService, wire)
168
+
169
+ const first = service.get('stripe')
170
+ const second = service.get('stripe')
171
+ assert.deepStrictEqual(await first, { key: 'sk_live' })
172
+ assert.deepStrictEqual(await second, { key: 'sk_live' })
173
+ })
174
+
175
+ test('should still take the sync path when there is nothing to load', async () => {
176
+ // Both early returns are settled states, so they must set `loaded` too.
177
+ const service = new PikkuCredentialWireService(
178
+ mockCredentialService({ stripe: { key: '1' } }),
179
+ {}
180
+ )
181
+ await service.getAll()
182
+ assert.ok(
183
+ !(service.getAll() instanceof Promise),
184
+ 'expected sync return after an unresolvable user'
185
+ )
186
+ })
187
+
125
188
  test('getScoped should only return allowed names', async () => {
126
189
  const credService = mockCredentialService({
127
190
  stripe: { key: '1' },
@@ -62,17 +62,35 @@ export class PikkuCredentialWireService {
62
62
  return this.loadPromise
63
63
  }
64
64
 
65
+ /**
66
+ * `loaded` means "the credentials are in hand", never "a load has started".
67
+ *
68
+ * The distinction matters because `get`, `getAll` and `getScoped` all answer
69
+ * SYNCHRONOUSLY once it is set. Setting it before the await let every caller
70
+ * that arrived while the fetch was still in flight take that synchronous path
71
+ * over an empty map and receive `null` for a credential that exists — the
72
+ * first of N concurrent readers got the value and the rest silently did not.
73
+ * `loadPromise` is what guards against loading twice; this flag is only ever
74
+ * about whether the fast path is safe to take.
75
+ *
76
+ * `finally` rather than a trailing assignment: the two early returns (no
77
+ * service or wire, no resolvable user) are settled states as well — there is
78
+ * nothing to fetch — so they should reach the fast path too.
79
+ */
65
80
  private async doLoad(): Promise<void> {
66
- this.loaded = true
67
- if (!this.credentialService || !this.wire) return
68
- const userId = defaultPikkuUserIdResolver(this.wire)
69
- if (!userId) return
70
- this.wire.pikkuUserId = userId
71
- const allCreds = await this.credentialService.getAll(userId)
72
- for (const [name, value] of Object.entries(allCreds)) {
73
- if (!(name in this.credentials)) {
74
- this.credentials[name] = value
81
+ try {
82
+ if (!this.credentialService || !this.wire) return
83
+ const userId = defaultPikkuUserIdResolver(this.wire)
84
+ if (!userId) return
85
+ this.wire.pikkuUserId = userId
86
+ const allCreds = await this.credentialService.getAll(userId)
87
+ for (const [name, value] of Object.entries(allCreds)) {
88
+ if (!(name in this.credentials)) {
89
+ this.credentials[name] = value
90
+ }
75
91
  }
92
+ } finally {
93
+ this.loaded = true
76
94
  }
77
95
  }
78
96
  }
@@ -18,6 +18,16 @@ export {
18
18
  QueueWebhookService,
19
19
  pikkuWebhookWorkerFunc,
20
20
  } from './queue-webhook-service.js'
21
+ export {
22
+ REMOTE_JOBS_SECRET_HEADER,
23
+ REMOTE_JOBS_SECRET_VARIABLE,
24
+ REMOTE_QUEUE_JOB_PATH,
25
+ REMOTE_SCHEDULER_JOB_PATH,
26
+ pikkuRemoteQueueJobFunc,
27
+ pikkuRemoteScheduledJobFunc,
28
+ type RemoteQueueJobData,
29
+ type RemoteScheduledJobData,
30
+ } from './remote-jobs.js'
21
31
  export { InMemoryQueueService } from './in-memory-queue-service.js'
22
32
  export { InMemoryTriggerService } from './in-memory-trigger-service.js'
23
33
  export { InMemoryAgentRunStateService } from './in-memory-agent-run-state-service.js'
@@ -0,0 +1,106 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { BadRequestError, UnauthorizedError } from '../errors/errors.js'
4
+ import { resetPikkuState, setSingletonServices } from '../pikku-state.js'
5
+ import { remoteJobsSecret } from '../middleware/remote-jobs-secret.js'
6
+ import {
7
+ REMOTE_JOBS_SECRET_HEADER,
8
+ pikkuRemoteScheduledJobFunc,
9
+ } from './remote-jobs.js'
10
+
11
+ const silentLogger = {
12
+ info: () => {},
13
+ warn: () => {},
14
+ error: () => {},
15
+ debug: () => {},
16
+ } as any
17
+
18
+ const wire = (header?: string) =>
19
+ ({
20
+ http: { request: { header: () => header ?? null } },
21
+ }) as any
22
+
23
+ const services = (secret?: string) =>
24
+ ({ variables: { get: async () => secret } }) as any
25
+
26
+ describe('remoteJobsSecret', () => {
27
+ test('lets a caller holding the secret through', async () => {
28
+ let reached = false
29
+ await remoteJobsSecret(services('s3cret'), wire('s3cret'), async () => {
30
+ reached = true
31
+ })
32
+ assert.equal(reached, true)
33
+ })
34
+
35
+ test('rejects a caller presenting the wrong secret', async () => {
36
+ await assert.rejects(
37
+ remoteJobsSecret(services('s3cret'), wire('nope'), async () => {}),
38
+ UnauthorizedError
39
+ )
40
+ })
41
+
42
+ test('rejects when the deployment configured no secret at all', async () => {
43
+ await assert.rejects(
44
+ remoteJobsSecret(services(undefined), wire('anything'), async () => {}),
45
+ UnauthorizedError
46
+ )
47
+ })
48
+
49
+ test('rejects a caller presenting no header', async () => {
50
+ await assert.rejects(
51
+ remoteJobsSecret(services('s3cret'), wire(), async () => {}),
52
+ UnauthorizedError
53
+ )
54
+ })
55
+
56
+ test('reads the header the dispatchers already send', async () => {
57
+ const seen: string[] = []
58
+ await assert.rejects(
59
+ remoteJobsSecret(
60
+ services('s3cret'),
61
+ {
62
+ http: {
63
+ request: {
64
+ header: (name: string) => {
65
+ seen.push(name)
66
+ return null
67
+ },
68
+ },
69
+ },
70
+ } as any,
71
+ async () => {}
72
+ ),
73
+ UnauthorizedError
74
+ )
75
+ assert.deepEqual(seen, [REMOTE_JOBS_SECRET_HEADER])
76
+ })
77
+
78
+ test('awaits a variables service that resolves asynchronously', async () => {
79
+ let reached = false
80
+ await remoteJobsSecret(
81
+ {
82
+ variables: {
83
+ get: () => new Promise((resolve) => setTimeout(() => resolve('s3cret'), 1)),
84
+ },
85
+ } as any,
86
+ wire('s3cret'),
87
+ async () => {
88
+ reached = true
89
+ }
90
+ )
91
+ assert.equal(reached, true)
92
+ })
93
+ })
94
+
95
+ describe('pikkuRemoteScheduledJobFunc', () => {
96
+ test('acks a task this deployment does not have rather than asking for a retry', async () => {
97
+ resetPikkuState()
98
+ setSingletonServices({ logger: silentLogger } as any)
99
+ await assert.rejects(
100
+ pikkuRemoteScheduledJobFunc({ logger: silentLogger }, {
101
+ taskName: 'not-a-task',
102
+ }),
103
+ BadRequestError
104
+ )
105
+ })
106
+ })
@@ -0,0 +1,92 @@
1
+ import { BadRequestError, PikkuMissingMetaError } from '../errors/errors.js'
2
+ import { QueueJobDiscardedError, runQueueJob } from '../wirings/queue/queue-runner.js'
3
+ import type { QueueJob, QueueJobStatus } from '../wirings/queue/queue.types.js'
4
+ import {
5
+ ScheduledTaskNotFoundError,
6
+ runScheduledTask,
7
+ } from '../wirings/scheduler/scheduler-runner.js'
8
+ import type { Logger } from './logger.js'
9
+
10
+ /** The variable holding the shared secret every remote job must present. */
11
+ export const REMOTE_JOBS_SECRET_VARIABLE = 'PIKKU_DISPATCH_SECRET'
12
+
13
+ export const REMOTE_JOBS_SECRET_HEADER = 'x-pikku-dispatch'
14
+
15
+ export const REMOTE_QUEUE_JOB_PATH = '/__pikku/queue-job'
16
+
17
+ export const REMOTE_SCHEDULER_JOB_PATH = '/__pikku/scheduler-job'
18
+
19
+ export interface RemoteQueueJobData {
20
+ queueName: string
21
+ data?: unknown
22
+ jobId?: string
23
+ traceId?: string
24
+ }
25
+
26
+ export interface RemoteScheduledJobData {
27
+ taskName: string
28
+ }
29
+
30
+ /**
31
+ * A dispatched job is delivered, not owned: the sender retries, so a permanent
32
+ * failure has to read differently from a transient one. An unknown worker or a
33
+ * discarded job answers 4xx so the sender stops; anything else propagates and
34
+ * is delivered again.
35
+ */
36
+ export async function pikkuRemoteQueueJobFunc(
37
+ { logger }: { logger: Logger },
38
+ { queueName, data, jobId, traceId }: RemoteQueueJobData
39
+ ): Promise<void> {
40
+ const job: QueueJob = {
41
+ queueName,
42
+ data,
43
+ id: jobId ?? traceId ?? `remote-${Date.now()}`,
44
+ status: async (): Promise<QueueJobStatus> => 'active',
45
+ metadata: () => ({
46
+ processedAt: new Date(),
47
+ attemptsMade: 0,
48
+ maxAttempts: undefined,
49
+ result: undefined,
50
+ progress: 0,
51
+ createdAt: new Date(),
52
+ completedAt: undefined,
53
+ failedAt: undefined,
54
+ error: undefined,
55
+ }),
56
+ waitForCompletion: async () => {
57
+ throw new Error('remote jobs do not support waitForCompletion')
58
+ },
59
+ }
60
+
61
+ try {
62
+ await runQueueJob({ job, traceId })
63
+ } catch (error) {
64
+ if (
65
+ error instanceof PikkuMissingMetaError ||
66
+ error instanceof QueueJobDiscardedError
67
+ ) {
68
+ throw new BadRequestError(error.message)
69
+ }
70
+ logger.error(
71
+ `remote-jobs: queue '${queueName}' failed: ${error instanceof Error ? error.message : String(error)}`
72
+ )
73
+ throw error
74
+ }
75
+ }
76
+
77
+ export async function pikkuRemoteScheduledJobFunc(
78
+ { logger }: { logger: Logger },
79
+ { taskName }: RemoteScheduledJobData
80
+ ): Promise<void> {
81
+ try {
82
+ await runScheduledTask({ name: taskName, traceId: `cron-${taskName}` })
83
+ } catch (error) {
84
+ if (error instanceof ScheduledTaskNotFoundError) {
85
+ throw new BadRequestError(error.message)
86
+ }
87
+ logger.error(
88
+ `remote-jobs: task '${taskName}' failed: ${error instanceof Error ? error.message : String(error)}`
89
+ )
90
+ throw error
91
+ }
92
+ }
@@ -20,6 +20,14 @@ export interface SendWebhookInput {
20
20
 
21
21
  export interface SendWebhookResult {
22
22
  jobId: string
23
+ /**
24
+ * The delivery row's id, which is what `getDelivery` reads back. Present only
25
+ * on store-backed implementations. It is not interchangeable with `jobId`: a
26
+ * broker is free to assign its own identity — the JetStream queue returns a
27
+ * stream sequence — so a caller that hands `jobId` to `getDelivery` looks up
28
+ * an id that was never written.
29
+ */
30
+ deliveryId?: string
23
31
  }
24
32
 
25
33
  export interface WebhookServiceConfig {
@@ -1,4 +1,5 @@
1
1
  export {
2
+ ScheduledTaskNotFoundError,
2
3
  runScheduledTask,
3
4
  wireScheduler,
4
5
  getScheduledTasks,
@@ -52,7 +52,7 @@ export const wireScheduler = <
52
52
  tasks.set(scheduledTask.name, scheduledTask)
53
53
  }
54
54
 
55
- class ScheduledTaskNotFoundError extends PikkuError {
55
+ export class ScheduledTaskNotFoundError extends PikkuError {
56
56
  constructor(title: string) {
57
57
  super(`Scheduled task not found: ${title}`)
58
58
  }