@pikku/core 0.12.104 → 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 +14 -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/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/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,17 @@
|
|
|
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
|
+
|
|
1
15
|
## 0.12.104
|
|
2
16
|
|
|
3
17
|
### 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
|
+
}
|
|
@@ -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
|
+
)
|
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
|
}
|