@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.
Files changed (144) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/dev/module-runner.js +33 -1
  3. package/dist/middleware/index.d.ts +1 -0
  4. package/dist/middleware/index.js +1 -0
  5. package/dist/middleware/remote-jobs-secret.d.ts +14 -0
  6. package/dist/middleware/remote-jobs-secret.js +27 -0
  7. package/dist/services/credential-wire-service.d.ts +15 -0
  8. package/dist/services/credential-wire-service.js +30 -11
  9. package/dist/services/index.d.ts +1 -0
  10. package/dist/services/index.js +1 -0
  11. package/dist/services/remote-jobs.d.ts +27 -0
  12. package/dist/services/remote-jobs.js +59 -0
  13. package/dist/wirings/scheduler/index.d.ts +1 -1
  14. package/dist/wirings/scheduler/index.js +1 -1
  15. package/dist/wirings/scheduler/scheduler-runner.d.ts +4 -0
  16. package/dist/wirings/scheduler/scheduler-runner.js +1 -1
  17. package/package.json +1 -1
  18. package/src/dev/module-runner.test.ts +23 -0
  19. package/src/dev/module-runner.ts +39 -1
  20. package/src/middleware/index.ts +1 -0
  21. package/src/middleware/remote-jobs-secret.ts +35 -0
  22. package/src/public-surface.json +61 -14
  23. package/src/services/credential-wire-service.test.ts +63 -0
  24. package/src/services/credential-wire-service.ts +27 -9
  25. package/src/services/index.ts +10 -0
  26. package/src/services/remote-jobs.test.ts +106 -0
  27. package/src/services/remote-jobs.ts +92 -0
  28. package/src/wirings/scheduler/index.ts +1 -0
  29. package/src/wirings/scheduler/scheduler-runner.ts +1 -1
  30. package/tsconfig.tsbuildinfo +1 -1
  31. package/dist/classification/data-lock.d.ts +0 -80
  32. package/dist/classification/data-lock.js +0 -146
  33. package/dist/column-form.d.ts +0 -32
  34. package/dist/column-form.js +0 -42
  35. package/dist/data-classification.d.ts +0 -64
  36. package/dist/data-classification.js +0 -1
  37. package/dist/ecosystem/agent-scorer.d.ts +0 -12
  38. package/dist/ecosystem/agent-scorer.js +0 -5
  39. package/dist/ecosystem/agent.d.ts +0 -21
  40. package/dist/ecosystem/agent.js +0 -6
  41. package/dist/ecosystem/channel/local.d.ts +0 -3
  42. package/dist/ecosystem/channel/local.js +0 -3
  43. package/dist/ecosystem/channel/serverless.d.ts +0 -9
  44. package/dist/ecosystem/channel/serverless.js +0 -1
  45. package/dist/ecosystem/channel.d.ts +0 -15
  46. package/dist/ecosystem/channel.js +0 -5
  47. package/dist/ecosystem/cli/channel.d.ts +0 -16
  48. package/dist/ecosystem/cli/channel.js +0 -2
  49. package/dist/ecosystem/cli.d.ts +0 -9
  50. package/dist/ecosystem/cli.js +0 -2
  51. package/dist/ecosystem/credential.d.ts +0 -10
  52. package/dist/ecosystem/credential.js +0 -1
  53. package/dist/ecosystem/crypto-utils.d.ts +0 -8
  54. package/dist/ecosystem/crypto-utils.js +0 -1
  55. package/dist/ecosystem/dev.d.ts +0 -9
  56. package/dist/ecosystem/dev.js +0 -2
  57. package/dist/ecosystem/errors.d.ts +0 -8
  58. package/dist/ecosystem/errors.js +0 -2
  59. package/dist/ecosystem/function.d.ts +0 -13
  60. package/dist/ecosystem/function.js +0 -2
  61. package/dist/ecosystem/gateway.d.ts +0 -1
  62. package/dist/ecosystem/gateway.js +0 -1
  63. package/dist/ecosystem/hmac.d.ts +0 -1
  64. package/dist/ecosystem/hmac.js +0 -1
  65. package/dist/ecosystem/http.d.ts +0 -13
  66. package/dist/ecosystem/http.js +0 -5
  67. package/dist/ecosystem/mcp.d.ts +0 -3
  68. package/dist/ecosystem/mcp.js +0 -2
  69. package/dist/ecosystem/middleware.d.ts +0 -18
  70. package/dist/ecosystem/middleware.js +0 -7
  71. package/dist/ecosystem/node-host-resolver.d.ts +0 -1
  72. package/dist/ecosystem/node-host-resolver.js +0 -1
  73. package/dist/ecosystem/node.d.ts +0 -1
  74. package/dist/ecosystem/node.js +0 -1
  75. package/dist/ecosystem/oauth2.d.ts +0 -1
  76. package/dist/ecosystem/oauth2.js +0 -1
  77. package/dist/ecosystem/persona.d.ts +0 -17
  78. package/dist/ecosystem/persona.js +0 -5
  79. package/dist/ecosystem/queue.d.ts +0 -13
  80. package/dist/ecosystem/queue.js +0 -4
  81. package/dist/ecosystem/remote.d.ts +0 -8
  82. package/dist/ecosystem/remote.js +0 -1
  83. package/dist/ecosystem/role.d.ts +0 -4
  84. package/dist/ecosystem/role.js +0 -2
  85. package/dist/ecosystem/rpc.d.ts +0 -6
  86. package/dist/ecosystem/rpc.js +0 -3
  87. package/dist/ecosystem/safe-fetch.d.ts +0 -7
  88. package/dist/ecosystem/safe-fetch.js +0 -1
  89. package/dist/ecosystem/scenario.d.ts +0 -21
  90. package/dist/ecosystem/scenario.js +0 -8
  91. package/dist/ecosystem/scheduler.d.ts +0 -3
  92. package/dist/ecosystem/scheduler.js +0 -2
  93. package/dist/ecosystem/schema.d.ts +0 -2
  94. package/dist/ecosystem/schema.js +0 -1
  95. package/dist/ecosystem/scope.d.ts +0 -5
  96. package/dist/ecosystem/scope.js +0 -3
  97. package/dist/ecosystem/secret.d.ts +0 -11
  98. package/dist/ecosystem/secret.js +0 -2
  99. package/dist/ecosystem/services/istanbul-coverage.d.ts +0 -1
  100. package/dist/ecosystem/services/istanbul-coverage.js +0 -1
  101. package/dist/ecosystem/services/local-content-request-handler.d.ts +0 -9
  102. package/dist/ecosystem/services/local-content-request-handler.js +0 -1
  103. package/dist/ecosystem/services/local-content.d.ts +0 -2
  104. package/dist/ecosystem/services/local-content.js +0 -1
  105. package/dist/ecosystem/services/local-meta.d.ts +0 -1
  106. package/dist/ecosystem/services/local-meta.js +0 -1
  107. package/dist/ecosystem/services/v8-coverage.d.ts +0 -1
  108. package/dist/ecosystem/services/v8-coverage.js +0 -1
  109. package/dist/ecosystem/services.d.ts +0 -36
  110. package/dist/ecosystem/services.js +0 -12
  111. package/dist/ecosystem/testing.d.ts +0 -7
  112. package/dist/ecosystem/testing.js +0 -1
  113. package/dist/ecosystem/trigger.d.ts +0 -2
  114. package/dist/ecosystem/trigger.js +0 -1
  115. package/dist/ecosystem/types.d.ts +0 -12
  116. package/dist/ecosystem/types.js +0 -6
  117. package/dist/ecosystem/variable.d.ts +0 -9
  118. package/dist/ecosystem/variable.js +0 -1
  119. package/dist/ecosystem/virtual-user.d.ts +0 -24
  120. package/dist/ecosystem/virtual-user.js +0 -8
  121. package/dist/ecosystem/workflow.d.ts +0 -20
  122. package/dist/ecosystem/workflow.js +0 -6
  123. package/dist/ecosystem.d.ts +0 -27
  124. package/dist/ecosystem.js +0 -26
  125. package/dist/index.d.ts +0 -60
  126. package/dist/index.js +0 -25
  127. package/dist/middleware/require-unlocked.d.ts +0 -23
  128. package/dist/middleware/require-unlocked.js +0 -21
  129. package/dist/pikku-request.d.ts +0 -8
  130. package/dist/pikku-request.js +0 -15
  131. package/dist/secret-value.d.ts +0 -56
  132. package/dist/secret-value.js +0 -46
  133. package/dist/wirings/data-lock/data-lock-wiring.d.ts +0 -40
  134. package/dist/wirings/data-lock/data-lock-wiring.js +0 -77
  135. package/dist/wirings/data-lock/index.d.ts +0 -9
  136. package/dist/wirings/data-lock/index.js +0 -8
  137. package/dist/wirings/rpc/addon-runner.d.ts +0 -17
  138. package/dist/wirings/rpc/addon-runner.js +0 -150
  139. package/dist/wirings/rpc/remote-addon-auth.d.ts +0 -16
  140. package/dist/wirings/rpc/remote-addon-auth.js +0 -39
  141. package/dist/wirings/rpc/wire-addon.d.ts +0 -62
  142. package/dist/wirings/rpc/wire-addon.js +0 -88
  143. package/dist/wirings/rpc/wire-remote-addon.d.ts +0 -22
  144. 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 = createRequire(pathToFileURL(absPath));
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';
@@ -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
- this.loaded = true;
61
- if (!this.credentialService || !this.wire)
62
- return;
63
- const userId = defaultPikkuUserIdResolver(this.wire);
64
- if (!userId)
65
- return;
66
- this.wire.pikkuUserId = userId;
67
- const allCreds = await this.credentialService.getAll(userId);
68
- for (const [name, value] of Object.entries(allCreds)) {
69
- if (!(name in this.credentials)) {
70
- this.credentials[name] = value;
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) {
@@ -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';
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/core",
3
- "version": "0.12.104",
3
+ "version": "0.12.106",
4
4
  "description": "The Pikku runtime — functions, wirings, services, middleware and types",
5
5
  "author": "yasser.fadl@gmail.com",
6
6
  "license": "MIT",
@@ -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
  })
@@ -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 = createRequire(pathToFileURL(absPath))
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
 
@@ -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
+ )
@@ -16,6 +16,7 @@
16
16
  "pikkuMiddleware",
17
17
  "pikkuMiddlewareFactory",
18
18
  "pikkuRemoteAuthMiddleware",
19
+ "remoteJobsSecret",
19
20
  "requireOrigin",
20
21
  "runMiddleware",
21
22
  "telemetryInner",
@@ -98,7 +99,9 @@
98
99
  "reconstructStateAt"
99
100
  ],
100
101
  "./workflow/types": [],
101
- "./actor-flow": ["runConversation"],
102
+ "./actor-flow": [
103
+ "runConversation"
104
+ ],
102
105
  "./virtual-user": [
103
106
  "DEFAULT_MAX_INTERVAL_MS",
104
107
  "DEFAULT_MIN_INTERVAL_MS",
@@ -161,7 +164,9 @@
161
164
  "wireHTTP",
162
165
  "wireHTTPRoutes"
163
166
  ],
164
- "./remote": ["buildRemoteHeaders"],
167
+ "./remote": [
168
+ "buildRemoteHeaders"
169
+ ],
165
170
  "./queue": [
166
171
  "QueueJobDiscardedError",
167
172
  "QueueJobFailedError",
@@ -171,12 +176,17 @@
171
176
  "wireQueueWorker"
172
177
  ],
173
178
  "./scheduler": [
179
+ "ScheduledTaskNotFoundError",
174
180
  "getScheduledTasks",
175
181
  "logSchedulers",
176
182
  "runScheduledTask",
177
183
  "wireScheduler"
178
184
  ],
179
- "./trigger": ["PikkuTriggerService", "wireTrigger", "wireTriggerSource"],
185
+ "./trigger": [
186
+ "PikkuTriggerService",
187
+ "wireTrigger",
188
+ "wireTriggerSource"
189
+ ],
180
190
  "./rpc": [
181
191
  "PikkuRPCService",
182
192
  "RPCNotFoundError",
@@ -196,7 +206,10 @@
196
206
  "safeFetch",
197
207
  "setDefaultHostResolver"
198
208
  ],
199
- "./node-host-resolver": ["installNodeHostResolver", "nodeHostResolver"],
209
+ "./node-host-resolver": [
210
+ "installNodeHostResolver",
211
+ "nodeHostResolver"
212
+ ],
200
213
  "./mcp": [
201
214
  "MCPEndpointRegistry",
202
215
  "MCPError",
@@ -265,7 +278,10 @@
265
278
  "runCLICommand",
266
279
  "wireCLI"
267
280
  ],
268
- "./cli/command-parser": ["generateCommandHelp", "parseCLIArguments"],
281
+ "./cli/command-parser": [
282
+ "generateCommandHelp",
283
+ "parseCLIArguments"
284
+ ],
269
285
  "./cli/channel": [
270
286
  "executeCLIViaChannel",
271
287
  "executeRawCLIViaChannel",
@@ -318,8 +334,14 @@
318
334
  "verifyActorSecret",
319
335
  "verifyPersonaRoles"
320
336
  ],
321
- "./secret": ["defineSecret", "validateAndBuildSecretDefinitionsMeta"],
322
- "./variable": ["defineVariable", "validateAndBuildVariableDefinitionsMeta"],
337
+ "./secret": [
338
+ "defineSecret",
339
+ "validateAndBuildSecretDefinitionsMeta"
340
+ ],
341
+ "./variable": [
342
+ "defineVariable",
343
+ "validateAndBuildVariableDefinitionsMeta"
344
+ ],
323
345
  "./oauth2": [],
324
346
  "./errors": [
325
347
  "AIProviderAuthError",
@@ -394,6 +416,10 @@
394
416
  "PikkuCredentialWireService",
395
417
  "PikkuSessionService",
396
418
  "QueueWebhookService",
419
+ "REMOTE_JOBS_SECRET_HEADER",
420
+ "REMOTE_JOBS_SECRET_VARIABLE",
421
+ "REMOTE_QUEUE_JOB_PATH",
422
+ "REMOTE_SCHEDULER_JOB_PATH",
397
423
  "SchedulerService",
398
424
  "ScopedCredentialService",
399
425
  "ScopedSecretService",
@@ -412,6 +438,8 @@
412
438
  "deriveActorSecret",
413
439
  "getStubTracker",
414
440
  "isTestRun",
441
+ "pikkuRemoteQueueJobFunc",
442
+ "pikkuRemoteScheduledJobFunc",
415
443
  "pikkuWebhookWorkerFunc",
416
444
  "renderEmail",
417
445
  "scenarioArtifactContentType",
@@ -421,10 +449,19 @@
421
449
  "verifyActorSecret",
422
450
  "withoutSecrets"
423
451
  ],
424
- "./services/local-meta": ["LocalMetaService"],
425
- "./services/v8-coverage": ["V8CoverageService"],
426
- "./services/istanbul-coverage": ["IstanbulCoverageService"],
427
- "./services/local-content": ["LocalContent", "signedContentPath"],
452
+ "./services/local-meta": [
453
+ "LocalMetaService"
454
+ ],
455
+ "./services/v8-coverage": [
456
+ "V8CoverageService"
457
+ ],
458
+ "./services/istanbul-coverage": [
459
+ "IstanbulCoverageService"
460
+ ],
461
+ "./services/local-content": [
462
+ "LocalContent",
463
+ "signedContentPath"
464
+ ],
428
465
  "./services/local-content-request-handler": [
429
466
  "createLocalContentRequestHandler",
430
467
  "verifySignedContentRequest"
@@ -454,7 +491,10 @@
454
491
  "verifyWithKeyMaterial",
455
492
  "wrapDEK"
456
493
  ],
457
- "./hmac": ["hmacSha256Hex", "timingSafeStringEqual"],
494
+ "./hmac": [
495
+ "hmacSha256Hex",
496
+ "timingSafeStringEqual"
497
+ ],
458
498
  "./state": [
459
499
  "addPackageServiceFactories",
460
500
  "getAllPackageStates",
@@ -501,6 +541,13 @@
501
541
  "getSchema",
502
542
  "validateSchema"
503
543
  ],
504
- "./testing": ["clearPikkuRuntimeState", "defineServiceTests"],
505
- "./dev": ["pikkuDevReloader", "reconcileAddonRegistry", "reloadGeneratedMeta"]
544
+ "./testing": [
545
+ "clearPikkuRuntimeState",
546
+ "defineServiceTests"
547
+ ],
548
+ "./dev": [
549
+ "pikkuDevReloader",
550
+ "reconcileAddonRegistry",
551
+ "reloadGeneratedMeta"
552
+ ]
506
553
  }