@pikku/core 0.12.145 → 0.12.147

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 (44) hide show
  1. package/dist/crypto-utils.d.ts +3 -3
  2. package/dist/crypto-utils.js +3 -3
  3. package/dist/function/function-meta.types.d.ts +1 -1
  4. package/dist/function/function-runner.js +7 -0
  5. package/dist/function/functions.types.d.ts +8 -0
  6. package/dist/{remote.d.ts → mesh.d.ts} +2 -2
  7. package/dist/{remote.js → mesh.js} +12 -12
  8. package/dist/middleware/index.d.ts +2 -2
  9. package/dist/middleware/index.js +2 -2
  10. package/dist/middleware/{remote-auth.d.ts → mesh-auth.d.ts} +6 -6
  11. package/dist/middleware/{remote-auth.js → mesh-auth.js} +20 -20
  12. package/dist/types/core.types.d.ts +3 -3
  13. package/dist/types/state.types.d.ts +12 -7
  14. package/dist/wirings/addon/addon-reach.d.ts +10 -0
  15. package/dist/wirings/addon/addon-reach.js +29 -0
  16. package/dist/wirings/addon/index.d.ts +2 -2
  17. package/dist/wirings/addon/index.js +1 -1
  18. package/dist/wirings/addon/pikku-studio.d.ts +60 -0
  19. package/dist/wirings/addon/pikku-studio.js +7 -0
  20. package/dist/wirings/addon/remote-addon-auth.d.ts +10 -2
  21. package/dist/wirings/addon/remote-addon-auth.js +6 -1
  22. package/dist/wirings/addon/remote-addon-package.d.ts +13 -0
  23. package/dist/wirings/addon/remote-addon-package.js +21 -0
  24. package/dist/wirings/addon/wire-addon.d.ts +17 -13
  25. package/dist/wirings/addon/wire-addon.js +6 -6
  26. package/dist/wirings/addon/wire-remote-addon.d.ts +37 -3
  27. package/dist/wirings/addon/wire-remote-addon.js +18 -1
  28. package/dist/wirings/agent/agent-rpc.js +6 -3
  29. package/dist/wirings/channel/channel-rpc-service.d.ts +1 -1
  30. package/dist/wirings/channel/channel-rpc-service.js +1 -1
  31. package/dist/wirings/rpc/index.d.ts +1 -1
  32. package/dist/wirings/rpc/index.js +1 -1
  33. package/dist/wirings/rpc/rpc-runner.d.ts +15 -9
  34. package/dist/wirings/rpc/rpc-runner.js +215 -49
  35. package/dist/wirings/rpc/rpc-types.d.ts +8 -5
  36. package/dist/wirings/workflow/graph/graph-runner.js +2 -2
  37. package/dist/wirings/workflow/pikku-workflow-service.js +3 -4
  38. package/dist/wirings/workflow/workflow-compensation.js +2 -2
  39. package/dist/wirings/workflow/workflow-meta-resolver.d.ts +5 -0
  40. package/dist/wirings/workflow/workflow-meta-resolver.js +5 -0
  41. package/dist/wirings/workflow/workflow-queue-routing.js +2 -2
  42. package/package.json +3 -2
  43. package/dist/wirings/addon/define-screens.d.ts +0 -34
  44. package/dist/wirings/addon/define-screens.js +0 -5
@@ -1,4 +1,4 @@
1
- import { addonCallerOf, addonMayReach } from '../addon/addon-reach.js';
1
+ import { AddonNotUsedError, addonCallerOf, addonMayReach, mapAddonCall, } from '../addon/addon-reach.js';
2
2
  import { forwardStepName, isCompensationStepName, } from '../../compensation-name.js';
3
3
  import { addonInstanceForNamespace } from '../addon/addon-runner.js';
4
4
  import { isAddonFunctionExposed } from '../addon/wire-addon.js';
@@ -6,6 +6,8 @@ import { pikkuState } from '../../pikku-state.js';
6
6
  import { PikkuError, addError } from '../../errors/error-handler.js';
7
7
  import { parseVersionedId } from '../../version.js';
8
8
  import { resolveRemoteAddonToken } from '../addon/remote-addon-auth.js';
9
+ import { DEFAULT_REMOTE_ADDON_TIMEOUT_MS, REMOTE_CONTRACT_HEADER, } from '../addon/wire-remote-addon.js';
10
+ import { BadRequestError, ConflictError, ForbiddenError, GoneError, LockedError, NotFoundError, PayloadTooLargeError, PaymentRequiredError, PreconditionFailedError, TooManyRequestsError, UnauthorizedError, UnprocessableContentError, UnsupportedMediaTypeError, } from '../../errors/errors.js';
9
11
  /**
10
12
  * The function runner, reached lazily. `function-runner` builds every
11
13
  * invocation's `wire.rpc` from this module, so a static import back would be a
@@ -31,8 +33,8 @@ const resolveWireSession = async (wire) => typeof wire.getSession === 'function'
31
33
  */
32
34
  export class RPCNotFoundError extends PikkuError {
33
35
  rpcName;
34
- constructor(rpcName) {
35
- super(`RPC function not found: ${rpcName}`);
36
+ constructor(rpcName, message = `RPC function not found: ${rpcName}`) {
37
+ super(message);
36
38
  this.rpcName = rpcName;
37
39
  }
38
40
  }
@@ -51,27 +53,120 @@ addError(RemoteAddonConfigError, {
51
53
  status: 500,
52
54
  message: 'Remote addon is misconfigured.',
53
55
  });
54
- /** Thrown when a remote addon answers an RPC with a non-2xx status. Maps to HTTP 502; the upstream status is kept on `httpStatus` and the first 300 characters of its body in the message. */
55
- export class RemoteAddonRequestError extends PikkuError {
56
- httpStatus;
57
- constructor(namespace, fnName, status, detail) {
58
- super(`Remote addon '${namespace}:${fnName}' returned ${status}${detail ? `: ${detail}` : ''}`);
59
- this.httpStatus = status;
56
+ /**
57
+ * Thrown when a remote addon fails on its own side — a 5xx, an unreachable
58
+ * host, or a response that is not JSON. Maps to HTTP 502. What went wrong is
59
+ * logged on this host; none of it reaches the caller.
60
+ */
61
+ export class RemoteAddonFailedError extends PikkuError {
62
+ constructor(namespace, fnName) {
63
+ super(`Remote addon '${namespace}:${fnName}' failed`);
60
64
  }
61
65
  }
62
- addError(RemoteAddonRequestError, {
66
+ addError(RemoteAddonFailedError, {
63
67
  status: 502,
64
- message: 'Remote addon request failed.',
68
+ message: 'Remote addon failed.',
65
69
  });
66
- export class AddonNotUsedError extends PikkuError {
67
- constructor(caller, target) {
68
- super(`Addon '${caller}' called '${target}' but does not list it in its wireAddon 'uses'.`);
70
+ /** Thrown when a remote addon does not answer within the wiring's `timeoutMs`. Maps to HTTP 504. */
71
+ export class RemoteAddonTimeoutError extends PikkuError {
72
+ constructor(namespace, fnName, timeoutMs) {
73
+ super(`Remote addon '${namespace}:${fnName}' did not answer within ${timeoutMs}ms`);
69
74
  }
70
75
  }
71
- addError(AddonNotUsedError, {
72
- status: 403,
73
- message: 'Addon called another addon it does not use.',
76
+ addError(RemoteAddonTimeoutError, {
77
+ status: 504,
78
+ message: 'Remote addon timed out.',
74
79
  });
80
+ /** Headers a wiring's `headers` hook cannot set: the call's auth, trace and contract. */
81
+ const RESERVED_REMOTE_HEADERS = new Set([
82
+ 'authorization',
83
+ 'x-request-id',
84
+ REMOTE_CONTRACT_HEADER,
85
+ 'content-type',
86
+ 'accept',
87
+ ]);
88
+ /**
89
+ * The plain error for each 4xx a remote addon may answer with, used when its
90
+ * body names no error this host knows. Constructed without a message, so the
91
+ * caller gets the registered generic one.
92
+ */
93
+ const GENERIC_CLIENT_ERRORS = {
94
+ 400: BadRequestError,
95
+ 401: UnauthorizedError,
96
+ 402: PaymentRequiredError,
97
+ 403: ForbiddenError,
98
+ 404: NotFoundError,
99
+ 409: ConflictError,
100
+ 410: GoneError,
101
+ 412: PreconditionFailedError,
102
+ 413: PayloadTooLargeError,
103
+ 415: UnsupportedMediaTypeError,
104
+ 422: UnprocessableContentError,
105
+ 423: LockedError,
106
+ 429: TooManyRequestsError,
107
+ };
108
+ /**
109
+ * The registered error class named `name` whose status is `status`, rebuilt
110
+ * without running its constructor — a constructor's arguments are not on the
111
+ * wire, and running one with the wrong ones could throw or mislabel the error.
112
+ */
113
+ const rebuildRegisteredError = (name, status, message) => {
114
+ if (typeof name !== 'string' || !name) {
115
+ return undefined;
116
+ }
117
+ for (const [ErrorClass, details] of pikkuState(null, 'misc', 'errors')) {
118
+ if (ErrorClass.name !== name || details.status !== status) {
119
+ continue;
120
+ }
121
+ const error = Object.create(ErrorClass.prototype);
122
+ Object.defineProperty(error, 'name', {
123
+ value: name,
124
+ writable: true,
125
+ configurable: true,
126
+ });
127
+ Object.defineProperty(error, 'message', {
128
+ value: typeof message === 'string' && message ? message : details.message,
129
+ writable: true,
130
+ configurable: true,
131
+ });
132
+ Error.captureStackTrace?.(error, rebuildRegisteredError);
133
+ return error;
134
+ }
135
+ return undefined;
136
+ };
137
+ /**
138
+ * The error a non-2xx response from a remote addon becomes on this host.
139
+ *
140
+ * A 4xx naming an error registered here with the same status is rebuilt as
141
+ * that error, message included — the addon already chose to show it to its
142
+ * caller. Any other 4xx becomes the generic error for its status. A 5xx, or a
143
+ * status with no generic error, becomes {@link RemoteAddonFailedError}. The
144
+ * response body is never passed through: anything beyond the rebuilt message
145
+ * is logged here and goes no further.
146
+ *
147
+ * knowledge: decisions/security/remote-addon-errors-are-rebuilt-not-passed-through.md
148
+ */
149
+ const remoteAddonResponseError = async (res, namespace, fnName, logger) => {
150
+ const body = await res.text().catch(() => '');
151
+ if (res.status >= 400 && res.status < 500) {
152
+ let parsed;
153
+ try {
154
+ parsed = JSON.parse(body);
155
+ }
156
+ catch { }
157
+ const rebuilt = rebuildRegisteredError(parsed?.name, res.status, parsed?.message);
158
+ if (rebuilt) {
159
+ return rebuilt;
160
+ }
161
+ const GenericError = GENERIC_CLIENT_ERRORS[res.status];
162
+ if (GenericError) {
163
+ return new GenericError();
164
+ }
165
+ }
166
+ logger.error(`Remote addon '${namespace}:${fnName}' answered ${res.status}: ${body.slice(0, 1000)}`);
167
+ return new RemoteAddonFailedError(namespace, fnName);
168
+ };
169
+ export { AddonNotUsedError };
75
170
  export const resolveNamespace = (namespacedFunction) => {
76
171
  const colonIndex = namespacedFunction.indexOf(':');
77
172
  if (colonIndex === -1) {
@@ -131,6 +226,27 @@ const resolvePikkuFunction = (rpcName, packageName = null) => {
131
226
  }
132
227
  return { pikkuFuncId: rpcMeta, packageName: null };
133
228
  };
229
+ /**
230
+ * A caller from outside names `fn@vN` because it was built against that
231
+ * version. Serving it the latest instead would hand it a contract it does not
232
+ * know, so a version this host no longer has is refused, not resolved to the
233
+ * base name the way an internal caller's is. An unversioned function is its
234
+ * own v1.
235
+ */
236
+ const refuseUnknownVersion = (rpcName) => {
237
+ const { baseName, version } = parseVersionedId(rpcName);
238
+ if (version === null)
239
+ return;
240
+ const rpc = pikkuState(null, 'rpc', 'meta');
241
+ if (rpc[rpcName])
242
+ return;
243
+ const current = rpc[baseName];
244
+ if (!current)
245
+ return;
246
+ if (version === 1 && parseVersionedId(current).version === null)
247
+ return;
248
+ throw new RPCNotFoundError(rpcName, `RPC function not found: ${rpcName}. Version ${version} of '${baseName}' is no longer served here; update the SDK this call was built against.`);
249
+ };
134
250
  /**
135
251
  * Marks an addon RPC name that could not be resolved. Module-private on purpose:
136
252
  * resolution failure must never be conflated with an `RPCNotFoundError` thrown by
@@ -157,22 +273,36 @@ export class ContextAwareRPCService {
157
273
  let resolvedAddon = null;
158
274
  if (funcName.includes(':')) {
159
275
  resolvedAddon = resolveNamespace(funcName);
276
+ if (resolvedAddon?.addonConfig.remote) {
277
+ // A remote addon's functions have no meta on this host. Its contract
278
+ // stands in for the addon's own `expose`, and the host enforces it:
279
+ // every forwarded call carries REMOTE_CONTRACT_HEADER.
280
+ if (!isAddonFunctionExposed(resolvedAddon.addonConfig.expose, parseVersionedId(resolvedAddon.function).baseName, true)) {
281
+ throw new RPCNotFoundError(funcName);
282
+ }
283
+ return await this.rpc(funcName, data);
284
+ }
160
285
  if (resolvedAddon) {
161
286
  functionMeta = pikkuState(resolvedAddon.package, 'function', 'meta')[resolvedAddon.function];
162
287
  }
163
288
  }
164
289
  else {
290
+ if (!this.packageName)
291
+ refuseUnknownVersion(funcName);
165
292
  const resolved = resolvePikkuFunction(funcName, this.packageName);
166
293
  functionMeta = pikkuState(resolved.packageName, 'function', 'meta')[resolved.pikkuFuncId];
167
294
  }
168
295
  if (!functionMeta) {
296
+ // Whether a function is in the remote contract is only known where its
297
+ // meta is, so a contract call never leaves this unit.
298
+ if (this.wire.http?.request?.header(REMOTE_CONTRACT_HEADER)) {
299
+ throw new RPCNotFoundError(funcName);
300
+ }
169
301
  // The addon runs in another deploy unit, which only carries the
170
- // functions the analyzer found exposed. A wiring present here still
171
- // gets to narrow what is forwarded.
302
+ // functions the analyzer found exposed. The wiring present here must
303
+ // still opt in to the function before it is forwarded.
172
304
  const refusedHere = resolvedAddon &&
173
- (resolvedAddon.addonConfig.expose === false ||
174
- (Array.isArray(resolvedAddon.addonConfig.expose) &&
175
- !resolvedAddon.addonConfig.expose.includes(resolvedAddon.function)));
305
+ !isAddonFunctionExposed(resolvedAddon.addonConfig.expose, resolvedAddon.function, true);
176
306
  if (funcName.includes(':') &&
177
307
  this.services.deploymentService &&
178
308
  !refusedHere) {
@@ -186,6 +316,10 @@ export class ContextAwareRPCService {
186
316
  if (!exposed || functionMeta.scenarioStep) {
187
317
  throw new RPCNotFoundError(funcName);
188
318
  }
319
+ if (this.wire.http?.request?.header(REMOTE_CONTRACT_HEADER) &&
320
+ functionMeta.remote !== true) {
321
+ throw new RPCNotFoundError(funcName);
322
+ }
189
323
  return await this.rpc(funcName, data);
190
324
  }
191
325
  async rpc(funcName, data) {
@@ -225,18 +359,7 @@ export class ContextAwareRPCService {
225
359
  });
226
360
  }
227
361
  mapUsedAddon(funcName) {
228
- const caller = this.wire.addonNamespace;
229
- if (!caller)
230
- return funcName;
231
- const colon = funcName.indexOf(':');
232
- const prefix = funcName.slice(0, colon);
233
- if (prefix === caller)
234
- return funcName;
235
- const uses = pikkuState(null, 'addons', 'packages').get(caller)?.uses;
236
- const mapped = uses?.[prefix];
237
- if (mapped)
238
- return `${mapped}${funcName.slice(colon)}`;
239
- throw new AddonNotUsedError(caller, funcName);
362
+ return mapAddonCall(this.wire.addonNamespace, funcName);
240
363
  }
241
364
  /**
242
365
  * Resolution half of an addon RPC call: never executes anything, so an
@@ -323,33 +446,76 @@ export class ContextAwareRPCService {
323
446
  if (!serverUrl) {
324
447
  throw new RemoteAddonConfigError(namespace, 'serverUrl resolved empty');
325
448
  }
326
- const remoteFn = cfg.remoteName ? cfg.remoteName(fnName) : fnName;
449
+ // The host serves `fn` as its own latest, which may be newer than the
450
+ // contract this consumer was built against, so a call always names a
451
+ // version: the caller's, or the newest in the SDK (registered by codegen).
452
+ const versionedFn = parseVersionedId(fnName).version !== null
453
+ ? fnName
454
+ : (pikkuState(cfg.package, 'rpc', 'meta')[fnName] ?? fnName);
455
+ // `remoteName` maps the base name; the version rides on the mapped name,
456
+ // so a lookup-table mapping still matches a versioned call.
457
+ const { baseName, version } = parseVersionedId(versionedFn);
458
+ const remoteFn = cfg.remoteName
459
+ ? `${cfg.remoteName(baseName)}${version !== null ? `@v${version}` : ''}`
460
+ : versionedFn;
327
461
  const token = await resolveRemoteAddonToken(cfg.remoteAuth, this.services, this.wire, namespace);
328
462
  const headers = {
329
463
  'content-type': 'application/json',
330
464
  accept: 'application/json',
331
465
  };
466
+ if (cfg.remoteHeaders) {
467
+ for (const [name, value] of Object.entries(await cfg.remoteHeaders(this.services, this.wire))) {
468
+ // Matched case-insensitively: `Authorization` next to `authorization`
469
+ // would reach the host as one combined value.
470
+ if (!RESERVED_REMOTE_HEADERS.has(name.toLowerCase())) {
471
+ headers[name] = value;
472
+ }
473
+ }
474
+ }
475
+ headers[REMOTE_CONTRACT_HEADER] = '1';
332
476
  if (token) {
333
477
  headers.authorization = `Bearer ${token}`;
334
478
  }
335
479
  if (this.wire.traceId) {
336
480
  headers['x-request-id'] = this.wire.traceId;
337
481
  }
482
+ const timeoutMs = cfg.remoteTimeoutMs ?? DEFAULT_REMOTE_ADDON_TIMEOUT_MS;
338
483
  const base = serverUrl.replace(/\/+$/, '');
339
- const res = await fetch(`${base}/remote/rpc/${encodeURIComponent(remoteFn)}`, {
340
- method: 'POST',
341
- headers,
342
- body: JSON.stringify({ rpcName: remoteFn, data }),
343
- });
484
+ let res;
485
+ try {
486
+ // The body the generated `POST /rpc/:rpcName` route reads, the same one
487
+ // the generated fetch client sends.
488
+ res = await fetch(`${base}/rpc/${encodeURIComponent(remoteFn)}`, {
489
+ method: 'POST',
490
+ headers,
491
+ body: JSON.stringify({ rpcName: remoteFn, data }),
492
+ signal: AbortSignal.timeout(timeoutMs),
493
+ });
494
+ }
495
+ catch (e) {
496
+ if (e?.name === 'TimeoutError') {
497
+ throw new RemoteAddonTimeoutError(namespace, remoteFn, timeoutMs);
498
+ }
499
+ this.services.logger.error(`Remote addon '${namespace}:${remoteFn}' could not be reached: ${e instanceof Error ? e.message : e}`);
500
+ throw new RemoteAddonFailedError(namespace, remoteFn);
501
+ }
344
502
  if (!res.ok) {
345
- const detail = (await res.text().catch(() => '')).slice(0, 300);
346
- throw new RemoteAddonRequestError(namespace, remoteFn, res.status, detail);
503
+ throw await remoteAddonResponseError(res, namespace, remoteFn, this.services.logger);
347
504
  }
348
505
  if (res.status === 204) {
349
506
  return undefined;
350
507
  }
351
508
  const text = await res.text();
352
- return (text ? JSON.parse(text) : undefined);
509
+ if (!text) {
510
+ return undefined;
511
+ }
512
+ try {
513
+ return JSON.parse(text);
514
+ }
515
+ catch {
516
+ this.services.logger.error(`Remote addon '${namespace}:${remoteFn}' answered ${res.status} with a body that is not JSON`);
517
+ throw new RemoteAddonFailedError(namespace, remoteFn);
518
+ }
353
519
  }
354
520
  async rpcWithWire(rpcName, data, wire) {
355
521
  const mergedWire = {
@@ -421,10 +587,10 @@ export class ContextAwareRPCService {
421
587
  }
422
588
  return createAgentRPC(this.wire, this.options);
423
589
  }
424
- async remote(funcName, data) {
590
+ async mesh(funcName, data) {
425
591
  if (!this.services.deploymentService) {
426
- throw new Error(`No DeploymentService configured for remote RPC: ${funcName}. ` +
427
- `Set up a DeploymentService to enable remote function calls.`);
592
+ throw new Error(`No DeploymentService configured for mesh RPC: ${funcName}. ` +
593
+ `Set up a DeploymentService to enable mesh function calls.`);
428
594
  }
429
595
  const session = await resolveWireSession(this.wire);
430
596
  return this.services.deploymentService.invoke(funcName, data, session, this.wire.traceId);
@@ -437,7 +603,7 @@ export class ContextAwareRPCService {
437
603
  * `DeploymentService` when the function lives in another deployment unit.
438
604
  * `rpc.exposed` does the same but only reaches functions marked `expose: true`
439
605
  * (and, for addons, those the consuming app exposes), which is what the public
440
- * RPC endpoint uses. `rpc.remote` always goes through the `DeploymentService`.
606
+ * RPC endpoint uses. `rpc.mesh` always goes through the `DeploymentService`.
441
607
  * Calls carry the caller's wire, so the session and trace ID propagate. An addon
442
608
  * can only call the addons it lists in `uses`.
443
609
  *
@@ -473,7 +639,7 @@ class ContextRPCView {
473
639
  depth;
474
640
  global = false;
475
641
  invoke;
476
- remote;
642
+ mesh;
477
643
  exposed;
478
644
  startWorkflow;
479
645
  rpcWithWire;
@@ -481,7 +647,7 @@ class ContextRPCView {
481
647
  this.serviceRPC = serviceRPC;
482
648
  this.depth = depth;
483
649
  this.invoke = serviceRPC.rpc.bind(serviceRPC);
484
- this.remote = serviceRPC.remote.bind(serviceRPC);
650
+ this.mesh = serviceRPC.mesh.bind(serviceRPC);
485
651
  this.exposed = serviceRPC.rpcExposed.bind(serviceRPC);
486
652
  this.startWorkflow = serviceRPC.startWorkflow.bind(serviceRPC);
487
653
  this.rpcWithWire = serviceRPC.rpcWithWire.bind(serviceRPC);
@@ -1,12 +1,12 @@
1
1
  import type { CredentialOverrides } from '../credential/credential-overrides.js';
2
2
  import type { PikkuRawWire } from '../../types/core.types.js';
3
3
  import type { AgentInterruptResult } from '../agent/agent-interrupt.js';
4
- /** The `rpc` object on a function's wire. An app's generated types narrow `invoke`, `remote`, `startWorkflow` and `agent` to its own functions, workflows and agents; see {@link PikkuRPCService} for how each call is routed. */
5
- export type PikkuRPC<Invoke extends (...args: any[]) => any = (...args: any[]) => any, Remote extends (...args: any[]) => any = (...args: any[]) => any, startWorkflow extends (...args: any[]) => any = (...args: any[]) => any, AgentRun extends (...args: any[]) => any = (...args: any[]) => any, AgentStream extends (...args: any[]) => any = (...args: any[]) => any> = {
4
+ /** The `rpc` object on a function's wire. An app's generated types narrow `invoke`, `mesh`, `startWorkflow` and `agent` to its own functions, workflows and agents; see {@link PikkuRPCService} for how each call is routed. */
5
+ export type PikkuRPC<Invoke extends (...args: any[]) => any = (...args: any[]) => any, Mesh extends (...args: any[]) => any = (...args: any[]) => any, startWorkflow extends (...args: any[]) => any = (...args: any[]) => any, AgentRun extends (...args: any[]) => any = (...args: any[]) => any, AgentStream extends (...args: any[]) => any = (...args: any[]) => any> = {
6
6
  depth: number;
7
7
  global: boolean;
8
8
  invoke: Invoke;
9
- remote: Remote;
9
+ mesh: Mesh;
10
10
  exposed: (name: string, data: any) => Promise<any>;
11
11
  /** Invoke an RPC with explicit wire fields merged over the caller's. */
12
12
  rpcWithWire: <In = any, Out = any>(rpcName: string, data: In, wire: PikkuRawWire) => Promise<Out>;
@@ -47,7 +47,7 @@ export interface ResolvedFunction {
47
47
  package: string;
48
48
  auth?: boolean;
49
49
  tags?: string[];
50
- /** Set by the consuming app: which functions `rpc.exposed` may call */
50
+ /** Set by the consuming app: which of the addon's declared-exposed functions `rpc.exposed` may call */
51
51
  expose?: boolean | string[];
52
52
  rpcEndpoint?: string;
53
53
  secretOverrides?: Record<string, string>;
@@ -75,8 +75,11 @@ export interface ResolvedFunction {
75
75
  } | {
76
76
  secretId: string;
77
77
  } | {
78
- resolve: (services: any, wire: any) => string | Promise<string>;
78
+ resolve: (services: any, wire: any) => string | null | Promise<string | null>;
79
79
  };
80
+ /** Maps the base fn name; a versioned call appends `@vN` after it. */
80
81
  remoteName?: (fn: string) => string;
82
+ remoteTimeoutMs?: number;
83
+ remoteHeaders?: (services: any, wire: any) => Record<string, string> | Promise<Record<string, string>>;
81
84
  };
82
85
  }
@@ -737,7 +737,7 @@ async function invokeGraphNodeRpc(workflowService, rpcService, runId, stepId, no
737
737
  export async function executeGraphStep(workflowService, rpcService, runId, stepId, nodeId, rpcName, data, graphName) {
738
738
  try {
739
739
  let result;
740
- const subWorkflowMeta = pikkuState(null, 'workflows', 'meta')[rpcName];
740
+ const subWorkflowMeta = getWorkflowMeta(rpcName);
741
741
  const agentMeta = subWorkflowMeta
742
742
  ? undefined
743
743
  : pikkuState(null, 'agent', 'agentsMeta')[rpcName];
@@ -801,7 +801,7 @@ async function executeGraphNodeInline(workflowService, rpcService, runId, graphN
801
801
  await workflowService.setStepRunning(stepState.stepId);
802
802
  try {
803
803
  let result;
804
- const subWorkflowMeta = pikkuState(null, 'workflows', 'meta')[rpcName];
804
+ const subWorkflowMeta = getWorkflowMeta(rpcName);
805
805
  const agentMeta = subWorkflowMeta
806
806
  ? undefined
807
807
  : pikkuState(null, 'agent', 'agentsMeta')[rpcName];
@@ -691,8 +691,7 @@ export class PikkuWorkflowService {
691
691
  if (!run) {
692
692
  throw new Error(`Workflow run not found: ${runId}`);
693
693
  }
694
- const meta = pikkuState(null, 'workflows', 'meta');
695
- const workflowMeta = meta[run.workflow];
694
+ const workflowMeta = resolveWorkflowMeta(run.workflow)?.meta;
696
695
  const isGraphWorkflow = workflowMeta?.source === 'graph';
697
696
  let graphNodeId;
698
697
  if (isGraphWorkflow && workflowMeta?.nodes) {
@@ -709,7 +708,7 @@ export class PikkuWorkflowService {
709
708
  result = await executeGraphStep(this, rpcService, runId, stepState.stepId, graphNodeId, workflowMeta.nodes[graphNodeId].rpcName, data, run.workflow);
710
709
  }
711
710
  else {
712
- if (meta[rpcName]) {
711
+ if (resolveWorkflowMeta(rpcName)) {
713
712
  result = await startQueuedChildStep({
714
713
  startWorkflow: (...args) => this.startWorkflow(...args),
715
714
  setStepChildRunId: (...args) => this.setStepChildRunId(...args),
@@ -860,7 +859,7 @@ export class PikkuWorkflowService {
860
859
  if (resolvedStepOptions.virtualUser) {
861
860
  return resolvedStepOptions.virtualUser.invoke(rpcName, data);
862
861
  }
863
- if (pikkuState(null, 'workflows', 'meta')[rpcName]) {
862
+ if (resolveWorkflowMeta(rpcName)) {
864
863
  return runInlineChildStep({
865
864
  startWorkflow: (...args) => this.startWorkflow(...args),
866
865
  setStepChildRunId: (...args) => this.setStepChildRunId(...args),
@@ -2,6 +2,7 @@ import { DEFAULT_STEP_RETRIES, NO_COMPENSATE_STATE_PREFIX, WORKFLOW_TERMINAL_STA
2
2
  import { pikkuState } from '../../pikku-state.js';
3
3
  import { resolveNamespace } from '../rpc/rpc-runner.js';
4
4
  import { runningStepLease } from './workflow-step-lease.js';
5
+ import { isWorkflowName } from './workflow-meta-resolver.js';
5
6
  import { planUnwind } from './workflow-unwind-plan.js';
6
7
  const ENTERABLE = new Set([
7
8
  'running',
@@ -177,8 +178,7 @@ export const functionHasCompensation = (rpcName) => {
177
178
  ?.compensate === true
178
179
  : false;
179
180
  };
180
- export const isChildWorkflowRecord = (record) => !!record.rpcName &&
181
- pikkuState(null, 'workflows', 'meta')[record.rpcName] !== undefined;
181
+ export const isChildWorkflowRecord = (record) => !!record.rpcName && isWorkflowName(record.rpcName);
182
182
  export const recordHasCompensation = (record) => isChildWorkflowRecord(record)
183
183
  ? !!record.childRunId
184
184
  : !!record.rpcName && functionHasCompensation(record.rpcName);
@@ -9,3 +9,8 @@ export type ResolvedWorkflowMeta = {
9
9
  * `namespace:name` form — against the addon package that claims the namespace.
10
10
  */
11
11
  export declare const resolveWorkflowMeta: (name: string) => ResolvedWorkflowMeta | null;
12
+ /**
13
+ * Whether a step's rpcName names a workflow — the app's, or an addon's by
14
+ * `namespace:name` — and so starts a child run rather than an RPC.
15
+ */
16
+ export declare const isWorkflowName: (name: string) => boolean;
@@ -29,3 +29,8 @@ export const resolveWorkflowMeta = (name) => {
29
29
  resolvedName: localName,
30
30
  };
31
31
  };
32
+ /**
33
+ * Whether a step's rpcName names a workflow — the app's, or an addon's by
34
+ * `namespace:name` — and so starts a child run rather than an RPC.
35
+ */
36
+ export const isWorkflowName = (name) => resolveWorkflowMeta(name) !== null;
@@ -1,6 +1,7 @@
1
1
  import { getSingletonServices, pikkuState } from '../../pikku-state.js';
2
2
  import { getDurationInMilliseconds } from '../../time-utils.js';
3
3
  import { DEFAULT_STEP_RETRIES, forwardStepName } from './workflow-constants.js';
4
+ import { isWorkflowName } from './workflow-meta-resolver.js';
4
5
  const toKebab = (s) => s.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
5
6
  export const resolveWorkflowConfig = () => {
6
7
  const workflow = getSingletonServices().config?.workflow;
@@ -54,8 +55,7 @@ export const stepDispatchTarget = async (rpcName, stepName, parentIsInline) => {
54
55
  }
55
56
  return 'queue';
56
57
  }
57
- const isWorkflow = pikkuState(null, 'workflows', 'meta')[rpcName] !== undefined;
58
- if (!isWorkflow || !hasQueue) {
58
+ if (!isWorkflowName(rpcName) || !hasQueue) {
59
59
  return 'inline';
60
60
  }
61
61
  return (await parentIsInline()) ? 'inline' : 'queue';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/core",
3
- "version": "0.12.145",
3
+ "version": "0.12.147",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/pikkujs/pikku.git",
@@ -24,6 +24,7 @@
24
24
  },
25
25
  "sideEffects": [
26
26
  "./dist/errors/errors.js",
27
+ "./dist/wirings/addon/addon-reach.js",
27
28
  "./dist/wirings/addon/remote-addon-auth.js",
28
29
  "./dist/wirings/agent/agent-rpc.js",
29
30
  "./dist/wirings/rpc/rpc-runner.js",
@@ -49,7 +50,7 @@
49
50
  "./channel/local": "./dist/wirings/channel/local/index.js",
50
51
  "./channel/serverless": "./dist/wirings/channel/serverless/index.js",
51
52
  "./http": "./dist/wirings/http/index.js",
52
- "./remote": "./dist/remote.js",
53
+ "./mesh": "./dist/mesh.js",
53
54
  "./queue": "./dist/wirings/queue/index.js",
54
55
  "./scheduler": "./dist/wirings/scheduler/index.js",
55
56
  "./trigger": "./dist/wirings/trigger/index.js",
@@ -1,34 +0,0 @@
1
- type AddonScreenBase = {
2
- /** Where the screen mounts under the addon, e.g. `/` or `/:id`. */
3
- path: string;
4
- title: string;
5
- /** Lists the screen in the host's navigation. */
6
- nav?: boolean;
7
- /** Required of the viewer to see the screen. The addon's functions must enforce the same scopes: hiding a screen is not access control. */
8
- scopes?: string[];
9
- };
10
- /** A screen rendered by the host from the addon's source. */
11
- export type AddonComponentScreen = AddonScreenBase & {
12
- component: () => Promise<{
13
- default: unknown;
14
- }>;
15
- };
16
- /** A static program the host serves in a sandboxed iframe. */
17
- export type AddonAppScreen = AddonScreenBase & {
18
- /** Directory of the built program, relative to the addon package. */
19
- app: string;
20
- };
21
- /** One screen an addon ships: either a component the host renders or a static app it serves in a sandboxed iframe. */
22
- export type AddonScreen = AddonComponentScreen | AddonAppScreen;
23
- /** The manifest passed to `defineScreens`: the addon's title, optional icon and its screens. */
24
- export type ScreensManifest = {
25
- title: string;
26
- icon?: string;
27
- screens: AddonScreen[];
28
- };
29
- /**
30
- * Declares the screens an addon ships, which the host mounts when it is wired
31
- * with `ui: true`. Read statically by `pikku all`, so `path`, `title`, `nav`, `scopes` and `app` must be literals.
32
- */
33
- export declare const defineScreens: (manifest: ScreensManifest) => ScreensManifest;
34
- export {};
@@ -1,5 +0,0 @@
1
- /**
2
- * Declares the screens an addon ships, which the host mounts when it is wired
3
- * with `ui: true`. Read statically by `pikku all`, so `path`, `title`, `nav`, `scopes` and `app` must be literals.
4
- */
5
- export const defineScreens = (manifest) => manifest;