@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/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
## 0.12.106
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- bbb9e3b: Add a `scaffold.remoteJobs` flag that generates HTTP routes an external dispatcher posts to, so a runtime holding neither a queue consumer nor a clock still runs its queue workers and scheduled tasks. The runners and the secret guard live in `@pikku/core`; the scaffold only wires them.
|
|
6
|
+
|
|
7
|
+
## 0.12.105
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 1ab831e: Fix hot reload silently keeping old code on Bun for any file importing a relative sibling.
|
|
12
|
+
|
|
13
|
+
Inside a `pikku dev` that has been running for a while, Bun stops honouring `createRequire`'s referrer: a file importing `./sibling.js` reloads as `Cannot find module './sibling.js'` even though it resolves fine in a fresh Bun script, so the reload aborts and the process goes on serving the previous version of a function whose source has visibly changed. Bun's own resolver still answers correctly when handed the importer's directory outright, so the module runner now resolves each specifier to an absolute path before requiring it, and the referrer never has to survive the trip. Specifiers Bun's resolver declines — builtins among them — fall through unchanged, and non-Bun runtimes keep the plain `createRequire`.
|
|
14
|
+
|
|
15
|
+
## 0.12.104
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- f10162a: `send()` now reports the delivery id it wrote, not only the broker's job id.
|
|
20
|
+
|
|
21
|
+
`KyselyWebhookService.send()` returned `{ jobId }` straight from `queueService.add()`, and callers handed that to `getDelivery()`. That only worked because every queue in the test suite echoed the requested `jobId` back. A broker is free to assign its own identity — the JetStream queue returns a stream sequence — so on those the read-back looked up an id that was never written and 404'd.
|
|
22
|
+
|
|
23
|
+
`SendWebhookResult` gains an optional `deliveryId`, which store-backed implementations always populate.
|
|
24
|
+
|
|
1
25
|
## 0.12.103
|
|
2
26
|
|
|
3
27
|
### Patch Changes
|
|
@@ -18,6 +18,38 @@ const loadTransform = async () => {
|
|
|
18
18
|
* is correct, and no amount of re-reading it will reveal that the reloader —
|
|
19
19
|
* not the file — is what cannot cope. */
|
|
20
20
|
export const isTopLevelAwaitLimitation = (error) => /top-level await/i.test(error.message);
|
|
21
|
+
/** The `require` a reloaded module is run with.
|
|
22
|
+
*
|
|
23
|
+
* On Bun, `createRequire`'s referrer stops being honoured somewhere in a
|
|
24
|
+
* long-running process: the same relative specifier that resolves in a fresh
|
|
25
|
+
* script fails inside a `pikku dev` that has been up for a while, so a file
|
|
26
|
+
* importing `./sibling.js` reloads as `Cannot find module` and the developer
|
|
27
|
+
* is left serving the previous code from an edit that looked applied. Bun's
|
|
28
|
+
* own resolver still answers correctly when handed the importer's directory
|
|
29
|
+
* outright, so the specifier is resolved to an absolute path first and the
|
|
30
|
+
* referrer never has to survive the trip. Anything Bun's resolver declines —
|
|
31
|
+
* builtins among them — falls through to `require` unchanged.
|
|
32
|
+
*/
|
|
33
|
+
const createRequireForFile = (absPath) => {
|
|
34
|
+
const base = createRequire(pathToFileURL(absPath));
|
|
35
|
+
const bun = globalThis.Bun;
|
|
36
|
+
if (!bun)
|
|
37
|
+
return base;
|
|
38
|
+
const importerDir = dirname(absPath);
|
|
39
|
+
const resolveThroughBun = (specifier) => {
|
|
40
|
+
try {
|
|
41
|
+
return bun.resolveSync(specifier, importerDir);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return specifier;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
// Only resolution is delegated; the require itself stays outside the guard so
|
|
48
|
+
// a module that throws while evaluating reports its own error rather than
|
|
49
|
+
// being retried under a second specifier.
|
|
50
|
+
const bunRequire = ((specifier) => base(resolveThroughBun(specifier)));
|
|
51
|
+
return Object.assign(bunRequire, base);
|
|
52
|
+
};
|
|
21
53
|
export const createModuleRunner = () => {
|
|
22
54
|
const registry = new Map();
|
|
23
55
|
const run = async (filePath) => {
|
|
@@ -31,7 +63,7 @@ export const createModuleRunner = () => {
|
|
|
31
63
|
sourcefile: absPath,
|
|
32
64
|
});
|
|
33
65
|
const fn = compileFunction(code, ['require', 'exports', 'module', '__filename', '__dirname'], { filename: absPath });
|
|
34
|
-
const require =
|
|
66
|
+
const require = createRequireForFile(absPath);
|
|
35
67
|
const moduleObj = { exports: {} };
|
|
36
68
|
fn(require, moduleObj.exports, moduleObj, absPath, dirname(absPath));
|
|
37
69
|
registry.set(absPath, moduleObj.exports);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { authAPIKey } from './auth-apikey.js';
|
|
2
2
|
export { authCookie } from './auth-cookie.js';
|
|
3
3
|
export { authBearer } from './auth-bearer.js';
|
|
4
|
+
export { remoteJobsSecret } from './remote-jobs-secret.js';
|
|
4
5
|
export { pikkuRemoteAuthMiddleware } from './remote-auth.js';
|
|
5
6
|
export { cors } from './cors.js';
|
|
6
7
|
export { requireOrigin, isAllowedOrigin, toOrigin } from './require-origin.js';
|
package/dist/middleware/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { authAPIKey } from './auth-apikey.js';
|
|
2
2
|
export { authCookie } from './auth-cookie.js';
|
|
3
3
|
export { authBearer } from './auth-bearer.js';
|
|
4
|
+
export { remoteJobsSecret } from './remote-jobs-secret.js';
|
|
4
5
|
export { pikkuRemoteAuthMiddleware } from './remote-auth.js';
|
|
5
6
|
export { cors } from './cors.js';
|
|
6
7
|
export { requireOrigin, isAllowedOrigin, toOrigin } from './require-origin.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guards the remote job inbox. Fails closed: an unset secret rejects every
|
|
3
|
+
* caller, so the routes are never open just because the deployment forgot to
|
|
4
|
+
* configure one.
|
|
5
|
+
*/
|
|
6
|
+
export declare const remoteJobsSecret: import("./middleware.types.js").CorePikkuMiddleware<import("../types/core.types.js").CoreSingletonServices<{
|
|
7
|
+
logLevel?: import("../services/logger.js").LogLevel;
|
|
8
|
+
secrets?: {
|
|
9
|
+
requireAllowedHosts?: boolean;
|
|
10
|
+
};
|
|
11
|
+
workflow?: import("../wirings/workflow/workflow.types.js").WorkflowServiceConfig;
|
|
12
|
+
webhook?: import("../services/webhook-service.js").WebhookServiceConfig;
|
|
13
|
+
postgres?: import("../types/core.types.js").PostgresConfig;
|
|
14
|
+
}>, import("../types/core.types.js").CoreUserSession>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { UnauthorizedError } from '../errors/errors.js';
|
|
2
|
+
import { REMOTE_JOBS_SECRET_HEADER, REMOTE_JOBS_SECRET_VARIABLE, } from '../services/remote-jobs.js';
|
|
3
|
+
import { pikkuMiddleware } from './middleware-factories.js';
|
|
4
|
+
const constantTimeEqual = (a, b) => {
|
|
5
|
+
if (a.length !== b.length)
|
|
6
|
+
return false;
|
|
7
|
+
let result = 0;
|
|
8
|
+
for (let i = 0; i < a.length; i++) {
|
|
9
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
10
|
+
}
|
|
11
|
+
return result === 0;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Guards the remote job inbox. Fails closed: an unset secret rejects every
|
|
15
|
+
* caller, so the routes are never open just because the deployment forgot to
|
|
16
|
+
* configure one.
|
|
17
|
+
*/
|
|
18
|
+
export const remoteJobsSecret = pikkuMiddleware(async ({ variables }, { http }, next) => {
|
|
19
|
+
const expected = await variables.get(REMOTE_JOBS_SECRET_VARIABLE);
|
|
20
|
+
const provided = http?.request?.header(REMOTE_JOBS_SECRET_HEADER);
|
|
21
|
+
if (!expected ||
|
|
22
|
+
typeof provided !== 'string' ||
|
|
23
|
+
!constantTimeEqual(provided, expected)) {
|
|
24
|
+
throw new UnauthorizedError('invalid dispatch secret');
|
|
25
|
+
}
|
|
26
|
+
return next();
|
|
27
|
+
});
|
|
@@ -19,6 +19,21 @@ export declare class PikkuCredentialWireService {
|
|
|
19
19
|
getAll(): Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
20
20
|
getScoped(allowedNames: string[]): Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
21
21
|
private lazyLoad;
|
|
22
|
+
/**
|
|
23
|
+
* `loaded` means "the credentials are in hand", never "a load has started".
|
|
24
|
+
*
|
|
25
|
+
* The distinction matters because `get`, `getAll` and `getScoped` all answer
|
|
26
|
+
* SYNCHRONOUSLY once it is set. Setting it before the await let every caller
|
|
27
|
+
* that arrived while the fetch was still in flight take that synchronous path
|
|
28
|
+
* over an empty map and receive `null` for a credential that exists — the
|
|
29
|
+
* first of N concurrent readers got the value and the rest silently did not.
|
|
30
|
+
* `loadPromise` is what guards against loading twice; this flag is only ever
|
|
31
|
+
* about whether the fast path is safe to take.
|
|
32
|
+
*
|
|
33
|
+
* `finally` rather than a trailing assignment: the two early returns (no
|
|
34
|
+
* service or wire, no resolvable user) are settled states as well — there is
|
|
35
|
+
* nothing to fetch — so they should reach the fast path too.
|
|
36
|
+
*/
|
|
22
37
|
private doLoad;
|
|
23
38
|
}
|
|
24
39
|
export declare function createMiddlewareCredentialWireProps(credentialWire: PikkuCredentialWireService): {
|
|
@@ -56,20 +56,39 @@ export class PikkuCredentialWireService {
|
|
|
56
56
|
this.loadPromise = this.doLoad();
|
|
57
57
|
return this.loadPromise;
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* `loaded` means "the credentials are in hand", never "a load has started".
|
|
61
|
+
*
|
|
62
|
+
* The distinction matters because `get`, `getAll` and `getScoped` all answer
|
|
63
|
+
* SYNCHRONOUSLY once it is set. Setting it before the await let every caller
|
|
64
|
+
* that arrived while the fetch was still in flight take that synchronous path
|
|
65
|
+
* over an empty map and receive `null` for a credential that exists — the
|
|
66
|
+
* first of N concurrent readers got the value and the rest silently did not.
|
|
67
|
+
* `loadPromise` is what guards against loading twice; this flag is only ever
|
|
68
|
+
* about whether the fast path is safe to take.
|
|
69
|
+
*
|
|
70
|
+
* `finally` rather than a trailing assignment: the two early returns (no
|
|
71
|
+
* service or wire, no resolvable user) are settled states as well — there is
|
|
72
|
+
* nothing to fetch — so they should reach the fast path too.
|
|
73
|
+
*/
|
|
59
74
|
async doLoad() {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
75
|
+
try {
|
|
76
|
+
if (!this.credentialService || !this.wire)
|
|
77
|
+
return;
|
|
78
|
+
const userId = defaultPikkuUserIdResolver(this.wire);
|
|
79
|
+
if (!userId)
|
|
80
|
+
return;
|
|
81
|
+
this.wire.pikkuUserId = userId;
|
|
82
|
+
const allCreds = await this.credentialService.getAll(userId);
|
|
83
|
+
for (const [name, value] of Object.entries(allCreds)) {
|
|
84
|
+
if (!(name in this.credentials)) {
|
|
85
|
+
this.credentials[name] = value;
|
|
86
|
+
}
|
|
71
87
|
}
|
|
72
88
|
}
|
|
89
|
+
finally {
|
|
90
|
+
this.loaded = true;
|
|
91
|
+
}
|
|
73
92
|
}
|
|
74
93
|
}
|
|
75
94
|
export function createMiddlewareCredentialWireProps(credentialWire) {
|
package/dist/services/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export { LocalVariablesService } from './local-variables.js';
|
|
|
12
12
|
export { ConsoleLogger, JsonConsoleLogger } from './logger-console.js';
|
|
13
13
|
export { InMemoryWorkflowService } from './in-memory-workflow-service.js';
|
|
14
14
|
export { QueueWebhookService, pikkuWebhookWorkerFunc, } from './queue-webhook-service.js';
|
|
15
|
+
export { REMOTE_JOBS_SECRET_HEADER, REMOTE_JOBS_SECRET_VARIABLE, REMOTE_QUEUE_JOB_PATH, REMOTE_SCHEDULER_JOB_PATH, pikkuRemoteQueueJobFunc, pikkuRemoteScheduledJobFunc, type RemoteQueueJobData, type RemoteScheduledJobData, } from './remote-jobs.js';
|
|
15
16
|
export { InMemoryQueueService } from './in-memory-queue-service.js';
|
|
16
17
|
export { InMemoryTriggerService } from './in-memory-trigger-service.js';
|
|
17
18
|
export { InMemoryAgentRunStateService } from './in-memory-agent-run-state-service.js';
|
package/dist/services/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export { LocalVariablesService } from './local-variables.js';
|
|
|
12
12
|
export { ConsoleLogger, JsonConsoleLogger } from './logger-console.js';
|
|
13
13
|
export { InMemoryWorkflowService } from './in-memory-workflow-service.js';
|
|
14
14
|
export { QueueWebhookService, pikkuWebhookWorkerFunc, } from './queue-webhook-service.js';
|
|
15
|
+
export { REMOTE_JOBS_SECRET_HEADER, REMOTE_JOBS_SECRET_VARIABLE, REMOTE_QUEUE_JOB_PATH, REMOTE_SCHEDULER_JOB_PATH, pikkuRemoteQueueJobFunc, pikkuRemoteScheduledJobFunc, } from './remote-jobs.js';
|
|
15
16
|
export { InMemoryQueueService } from './in-memory-queue-service.js';
|
|
16
17
|
export { InMemoryTriggerService } from './in-memory-trigger-service.js';
|
|
17
18
|
export { InMemoryAgentRunStateService } from './in-memory-agent-run-state-service.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Logger } from './logger.js';
|
|
2
|
+
/** The variable holding the shared secret every remote job must present. */
|
|
3
|
+
export declare const REMOTE_JOBS_SECRET_VARIABLE = "PIKKU_DISPATCH_SECRET";
|
|
4
|
+
export declare const REMOTE_JOBS_SECRET_HEADER = "x-pikku-dispatch";
|
|
5
|
+
export declare const REMOTE_QUEUE_JOB_PATH = "/__pikku/queue-job";
|
|
6
|
+
export declare const REMOTE_SCHEDULER_JOB_PATH = "/__pikku/scheduler-job";
|
|
7
|
+
export interface RemoteQueueJobData {
|
|
8
|
+
queueName: string;
|
|
9
|
+
data?: unknown;
|
|
10
|
+
jobId?: string;
|
|
11
|
+
traceId?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface RemoteScheduledJobData {
|
|
14
|
+
taskName: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A dispatched job is delivered, not owned: the sender retries, so a permanent
|
|
18
|
+
* failure has to read differently from a transient one. An unknown worker or a
|
|
19
|
+
* discarded job answers 4xx so the sender stops; anything else propagates and
|
|
20
|
+
* is delivered again.
|
|
21
|
+
*/
|
|
22
|
+
export declare function pikkuRemoteQueueJobFunc({ logger }: {
|
|
23
|
+
logger: Logger;
|
|
24
|
+
}, { queueName, data, jobId, traceId }: RemoteQueueJobData): Promise<void>;
|
|
25
|
+
export declare function pikkuRemoteScheduledJobFunc({ logger }: {
|
|
26
|
+
logger: Logger;
|
|
27
|
+
}, { taskName }: RemoteScheduledJobData): Promise<void>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { BadRequestError, PikkuMissingMetaError } from '../errors/errors.js';
|
|
2
|
+
import { QueueJobDiscardedError, runQueueJob } from '../wirings/queue/queue-runner.js';
|
|
3
|
+
import { ScheduledTaskNotFoundError, runScheduledTask, } from '../wirings/scheduler/scheduler-runner.js';
|
|
4
|
+
/** The variable holding the shared secret every remote job must present. */
|
|
5
|
+
export const REMOTE_JOBS_SECRET_VARIABLE = 'PIKKU_DISPATCH_SECRET';
|
|
6
|
+
export const REMOTE_JOBS_SECRET_HEADER = 'x-pikku-dispatch';
|
|
7
|
+
export const REMOTE_QUEUE_JOB_PATH = '/__pikku/queue-job';
|
|
8
|
+
export const REMOTE_SCHEDULER_JOB_PATH = '/__pikku/scheduler-job';
|
|
9
|
+
/**
|
|
10
|
+
* A dispatched job is delivered, not owned: the sender retries, so a permanent
|
|
11
|
+
* failure has to read differently from a transient one. An unknown worker or a
|
|
12
|
+
* discarded job answers 4xx so the sender stops; anything else propagates and
|
|
13
|
+
* is delivered again.
|
|
14
|
+
*/
|
|
15
|
+
export async function pikkuRemoteQueueJobFunc({ logger }, { queueName, data, jobId, traceId }) {
|
|
16
|
+
const job = {
|
|
17
|
+
queueName,
|
|
18
|
+
data,
|
|
19
|
+
id: jobId ?? traceId ?? `remote-${Date.now()}`,
|
|
20
|
+
status: async () => 'active',
|
|
21
|
+
metadata: () => ({
|
|
22
|
+
processedAt: new Date(),
|
|
23
|
+
attemptsMade: 0,
|
|
24
|
+
maxAttempts: undefined,
|
|
25
|
+
result: undefined,
|
|
26
|
+
progress: 0,
|
|
27
|
+
createdAt: new Date(),
|
|
28
|
+
completedAt: undefined,
|
|
29
|
+
failedAt: undefined,
|
|
30
|
+
error: undefined,
|
|
31
|
+
}),
|
|
32
|
+
waitForCompletion: async () => {
|
|
33
|
+
throw new Error('remote jobs do not support waitForCompletion');
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
try {
|
|
37
|
+
await runQueueJob({ job, traceId });
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof PikkuMissingMetaError ||
|
|
41
|
+
error instanceof QueueJobDiscardedError) {
|
|
42
|
+
throw new BadRequestError(error.message);
|
|
43
|
+
}
|
|
44
|
+
logger.error(`remote-jobs: queue '${queueName}' failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function pikkuRemoteScheduledJobFunc({ logger }, { taskName }) {
|
|
49
|
+
try {
|
|
50
|
+
await runScheduledTask({ name: taskName, traceId: `cron-${taskName}` });
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error instanceof ScheduledTaskNotFoundError) {
|
|
54
|
+
throw new BadRequestError(error.message);
|
|
55
|
+
}
|
|
56
|
+
logger.error(`remote-jobs: task '${taskName}' failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -16,6 +16,14 @@ export interface SendWebhookInput {
|
|
|
16
16
|
}
|
|
17
17
|
export interface SendWebhookResult {
|
|
18
18
|
jobId: string;
|
|
19
|
+
/**
|
|
20
|
+
* The delivery row's id, which is what `getDelivery` reads back. Present only
|
|
21
|
+
* on store-backed implementations. It is not interchangeable with `jobId`: a
|
|
22
|
+
* broker is free to assign its own identity — the JetStream queue returns a
|
|
23
|
+
* stream sequence — so a caller that hands `jobId` to `getDelivery` looks up
|
|
24
|
+
* an id that was never written.
|
|
25
|
+
*/
|
|
26
|
+
deliveryId?: string;
|
|
19
27
|
}
|
|
20
28
|
export interface WebhookServiceConfig {
|
|
21
29
|
retries?: number;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { runScheduledTask, wireScheduler, getScheduledTasks, } from './scheduler-runner.js';
|
|
1
|
+
export { ScheduledTaskNotFoundError, runScheduledTask, wireScheduler, getScheduledTasks, } from './scheduler-runner.js';
|
|
2
2
|
export { logSchedulers } from './log-schedulers.js';
|
|
3
3
|
export type { CoreScheduledTask, ScheduledTasksMeta, } from './scheduler.types.js';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { runScheduledTask, wireScheduler, getScheduledTasks, } from './scheduler-runner.js';
|
|
1
|
+
export { ScheduledTaskNotFoundError, runScheduledTask, wireScheduler, getScheduledTasks, } from './scheduler-runner.js';
|
|
2
2
|
export { logSchedulers } from './log-schedulers.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CoreUserSession } from '../../types/core.types.js';
|
|
2
2
|
import type { CoreScheduledTask } from './scheduler.types.js';
|
|
3
|
+
import { PikkuError } from '../../errors/error-handler.js';
|
|
3
4
|
import type { CorePikkuFunctionConfig, CorePikkuFunctionSessionless } from '../../function/functions.types.js';
|
|
4
5
|
export type RunScheduledTasksParams = {
|
|
5
6
|
name: string;
|
|
@@ -7,5 +8,8 @@ export type RunScheduledTasksParams = {
|
|
|
7
8
|
traceId?: string;
|
|
8
9
|
};
|
|
9
10
|
export declare const wireScheduler: <PikkuFunctionConfig extends CorePikkuFunctionConfig<CorePikkuFunctionSessionless<void, void>>>(scheduledTask: CoreScheduledTask<PikkuFunctionConfig>) => void;
|
|
11
|
+
export declare class ScheduledTaskNotFoundError extends PikkuError {
|
|
12
|
+
constructor(title: string);
|
|
13
|
+
}
|
|
10
14
|
export declare function runScheduledTask({ name, session, traceId, }: RunScheduledTasksParams): Promise<void>;
|
|
11
15
|
export declare const getScheduledTasks: () => Map<string, CoreScheduledTask>;
|
|
@@ -22,7 +22,7 @@ export const wireScheduler = (scheduledTask) => {
|
|
|
22
22
|
}
|
|
23
23
|
tasks.set(scheduledTask.name, scheduledTask);
|
|
24
24
|
};
|
|
25
|
-
class ScheduledTaskNotFoundError extends PikkuError {
|
|
25
|
+
export class ScheduledTaskNotFoundError extends PikkuError {
|
|
26
26
|
constructor(title) {
|
|
27
27
|
super(`Scheduled task not found: ${title}`);
|
|
28
28
|
}
|
package/package.json
CHANGED
|
@@ -209,4 +209,27 @@ describe('createModuleRunner', { concurrency: false }, () => {
|
|
|
209
209
|
`heap grew ${growth.toFixed(1)} MB over 200 edits (expected < 15 MB)`
|
|
210
210
|
)
|
|
211
211
|
})
|
|
212
|
+
|
|
213
|
+
test('a .js specifier resolves to the .ts file that actually exists', async () => {
|
|
214
|
+
// How every TypeScript source in a pikku app refers to a sibling: the
|
|
215
|
+
// import is written `.js` and only the `.ts` is on disk. Nothing else here
|
|
216
|
+
// covers it — the delegated-import test writes a real `.js` — and it is the
|
|
217
|
+
// shape that breaks under Bun once `pikku dev` has been running, where the
|
|
218
|
+
// reload fails and the process quietly keeps serving the previous code.
|
|
219
|
+
await writeFile(join(tmpDir, 'dep.ts'), `export const value = 'from-dep'`)
|
|
220
|
+
|
|
221
|
+
const runner = createModuleRunner()
|
|
222
|
+
const userFile = join(tmpDir, 'importer.ts')
|
|
223
|
+
await writeFile(
|
|
224
|
+
userFile,
|
|
225
|
+
`import { value } from './dep.js'
|
|
226
|
+
export const fn = { func: async () => value }`
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
const result = await runner.run(userFile)
|
|
230
|
+
assert.equal(result.ok, true)
|
|
231
|
+
const mod = (result as { exports: Record<string, unknown> }).exports
|
|
232
|
+
assert.equal(await (mod.fn as { func: () => Promise<string> }).func(), 'from-dep')
|
|
233
|
+
})
|
|
234
|
+
|
|
212
235
|
})
|
package/src/dev/module-runner.ts
CHANGED
|
@@ -43,6 +43,44 @@ export interface PikkuModuleRunner {
|
|
|
43
43
|
export const isTopLevelAwaitLimitation = (error: Error): boolean =>
|
|
44
44
|
/top-level await/i.test(error.message)
|
|
45
45
|
|
|
46
|
+
interface BunResolver {
|
|
47
|
+
resolveSync: (specifier: string, parent: string) => string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The `require` a reloaded module is run with.
|
|
51
|
+
*
|
|
52
|
+
* On Bun, `createRequire`'s referrer stops being honoured somewhere in a
|
|
53
|
+
* long-running process: the same relative specifier that resolves in a fresh
|
|
54
|
+
* script fails inside a `pikku dev` that has been up for a while, so a file
|
|
55
|
+
* importing `./sibling.js` reloads as `Cannot find module` and the developer
|
|
56
|
+
* is left serving the previous code from an edit that looked applied. Bun's
|
|
57
|
+
* own resolver still answers correctly when handed the importer's directory
|
|
58
|
+
* outright, so the specifier is resolved to an absolute path first and the
|
|
59
|
+
* referrer never has to survive the trip. Anything Bun's resolver declines —
|
|
60
|
+
* builtins among them — falls through to `require` unchanged.
|
|
61
|
+
*/
|
|
62
|
+
const createRequireForFile = (absPath: string): NodeRequire => {
|
|
63
|
+
const base = createRequire(pathToFileURL(absPath))
|
|
64
|
+
const bun = (globalThis as { Bun?: BunResolver }).Bun
|
|
65
|
+
if (!bun) return base
|
|
66
|
+
|
|
67
|
+
const importerDir = dirname(absPath)
|
|
68
|
+
const resolveThroughBun = (specifier: string): string => {
|
|
69
|
+
try {
|
|
70
|
+
return bun.resolveSync(specifier, importerDir)
|
|
71
|
+
} catch {
|
|
72
|
+
return specifier
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Only resolution is delegated; the require itself stays outside the guard so
|
|
77
|
+
// a module that throws while evaluating reports its own error rather than
|
|
78
|
+
// being retried under a second specifier.
|
|
79
|
+
const bunRequire = ((specifier: string) =>
|
|
80
|
+
base(resolveThroughBun(specifier))) as NodeRequire
|
|
81
|
+
return Object.assign(bunRequire, base)
|
|
82
|
+
}
|
|
83
|
+
|
|
46
84
|
export const createModuleRunner = (): PikkuModuleRunner => {
|
|
47
85
|
const registry = new Map<string, Record<string, unknown>>()
|
|
48
86
|
|
|
@@ -63,7 +101,7 @@ export const createModuleRunner = (): PikkuModuleRunner => {
|
|
|
63
101
|
{ filename: absPath }
|
|
64
102
|
)
|
|
65
103
|
|
|
66
|
-
const require =
|
|
104
|
+
const require = createRequireForFile(absPath)
|
|
67
105
|
const moduleObj: { exports: Record<string, unknown> } = { exports: {} }
|
|
68
106
|
fn(require, moduleObj.exports, moduleObj, absPath, dirname(absPath))
|
|
69
107
|
|
package/src/middleware/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { authAPIKey } from './auth-apikey.js'
|
|
2
2
|
export { authCookie } from './auth-cookie.js'
|
|
3
3
|
export { authBearer } from './auth-bearer.js'
|
|
4
|
+
export { remoteJobsSecret } from './remote-jobs-secret.js'
|
|
4
5
|
export { pikkuRemoteAuthMiddleware } from './remote-auth.js'
|
|
5
6
|
export { cors } from './cors.js'
|
|
6
7
|
export { requireOrigin, isAllowedOrigin, toOrigin } from './require-origin.js'
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { UnauthorizedError } from '../errors/errors.js'
|
|
2
|
+
import {
|
|
3
|
+
REMOTE_JOBS_SECRET_HEADER,
|
|
4
|
+
REMOTE_JOBS_SECRET_VARIABLE,
|
|
5
|
+
} from '../services/remote-jobs.js'
|
|
6
|
+
import { pikkuMiddleware } from './middleware-factories.js'
|
|
7
|
+
|
|
8
|
+
const constantTimeEqual = (a: string, b: string): boolean => {
|
|
9
|
+
if (a.length !== b.length) return false
|
|
10
|
+
let result = 0
|
|
11
|
+
for (let i = 0; i < a.length; i++) {
|
|
12
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i)
|
|
13
|
+
}
|
|
14
|
+
return result === 0
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Guards the remote job inbox. Fails closed: an unset secret rejects every
|
|
19
|
+
* caller, so the routes are never open just because the deployment forgot to
|
|
20
|
+
* configure one.
|
|
21
|
+
*/
|
|
22
|
+
export const remoteJobsSecret = pikkuMiddleware(
|
|
23
|
+
async ({ variables }, { http }, next) => {
|
|
24
|
+
const expected = await variables.get(REMOTE_JOBS_SECRET_VARIABLE)
|
|
25
|
+
const provided = http?.request?.header(REMOTE_JOBS_SECRET_HEADER)
|
|
26
|
+
if (
|
|
27
|
+
!expected ||
|
|
28
|
+
typeof provided !== 'string' ||
|
|
29
|
+
!constantTimeEqual(provided, expected)
|
|
30
|
+
) {
|
|
31
|
+
throw new UnauthorizedError('invalid dispatch secret')
|
|
32
|
+
}
|
|
33
|
+
return next()
|
|
34
|
+
}
|
|
35
|
+
)
|