@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.
- package/CHANGELOG.md +24 -0
- package/dist/dev/module-runner.js +33 -1
- package/dist/middleware/index.d.ts +1 -0
- package/dist/middleware/index.js +1 -0
- package/dist/middleware/remote-jobs-secret.d.ts +14 -0
- package/dist/middleware/remote-jobs-secret.js +27 -0
- package/dist/services/credential-wire-service.d.ts +15 -0
- package/dist/services/credential-wire-service.js +30 -11
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/remote-jobs.d.ts +27 -0
- package/dist/services/remote-jobs.js +59 -0
- package/dist/services/webhook-service.d.ts +8 -0
- package/dist/wirings/scheduler/index.d.ts +1 -1
- package/dist/wirings/scheduler/index.js +1 -1
- package/dist/wirings/scheduler/scheduler-runner.d.ts +4 -0
- package/dist/wirings/scheduler/scheduler-runner.js +1 -1
- package/package.json +1 -1
- package/src/dev/module-runner.test.ts +23 -0
- package/src/dev/module-runner.ts +39 -1
- package/src/middleware/index.ts +1 -0
- package/src/middleware/remote-jobs-secret.ts +35 -0
- package/src/public-surface.json +61 -14
- package/src/services/credential-wire-service.test.ts +63 -0
- package/src/services/credential-wire-service.ts +27 -9
- package/src/services/index.ts +10 -0
- package/src/services/remote-jobs.test.ts +106 -0
- package/src/services/remote-jobs.ts +92 -0
- package/src/services/webhook-service.ts +8 -0
- package/src/wirings/scheduler/index.ts +1 -0
- package/src/wirings/scheduler/scheduler-runner.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/classification/data-lock.d.ts +0 -80
- package/dist/classification/data-lock.js +0 -146
- package/dist/column-form.d.ts +0 -32
- package/dist/column-form.js +0 -42
- package/dist/data-classification.d.ts +0 -64
- package/dist/data-classification.js +0 -1
- package/dist/ecosystem/agent-scorer.d.ts +0 -12
- package/dist/ecosystem/agent-scorer.js +0 -5
- package/dist/ecosystem/agent.d.ts +0 -21
- package/dist/ecosystem/agent.js +0 -6
- package/dist/ecosystem/channel/local.d.ts +0 -3
- package/dist/ecosystem/channel/local.js +0 -3
- package/dist/ecosystem/channel/serverless.d.ts +0 -9
- package/dist/ecosystem/channel/serverless.js +0 -1
- package/dist/ecosystem/channel.d.ts +0 -15
- package/dist/ecosystem/channel.js +0 -5
- package/dist/ecosystem/cli/channel.d.ts +0 -16
- package/dist/ecosystem/cli/channel.js +0 -2
- package/dist/ecosystem/cli.d.ts +0 -9
- package/dist/ecosystem/cli.js +0 -2
- package/dist/ecosystem/credential.d.ts +0 -10
- package/dist/ecosystem/credential.js +0 -1
- package/dist/ecosystem/crypto-utils.d.ts +0 -8
- package/dist/ecosystem/crypto-utils.js +0 -1
- package/dist/ecosystem/dev.d.ts +0 -9
- package/dist/ecosystem/dev.js +0 -2
- package/dist/ecosystem/errors.d.ts +0 -8
- package/dist/ecosystem/errors.js +0 -2
- package/dist/ecosystem/function.d.ts +0 -13
- package/dist/ecosystem/function.js +0 -2
- package/dist/ecosystem/gateway.d.ts +0 -1
- package/dist/ecosystem/gateway.js +0 -1
- package/dist/ecosystem/hmac.d.ts +0 -1
- package/dist/ecosystem/hmac.js +0 -1
- package/dist/ecosystem/http.d.ts +0 -13
- package/dist/ecosystem/http.js +0 -5
- package/dist/ecosystem/mcp.d.ts +0 -3
- package/dist/ecosystem/mcp.js +0 -2
- package/dist/ecosystem/middleware.d.ts +0 -18
- package/dist/ecosystem/middleware.js +0 -7
- package/dist/ecosystem/node-host-resolver.d.ts +0 -1
- package/dist/ecosystem/node-host-resolver.js +0 -1
- package/dist/ecosystem/node.d.ts +0 -1
- package/dist/ecosystem/node.js +0 -1
- package/dist/ecosystem/oauth2.d.ts +0 -1
- package/dist/ecosystem/oauth2.js +0 -1
- package/dist/ecosystem/persona.d.ts +0 -17
- package/dist/ecosystem/persona.js +0 -5
- package/dist/ecosystem/queue.d.ts +0 -13
- package/dist/ecosystem/queue.js +0 -4
- package/dist/ecosystem/remote.d.ts +0 -8
- package/dist/ecosystem/remote.js +0 -1
- package/dist/ecosystem/role.d.ts +0 -4
- package/dist/ecosystem/role.js +0 -2
- package/dist/ecosystem/rpc.d.ts +0 -6
- package/dist/ecosystem/rpc.js +0 -3
- package/dist/ecosystem/safe-fetch.d.ts +0 -7
- package/dist/ecosystem/safe-fetch.js +0 -1
- package/dist/ecosystem/scenario.d.ts +0 -21
- package/dist/ecosystem/scenario.js +0 -8
- package/dist/ecosystem/scheduler.d.ts +0 -3
- package/dist/ecosystem/scheduler.js +0 -2
- package/dist/ecosystem/schema.d.ts +0 -2
- package/dist/ecosystem/schema.js +0 -1
- package/dist/ecosystem/scope.d.ts +0 -5
- package/dist/ecosystem/scope.js +0 -3
- package/dist/ecosystem/secret.d.ts +0 -11
- package/dist/ecosystem/secret.js +0 -2
- package/dist/ecosystem/services/istanbul-coverage.d.ts +0 -1
- package/dist/ecosystem/services/istanbul-coverage.js +0 -1
- package/dist/ecosystem/services/local-content-request-handler.d.ts +0 -9
- package/dist/ecosystem/services/local-content-request-handler.js +0 -1
- package/dist/ecosystem/services/local-content.d.ts +0 -2
- package/dist/ecosystem/services/local-content.js +0 -1
- package/dist/ecosystem/services/local-meta.d.ts +0 -1
- package/dist/ecosystem/services/local-meta.js +0 -1
- package/dist/ecosystem/services/v8-coverage.d.ts +0 -1
- package/dist/ecosystem/services/v8-coverage.js +0 -1
- package/dist/ecosystem/services.d.ts +0 -36
- package/dist/ecosystem/services.js +0 -12
- package/dist/ecosystem/testing.d.ts +0 -7
- package/dist/ecosystem/testing.js +0 -1
- package/dist/ecosystem/trigger.d.ts +0 -2
- package/dist/ecosystem/trigger.js +0 -1
- package/dist/ecosystem/types.d.ts +0 -12
- package/dist/ecosystem/types.js +0 -6
- package/dist/ecosystem/variable.d.ts +0 -9
- package/dist/ecosystem/variable.js +0 -1
- package/dist/ecosystem/virtual-user.d.ts +0 -24
- package/dist/ecosystem/virtual-user.js +0 -8
- package/dist/ecosystem/workflow.d.ts +0 -20
- package/dist/ecosystem/workflow.js +0 -6
- package/dist/ecosystem.d.ts +0 -27
- package/dist/ecosystem.js +0 -26
- package/dist/index.d.ts +0 -60
- package/dist/index.js +0 -25
- package/dist/middleware/require-unlocked.d.ts +0 -23
- package/dist/middleware/require-unlocked.js +0 -21
- package/dist/pikku-request.d.ts +0 -8
- package/dist/pikku-request.js +0 -15
- package/dist/secret-value.d.ts +0 -56
- package/dist/secret-value.js +0 -46
- package/dist/wirings/data-lock/data-lock-wiring.d.ts +0 -40
- package/dist/wirings/data-lock/data-lock-wiring.js +0 -77
- package/dist/wirings/data-lock/index.d.ts +0 -9
- package/dist/wirings/data-lock/index.js +0 -8
- package/dist/wirings/rpc/addon-runner.d.ts +0 -17
- package/dist/wirings/rpc/addon-runner.js +0 -150
- package/dist/wirings/rpc/remote-addon-auth.d.ts +0 -16
- package/dist/wirings/rpc/remote-addon-auth.js +0 -39
- package/dist/wirings/rpc/wire-addon.d.ts +0 -62
- package/dist/wirings/rpc/wire-addon.js +0 -88
- package/dist/wirings/rpc/wire-remote-addon.d.ts +0 -22
- package/dist/wirings/rpc/wire-remote-addon.js +0 -11
package/src/public-surface.json
CHANGED
|
@@ -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": [
|
|
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": [
|
|
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": [
|
|
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": [
|
|
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": [
|
|
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": [
|
|
322
|
-
|
|
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": [
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
"./services/
|
|
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": [
|
|
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": [
|
|
505
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
}
|
package/src/services/index.ts
CHANGED
|
@@ -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 {
|
|
@@ -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
|
}
|