@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.
- package/dist/crypto-utils.d.ts +3 -3
- package/dist/crypto-utils.js +3 -3
- package/dist/function/function-meta.types.d.ts +1 -1
- package/dist/function/function-runner.js +7 -0
- package/dist/function/functions.types.d.ts +8 -0
- package/dist/{remote.d.ts → mesh.d.ts} +2 -2
- package/dist/{remote.js → mesh.js} +12 -12
- package/dist/middleware/index.d.ts +2 -2
- package/dist/middleware/index.js +2 -2
- package/dist/middleware/{remote-auth.d.ts → mesh-auth.d.ts} +6 -6
- package/dist/middleware/{remote-auth.js → mesh-auth.js} +20 -20
- package/dist/types/core.types.d.ts +3 -3
- package/dist/types/state.types.d.ts +12 -7
- package/dist/wirings/addon/addon-reach.d.ts +10 -0
- package/dist/wirings/addon/addon-reach.js +29 -0
- package/dist/wirings/addon/index.d.ts +2 -2
- package/dist/wirings/addon/index.js +1 -1
- package/dist/wirings/addon/pikku-studio.d.ts +60 -0
- package/dist/wirings/addon/pikku-studio.js +7 -0
- package/dist/wirings/addon/remote-addon-auth.d.ts +10 -2
- package/dist/wirings/addon/remote-addon-auth.js +6 -1
- package/dist/wirings/addon/remote-addon-package.d.ts +13 -0
- package/dist/wirings/addon/remote-addon-package.js +21 -0
- package/dist/wirings/addon/wire-addon.d.ts +17 -13
- package/dist/wirings/addon/wire-addon.js +6 -6
- package/dist/wirings/addon/wire-remote-addon.d.ts +37 -3
- package/dist/wirings/addon/wire-remote-addon.js +18 -1
- package/dist/wirings/agent/agent-rpc.js +6 -3
- package/dist/wirings/channel/channel-rpc-service.d.ts +1 -1
- package/dist/wirings/channel/channel-rpc-service.js +1 -1
- package/dist/wirings/rpc/index.d.ts +1 -1
- package/dist/wirings/rpc/index.js +1 -1
- package/dist/wirings/rpc/rpc-runner.d.ts +15 -9
- package/dist/wirings/rpc/rpc-runner.js +215 -49
- package/dist/wirings/rpc/rpc-types.d.ts +8 -5
- package/dist/wirings/workflow/graph/graph-runner.js +2 -2
- package/dist/wirings/workflow/pikku-workflow-service.js +3 -4
- package/dist/wirings/workflow/workflow-compensation.js +2 -2
- package/dist/wirings/workflow/workflow-meta-resolver.d.ts +5 -0
- package/dist/wirings/workflow/workflow-meta-resolver.js +5 -0
- package/dist/wirings/workflow/workflow-queue-routing.js +2 -2
- package/package.json +3 -2
- package/dist/wirings/addon/define-screens.d.ts +0 -34
- 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(
|
|
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
|
-
/**
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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(
|
|
66
|
+
addError(RemoteAddonFailedError, {
|
|
63
67
|
status: 502,
|
|
64
|
-
message: 'Remote addon
|
|
68
|
+
message: 'Remote addon failed.',
|
|
65
69
|
});
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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(
|
|
72
|
-
status:
|
|
73
|
-
message: '
|
|
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.
|
|
171
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
590
|
+
async mesh(funcName, data) {
|
|
425
591
|
if (!this.services.deploymentService) {
|
|
426
|
-
throw new Error(`No DeploymentService configured for
|
|
427
|
-
`Set up a DeploymentService to enable
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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`, `
|
|
5
|
-
export type PikkuRPC<Invoke 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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
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
|
-
"./
|
|
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 {};
|