@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
package/dist/crypto-utils.d.ts
CHANGED
|
@@ -24,10 +24,10 @@ export declare const encryptJSON: (secret: string, value: unknown) => Promise<st
|
|
|
24
24
|
*/
|
|
25
25
|
export declare const decryptJSON: <T>(secret: string, token: string) => Promise<T>;
|
|
26
26
|
/**
|
|
27
|
-
* Namespaces the
|
|
27
|
+
* Namespaces the mesh-RPC session key so the same deployment secret used for
|
|
28
28
|
* another purpose derives a different key. See knowledge/crypto.md.
|
|
29
29
|
*/
|
|
30
|
-
export declare const
|
|
30
|
+
export declare const MESH_SESSION_INFO = "pikku:mesh-session";
|
|
31
31
|
/** Shortest key material, in characters, that the `*WithKeyMaterial` helpers accept. */
|
|
32
32
|
export declare const MIN_KEY_MATERIAL_LENGTH = 32;
|
|
33
33
|
/**
|
|
@@ -44,7 +44,7 @@ export declare const assertStrongKeyMaterial: (name: string, keyMaterial: string
|
|
|
44
44
|
* high-entropy key material by HKDF-SHA256, with a fresh random salt per call.
|
|
45
45
|
*
|
|
46
46
|
* `info` domain-separates the derived key, so the same deployment secret used for
|
|
47
|
-
* two purposes (e.g. {@link
|
|
47
|
+
* two purposes (e.g. {@link MESH_SESSION_INFO}) yields two unrelated keys.
|
|
48
48
|
*
|
|
49
49
|
* @param name - The key material's name, for the weak-key error.
|
|
50
50
|
* @param keyMaterial - At least {@link MIN_KEY_MATERIAL_LENGTH} characters.
|
package/dist/crypto-utils.js
CHANGED
|
@@ -109,10 +109,10 @@ export const decryptJSON = async (secret, token) => {
|
|
|
109
109
|
return JSON.parse(decoder.decode(new Uint8Array(decrypted)));
|
|
110
110
|
};
|
|
111
111
|
/**
|
|
112
|
-
* Namespaces the
|
|
112
|
+
* Namespaces the mesh-RPC session key so the same deployment secret used for
|
|
113
113
|
* another purpose derives a different key. See knowledge/crypto.md.
|
|
114
114
|
*/
|
|
115
|
-
export const
|
|
115
|
+
export const MESH_SESSION_INFO = 'pikku:mesh-session';
|
|
116
116
|
/** Shortest key material, in characters, that the `*WithKeyMaterial` helpers accept. */
|
|
117
117
|
export const MIN_KEY_MATERIAL_LENGTH = 32;
|
|
118
118
|
/**
|
|
@@ -143,7 +143,7 @@ const expandKeyMaterial = async (keyMaterial, info, salt) => {
|
|
|
143
143
|
* high-entropy key material by HKDF-SHA256, with a fresh random salt per call.
|
|
144
144
|
*
|
|
145
145
|
* `info` domain-separates the derived key, so the same deployment secret used for
|
|
146
|
-
* two purposes (e.g. {@link
|
|
146
|
+
* two purposes (e.g. {@link MESH_SESSION_INFO}) yields two unrelated keys.
|
|
147
147
|
*
|
|
148
148
|
* @param name - The key material's name, for the weak-key error.
|
|
149
149
|
* @param keyMaterial - At least {@link MIN_KEY_MATERIAL_LENGTH} characters.
|
|
@@ -165,7 +165,7 @@ export type FunctionMeta = FunctionRuntimeMeta & Partial<{
|
|
|
165
165
|
exportedName: string;
|
|
166
166
|
/**
|
|
167
167
|
* RPC names this function's own body calls by literal name through
|
|
168
|
-
* `rpc.invoke(...)` / `rpc.
|
|
168
|
+
* `rpc.invoke(...)` / `rpc.mesh(...)`. The deploy planner binds the
|
|
169
169
|
* function's unit to each callee's unit from this; a call with a computed
|
|
170
170
|
* name is not listed, and is warned about instead.
|
|
171
171
|
*/
|
|
@@ -62,6 +62,13 @@ async function resolveSession(wire, singletonServices, sessionService) {
|
|
|
62
62
|
* @param packageName - The addon package it belongs to; `null` for the app.
|
|
63
63
|
*/
|
|
64
64
|
export const addFunction = (funcName, funcConfig, packageName = null) => {
|
|
65
|
+
if (funcConfig.disabled) {
|
|
66
|
+
throw new Error(`Function '${funcName}' is disabled` +
|
|
67
|
+
(typeof funcConfig.disabled === 'string'
|
|
68
|
+
? ` (${funcConfig.disabled})`
|
|
69
|
+
: '') +
|
|
70
|
+
' and cannot be registered.');
|
|
71
|
+
}
|
|
65
72
|
pikkuState(packageName, 'function', 'functions').set(funcName, funcConfig);
|
|
66
73
|
};
|
|
67
74
|
export const getFunctionNames = (packageName = null) => {
|
|
@@ -128,6 +128,14 @@ export type CorePikkuFunctionConfig<PikkuFunction extends CorePikkuFunction<any,
|
|
|
128
128
|
version?: number;
|
|
129
129
|
/** Filters this function in and out of a build — see the `tags` option on `pikku all`. It has no effect at runtime. */
|
|
130
130
|
tags?: string[];
|
|
131
|
+
/**
|
|
132
|
+
* Switches the function off: `true`, or a string saying why. The build
|
|
133
|
+
* leaves it out of every map, client and wiring, and refuses (PKU562)
|
|
134
|
+
* anything that still references it, so marking a function believed dead
|
|
135
|
+
* proves it. The source keeps type-checking; deleting the flag turns it
|
|
136
|
+
* back on.
|
|
137
|
+
*/
|
|
138
|
+
disabled?: true | string;
|
|
131
139
|
/** Makes the function callable from outside as `POST /rpc/<name>`. Without a session requirement, a permission or an addon gate, that means callable by anyone. */
|
|
132
140
|
expose?: boolean;
|
|
133
141
|
/**
|
|
@@ -2,7 +2,7 @@ import type { JWTService } from './services/jwt-service.js';
|
|
|
2
2
|
import type { SecretService } from './services/secret-service.js';
|
|
3
3
|
/**
|
|
4
4
|
* Authorization headers carrying a JWT-signed session and traceId for
|
|
5
|
-
* `
|
|
5
|
+
* `pikkuMeshAuthMiddleware` on the receiving end. Used by every deployment
|
|
6
6
|
* service regardless of transport (HTTP, Lambda Invoke, service bindings).
|
|
7
7
|
*/
|
|
8
|
-
export declare function
|
|
8
|
+
export declare function buildMeshHeaders(jwt: JWTService | undefined, secrets: SecretService | undefined, funcName: string, session?: unknown, traceId?: string): Promise<Record<string, string>>;
|
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
import { assertStrongKeyMaterial, encryptWithKeyMaterial,
|
|
1
|
+
import { assertStrongKeyMaterial, encryptWithKeyMaterial, MESH_SESSION_INFO, } from './crypto-utils.js';
|
|
2
2
|
/**
|
|
3
|
-
* An unset secret means
|
|
3
|
+
* An unset secret means mesh auth is not configured, so the call goes out
|
|
4
4
|
* unauthenticated. A secrets backend that fails is a different thing: sending
|
|
5
5
|
* the call anyway would only earn a 401 from the peer with no hint why.
|
|
6
6
|
*/
|
|
7
|
-
const
|
|
7
|
+
const readMeshSecret = async (secrets) => {
|
|
8
8
|
if (!secrets) {
|
|
9
9
|
return undefined;
|
|
10
10
|
}
|
|
11
11
|
try {
|
|
12
|
-
if (!(await secrets.hasSecret('
|
|
12
|
+
if (!(await secrets.hasSecret('PIKKU_MESH_SECRET'))) {
|
|
13
13
|
return undefined;
|
|
14
14
|
}
|
|
15
|
-
return (await secrets.getSecret('
|
|
15
|
+
return (await secrets.getSecret('PIKKU_MESH_SECRET')).reveal();
|
|
16
16
|
}
|
|
17
17
|
catch (cause) {
|
|
18
|
-
throw new Error('Could not read
|
|
18
|
+
throw new Error('Could not read PIKKU_MESH_SECRET from the secrets service; refusing to send an unauthenticated mesh call', { cause });
|
|
19
19
|
}
|
|
20
20
|
};
|
|
21
21
|
/**
|
|
22
22
|
* Authorization headers carrying a JWT-signed session and traceId for
|
|
23
|
-
* `
|
|
23
|
+
* `pikkuMeshAuthMiddleware` on the receiving end. Used by every deployment
|
|
24
24
|
* service regardless of transport (HTTP, Lambda Invoke, service bindings).
|
|
25
25
|
*/
|
|
26
|
-
export async function
|
|
26
|
+
export async function buildMeshHeaders(jwt, secrets, funcName, session, traceId) {
|
|
27
27
|
const headers = {
|
|
28
28
|
'content-type': 'application/json',
|
|
29
29
|
...(traceId && { 'x-request-id': traceId }),
|
|
30
30
|
};
|
|
31
|
-
const secret = await
|
|
31
|
+
const secret = await readMeshSecret(secrets);
|
|
32
32
|
if (secret && jwt) {
|
|
33
|
-
assertStrongKeyMaterial('
|
|
33
|
+
assertStrongKeyMaterial('PIKKU_MESH_SECRET', secret);
|
|
34
34
|
const sessionEnc = session
|
|
35
|
-
? await encryptWithKeyMaterial('
|
|
35
|
+
? await encryptWithKeyMaterial('PIKKU_MESH_SECRET', secret, MESH_SESSION_INFO, {
|
|
36
36
|
session,
|
|
37
37
|
})
|
|
38
38
|
: undefined;
|
|
39
39
|
const token = await jwt.encode({ value: 5, unit: 'minute' }, {
|
|
40
|
-
aud: 'pikku-
|
|
40
|
+
aud: 'pikku-mesh',
|
|
41
41
|
fn: funcName,
|
|
42
42
|
iat: Math.floor(Date.now() / 1000),
|
|
43
43
|
session: sessionEnc,
|
|
@@ -1,9 +1,9 @@
|
|
|
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 { addonScopeCap, ADDON_HEADER
|
|
4
|
+
export { addonScopeCap, ADDON_HEADER } from './addon-scope-cap.js';
|
|
5
5
|
export { remoteJobsSecret } from './remote-jobs-secret.js';
|
|
6
|
-
export {
|
|
6
|
+
export { pikkuMeshAuthMiddleware } from './mesh-auth.js';
|
|
7
7
|
export { cors } from './cors.js';
|
|
8
8
|
export { requireOrigin, isAllowedOrigin, toOrigin } from './require-origin.js';
|
|
9
9
|
export { telemetryOuter, telemetryInner } from './telemetry.js';
|
package/dist/middleware/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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 { addonScopeCap, ADDON_HEADER
|
|
4
|
+
export { addonScopeCap, ADDON_HEADER } from './addon-scope-cap.js';
|
|
5
5
|
export { remoteJobsSecret } from './remote-jobs-secret.js';
|
|
6
|
-
export {
|
|
6
|
+
export { pikkuMeshAuthMiddleware } from './mesh-auth.js';
|
|
7
7
|
export { cors } from './cors.js';
|
|
8
8
|
export { requireOrigin, isAllowedOrigin, toOrigin } from './require-origin.js';
|
|
9
9
|
export { telemetryOuter, telemetryInner } from './telemetry.js';
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Authenticates calls between deployments of the same app, on the generated
|
|
3
|
-
*
|
|
4
|
-
* `Bearer` JWT with audience `pikku-
|
|
5
|
-
* is only accepted on that function's `/
|
|
3
|
+
* mesh-RPC route. Once `PIKKU_MESH_SECRET` is set it requires a
|
|
4
|
+
* `Bearer` JWT with audience `pikku-mesh`; a token bound to a function (`fn`)
|
|
5
|
+
* is only accepted on that function's `/mesh/rpc/<fn>` path, and any session
|
|
6
6
|
* it carries is decrypted with the secret and set on the wire.
|
|
7
7
|
*
|
|
8
|
-
* Without the secret it lets non-
|
|
9
|
-
* `/
|
|
8
|
+
* Without the secret it lets non-mesh paths through and refuses
|
|
9
|
+
* `/mesh/rpc/*`, so the mesh surface fails closed.
|
|
10
10
|
*
|
|
11
11
|
* @throws {UnauthorizedError} On a missing, malformed, mis-addressed or
|
|
12
12
|
* undecryptable token.
|
|
13
13
|
* @throws {WeakKeyMaterialError} When the secret is too short to use.
|
|
14
14
|
* @throws {Error} When the secret is set but no JWT service is configured.
|
|
15
15
|
*/
|
|
16
|
-
export declare const
|
|
16
|
+
export declare const pikkuMeshAuthMiddleware: import("./middleware.types.js").CorePikkuMiddleware<import("../types/core.types.js").CoreSingletonServices<{
|
|
17
17
|
logLevel?: import("../services/logger.js").LogLevel;
|
|
18
18
|
secrets?: {
|
|
19
19
|
requireAllowedHosts?: boolean;
|
|
@@ -1,47 +1,47 @@
|
|
|
1
1
|
import { UnauthorizedError } from '../errors/errors.js';
|
|
2
2
|
import { pikkuMiddleware } from './middleware-factories.js';
|
|
3
|
-
import { assertStrongKeyMaterial, decryptWithKeyMaterial,
|
|
4
|
-
const
|
|
3
|
+
import { assertStrongKeyMaterial, decryptWithKeyMaterial, MESH_SESSION_INFO, } from '../crypto-utils.js';
|
|
4
|
+
const MESH_RPC_PREFIX = '/mesh/rpc/';
|
|
5
5
|
/**
|
|
6
|
-
* Whether this request targets the
|
|
6
|
+
* Whether this request targets the mesh-RPC surface. Compared case-insensitively
|
|
7
7
|
* because the router matches routes that way (path-to-regexp defaults to
|
|
8
|
-
* `sensitive: false`), so `/
|
|
9
|
-
* `/
|
|
8
|
+
* `sensitive: false`), so `/Mesh/RPC/fn` reaches the same handler as
|
|
9
|
+
* `/mesh/rpc/fn`. A case-sensitive check here let a case-varied path slip
|
|
10
10
|
* past the trust gate and the token's `fn` binding while still being dispatched.
|
|
11
11
|
*/
|
|
12
|
-
const
|
|
12
|
+
const isMeshRpcPath = (path) => path.toLowerCase().startsWith(MESH_RPC_PREFIX);
|
|
13
13
|
/**
|
|
14
14
|
* Authenticates calls between deployments of the same app, on the generated
|
|
15
|
-
*
|
|
16
|
-
* `Bearer` JWT with audience `pikku-
|
|
17
|
-
* is only accepted on that function's `/
|
|
15
|
+
* mesh-RPC route. Once `PIKKU_MESH_SECRET` is set it requires a
|
|
16
|
+
* `Bearer` JWT with audience `pikku-mesh`; a token bound to a function (`fn`)
|
|
17
|
+
* is only accepted on that function's `/mesh/rpc/<fn>` path, and any session
|
|
18
18
|
* it carries is decrypted with the secret and set on the wire.
|
|
19
19
|
*
|
|
20
|
-
* Without the secret it lets non-
|
|
21
|
-
* `/
|
|
20
|
+
* Without the secret it lets non-mesh paths through and refuses
|
|
21
|
+
* `/mesh/rpc/*`, so the mesh surface fails closed.
|
|
22
22
|
*
|
|
23
23
|
* @throws {UnauthorizedError} On a missing, malformed, mis-addressed or
|
|
24
24
|
* undecryptable token.
|
|
25
25
|
* @throws {WeakKeyMaterialError} When the secret is too short to use.
|
|
26
26
|
* @throws {Error} When the secret is set but no JWT service is configured.
|
|
27
27
|
*/
|
|
28
|
-
export const
|
|
28
|
+
export const pikkuMeshAuthMiddleware = pikkuMiddleware(async ({ secrets, jwt }, { http, setInitialSession }, next) => {
|
|
29
29
|
if (!http?.request || !secrets) {
|
|
30
30
|
return next();
|
|
31
31
|
}
|
|
32
32
|
let secret;
|
|
33
33
|
try {
|
|
34
|
-
secret = (await secrets.getSecret('
|
|
34
|
+
secret = (await secrets.getSecret('PIKKU_MESH_SECRET')).reveal();
|
|
35
35
|
}
|
|
36
36
|
catch {
|
|
37
|
-
if (
|
|
37
|
+
if (isMeshRpcPath(http.request.path())) {
|
|
38
38
|
throw new UnauthorizedError();
|
|
39
39
|
}
|
|
40
40
|
return next();
|
|
41
41
|
}
|
|
42
|
-
assertStrongKeyMaterial('
|
|
42
|
+
assertStrongKeyMaterial('PIKKU_MESH_SECRET', secret);
|
|
43
43
|
if (!jwt) {
|
|
44
|
-
throw new Error('
|
|
44
|
+
throw new Error('PIKKU_MESH_SECRET set but JWT service missing');
|
|
45
45
|
}
|
|
46
46
|
const authHeader = http.request.header('authorization') ||
|
|
47
47
|
http.request.header('Authorization');
|
|
@@ -59,20 +59,20 @@ export const pikkuRemoteAuthMiddleware = pikkuMiddleware(async ({ secrets, jwt }
|
|
|
59
59
|
catch {
|
|
60
60
|
throw new UnauthorizedError();
|
|
61
61
|
}
|
|
62
|
-
if (payload?.aud !== 'pikku-
|
|
62
|
+
if (payload?.aud !== 'pikku-mesh') {
|
|
63
63
|
throw new UnauthorizedError();
|
|
64
64
|
}
|
|
65
|
-
if (payload?.fn &&
|
|
65
|
+
if (payload?.fn && isMeshRpcPath(http.request.path())) {
|
|
66
66
|
// The prefix length is the same in any case, so slicing the raw path
|
|
67
67
|
// keeps the requested function name in its original case for the compare.
|
|
68
|
-
const fn = decodeURIComponent(http.request.path().slice(
|
|
68
|
+
const fn = decodeURIComponent(http.request.path().slice(MESH_RPC_PREFIX.length));
|
|
69
69
|
if (fn && payload.fn !== fn) {
|
|
70
70
|
throw new UnauthorizedError();
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
if (payload?.session) {
|
|
74
74
|
try {
|
|
75
|
-
const decrypted = await decryptWithKeyMaterial('
|
|
75
|
+
const decrypted = await decryptWithKeyMaterial('PIKKU_MESH_SECRET', secret, MESH_SESSION_INFO, payload.session);
|
|
76
76
|
if (decrypted?.session && setInitialSession) {
|
|
77
77
|
setInitialSession(decrypted.session);
|
|
78
78
|
}
|
|
@@ -255,16 +255,16 @@ export type PikkuWire<In = unknown, Out = unknown, HasInitialSession extends boo
|
|
|
255
255
|
* sets it; services that log fall back to the singleton logger.
|
|
256
256
|
*/
|
|
257
257
|
logger: Logger;
|
|
258
|
-
/** Trace ID for distributed tracing — propagated across
|
|
258
|
+
/** Trace ID for distributed tracing — propagated across mesh RPC calls via the x-request-id header */
|
|
259
259
|
traceId: string;
|
|
260
260
|
functionId: string;
|
|
261
261
|
addonNamespace: string;
|
|
262
262
|
http: PikkuHTTP<In>;
|
|
263
263
|
mcp: PikkuMCP<MCPTools>;
|
|
264
|
-
channel: [IsChannel] extends [null] ? PikkuChannel<unknown, Out, TypedRPC['
|
|
264
|
+
channel: [IsChannel] extends [null] ? PikkuChannel<unknown, Out, TypedRPC['mesh']> : PikkuChannel<unknown, Out, TypedRPC['mesh']> | undefined;
|
|
265
265
|
scheduledTask: PikkuScheduledTask;
|
|
266
266
|
queue: PikkuQueue;
|
|
267
|
-
cli: PikkuCLI<TypedRPC['
|
|
267
|
+
cli: PikkuCLI<TypedRPC['mesh']>;
|
|
268
268
|
workflow: TypedWorkflow;
|
|
269
269
|
scenario: TypedScenario;
|
|
270
270
|
virtualUsers: TScenarioVirtualUsers;
|
|
@@ -15,7 +15,7 @@ import type { CoreScheduledTask, ScheduledTasksMeta } from '../wirings/scheduler
|
|
|
15
15
|
import type { CoreWorkflow, WorkflowsRuntimeMeta } from '../wirings/workflow/workflow.types.js';
|
|
16
16
|
import type { CoreFeature } from '../wirings/workflow/scenario.types.js';
|
|
17
17
|
import type { CoreTrigger, CoreTriggerSource, TriggerMeta, TriggerSourceMeta } from '../wirings/trigger/trigger.types.js';
|
|
18
|
-
import type { CreateConfig, CreateSingletonServices, CreateWireServices,
|
|
18
|
+
import type { CreateConfig, CreateSingletonServices, CreateWireServices, CoreSingletonServices } from './core.types.js';
|
|
19
19
|
import type { FunctionsMeta } from '../function/function-meta.types.js';
|
|
20
20
|
import type { CorePikkuMiddleware, CorePikkuMiddlewareGroup } from '../middleware/middleware.types.js';
|
|
21
21
|
import type { CorePikkuChannelMiddleware } from '../wirings/channel/channel.types.js';
|
|
@@ -42,7 +42,7 @@ export interface PikkuPackageState {
|
|
|
42
42
|
rpcEndpoint?: string;
|
|
43
43
|
auth?: boolean;
|
|
44
44
|
tags?: string[];
|
|
45
|
-
/** Which functions `rpc.exposed` may call
|
|
45
|
+
/** Which functions `rpc.exposed` may call, from those the addon declared `expose: true`: `true` all of them, a list those named, unset/`false` none */
|
|
46
46
|
expose?: boolean | string[];
|
|
47
47
|
/** Required of every function in this package, on top of the function's own */
|
|
48
48
|
scopes?: string[];
|
|
@@ -78,10 +78,14 @@ export interface PikkuPackageState {
|
|
|
78
78
|
} | {
|
|
79
79
|
secretId: string;
|
|
80
80
|
} | {
|
|
81
|
-
resolve: (services: any, wire: any) => string | Promise<string>;
|
|
81
|
+
resolve: (services: any, wire: any) => string | null | Promise<string | null>;
|
|
82
82
|
};
|
|
83
|
-
/** Map consumer-facing fn name → remote fn name (wireRemoteAddon) */
|
|
83
|
+
/** Map consumer-facing base fn name → remote fn name (wireRemoteAddon); `@vN` is appended after */
|
|
84
84
|
remoteName?: (fn: string) => string;
|
|
85
|
+
/** Milliseconds a remote addon call may take before it is aborted (wireRemoteAddon) */
|
|
86
|
+
remoteTimeoutMs?: number;
|
|
87
|
+
/** Extra headers sent on every call to the hosted addon (wireRemoteAddon) */
|
|
88
|
+
remoteHeaders?: (services: any, wire: any) => Record<string, string> | Promise<Record<string, string>>;
|
|
85
89
|
}>;
|
|
86
90
|
};
|
|
87
91
|
http: {
|
|
@@ -164,10 +168,11 @@ export interface PikkuPackageState {
|
|
|
164
168
|
permissions: Record<string, CorePermissionGroup | CorePikkuPermission[]>;
|
|
165
169
|
};
|
|
166
170
|
package: {
|
|
171
|
+
/** Every package's own factories, each typed where it is defined. */
|
|
167
172
|
factories: {
|
|
168
|
-
createConfig?: CreateConfig<
|
|
169
|
-
createSingletonServices?: CreateSingletonServices<
|
|
170
|
-
createWireServices?: CreateWireServices<
|
|
173
|
+
createConfig?: CreateConfig<any>;
|
|
174
|
+
createSingletonServices?: CreateSingletonServices<any, any>;
|
|
175
|
+
createWireServices?: CreateWireServices<any, any, any>;
|
|
171
176
|
} | null;
|
|
172
177
|
singletonServices: CoreSingletonServices | null;
|
|
173
178
|
/** The pikkuBetterAuth factory, self-registered when auth.ts is evaluated.
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import { PikkuError } from '../../errors/error-handler.js';
|
|
1
2
|
import type { PikkuRawWire } from '../../types/core.types.js';
|
|
2
3
|
export declare const addonMayReach: (caller: string, funcName: string) => boolean;
|
|
3
4
|
export declare const addonCallerOf: (wire: PikkuRawWire) => string | undefined;
|
|
5
|
+
export declare class AddonNotUsedError extends PikkuError {
|
|
6
|
+
constructor(caller: string, target: string);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The name an addon's call resolves to on this host, through its `uses`.
|
|
10
|
+
*
|
|
11
|
+
* knowledge: decisions/security/an-addon-may-reach-only-itself-and-the-addons-it-uses.md
|
|
12
|
+
*/
|
|
13
|
+
export declare const mapAddonCall: (caller: string | undefined, funcName: string) => string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ForbiddenError } from '../../errors/errors.js';
|
|
2
|
+
import { PikkuError, addError } from '../../errors/error-handler.js';
|
|
2
3
|
import { ADDON_HEADER } from '../../middleware/addon-scope-cap.js';
|
|
3
4
|
import { pikkuState } from '../../pikku-state.js';
|
|
4
5
|
export const addonMayReach = (caller, funcName) => {
|
|
@@ -19,3 +20,31 @@ export const addonCallerOf = (wire) => {
|
|
|
19
20
|
}
|
|
20
21
|
return name;
|
|
21
22
|
};
|
|
23
|
+
export class AddonNotUsedError extends PikkuError {
|
|
24
|
+
constructor(caller, target) {
|
|
25
|
+
super(`Addon '${caller}' called '${target}' but does not list it in its wireAddon 'uses'.`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
addError(AddonNotUsedError, {
|
|
29
|
+
status: 403,
|
|
30
|
+
message: 'Addon called another addon it does not use.',
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* The name an addon's call resolves to on this host, through its `uses`.
|
|
34
|
+
*
|
|
35
|
+
* knowledge: decisions/security/an-addon-may-reach-only-itself-and-the-addons-it-uses.md
|
|
36
|
+
*/
|
|
37
|
+
export const mapAddonCall = (caller, funcName) => {
|
|
38
|
+
if (!caller)
|
|
39
|
+
return funcName;
|
|
40
|
+
const colon = funcName.indexOf(':');
|
|
41
|
+
if (colon === -1)
|
|
42
|
+
return funcName;
|
|
43
|
+
const prefix = funcName.slice(0, colon);
|
|
44
|
+
if (prefix === caller)
|
|
45
|
+
return funcName;
|
|
46
|
+
const mapped = pikkuState(null, 'addons', 'packages').get(caller)?.uses?.[prefix];
|
|
47
|
+
if (mapped)
|
|
48
|
+
return `${mapped}${funcName.slice(colon)}`;
|
|
49
|
+
throw new AddonNotUsedError(caller, funcName);
|
|
50
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { wireAddon } from './wire-addon.js';
|
|
2
2
|
export type { WireAddonConfig } from './wire-addon.js';
|
|
3
|
-
export {
|
|
4
|
-
export type {
|
|
3
|
+
export { pikkuStudio } from './pikku-studio.js';
|
|
4
|
+
export type { StudioManifest, StudioScreen, StudioComponentScreen, StudioAppScreen, StudioNav, } from './pikku-studio.js';
|
|
5
5
|
export { wireRemoteAddon } from './wire-remote-addon.js';
|
|
6
6
|
export type { WireRemoteAddonConfig, RemoteAddonAuth, } from './wire-remote-addon.js';
|
|
7
7
|
export { resolveRemoteAddonToken, RemoteAddonAuthError, } from './remote-addon-auth.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { wireAddon } from './wire-addon.js';
|
|
2
|
-
export {
|
|
2
|
+
export { pikkuStudio } from './pikku-studio.js';
|
|
3
3
|
export { wireRemoteAddon } from './wire-remote-addon.js';
|
|
4
4
|
export { resolveRemoteAddonToken, RemoteAddonAuthError, } from './remote-addon-auth.js';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where an addon screen shows in Studio's navigation. Leave it out and the
|
|
3
|
+
* screen is routed but not listed.
|
|
4
|
+
*/
|
|
5
|
+
export type StudioNav = {
|
|
6
|
+
area: 'rail' | 'team' | 'project' | 'account';
|
|
7
|
+
/** The icon's name. */
|
|
8
|
+
icon: string;
|
|
9
|
+
order: number;
|
|
10
|
+
/** Names a function of the addon that returns a count shown on the entry. */
|
|
11
|
+
badge?: string;
|
|
12
|
+
};
|
|
13
|
+
type StudioScreenBase = {
|
|
14
|
+
/** The route path, under the prefix its `scope` gives it. */
|
|
15
|
+
path: string;
|
|
16
|
+
/** A message key in the addon's catalog, `messages/{locale}.json`. It is also the nav label. */
|
|
17
|
+
title: string;
|
|
18
|
+
nav?: StudioNav;
|
|
19
|
+
/** Takes over the core screen at the same path. Only first-party addons may use it. */
|
|
20
|
+
replaces?: boolean;
|
|
21
|
+
/** `'bare'` renders the screen without the shell. */
|
|
22
|
+
layout?: 'bare';
|
|
23
|
+
/** Hides the screen and its nav for viewers without access. The addon's functions must enforce the same scopes: hiding a screen is not access control. */
|
|
24
|
+
scopes?: string[];
|
|
25
|
+
} & ({
|
|
26
|
+
/** `'studio'` gives `/<path>`, `'team'` gives `/<team>/<path>`. */
|
|
27
|
+
scope: 'studio' | 'team';
|
|
28
|
+
needsApp?: never;
|
|
29
|
+
} | {
|
|
30
|
+
/** `/<team>/<project>[/<target>]/<path>`. */
|
|
31
|
+
scope: 'project';
|
|
32
|
+
/** The screen needs the project's app running. Defaults to `false`. */
|
|
33
|
+
needsApp?: boolean;
|
|
34
|
+
});
|
|
35
|
+
/** A screen rendered by the host from the addon's source. */
|
|
36
|
+
export type StudioComponentScreen = StudioScreenBase & {
|
|
37
|
+
component: () => Promise<{
|
|
38
|
+
default: unknown;
|
|
39
|
+
}>;
|
|
40
|
+
};
|
|
41
|
+
/** A static program the host serves in a sandboxed iframe. */
|
|
42
|
+
export type StudioAppScreen = StudioScreenBase & {
|
|
43
|
+
/** Directory of the built program, relative to the addon package. */
|
|
44
|
+
app: string;
|
|
45
|
+
};
|
|
46
|
+
/** One screen an addon ships: either a component the host renders or a static app it serves in a sandboxed iframe. */
|
|
47
|
+
export type StudioScreen = StudioComponentScreen | StudioAppScreen;
|
|
48
|
+
/** The manifest passed to `pikkuStudio`: the addon's title, a message key, and its screens. */
|
|
49
|
+
export type StudioManifest = {
|
|
50
|
+
title: string;
|
|
51
|
+
screens: StudioScreen[];
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Declares the screens and nav an addon brings to Pikku Studio. Read
|
|
55
|
+
* statically by `pikku all`, so every field must be a literal. Titles are
|
|
56
|
+
* message keys in the addon's own catalog, `messages/{locale}.json`: a key
|
|
57
|
+
* missing from `messages/en.json` fails the build.
|
|
58
|
+
*/
|
|
59
|
+
export declare const pikkuStudio: (manifest: StudioManifest) => StudioManifest;
|
|
60
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declares the screens and nav an addon brings to Pikku Studio. Read
|
|
3
|
+
* statically by `pikku all`, so every field must be a literal. Titles are
|
|
4
|
+
* message keys in the addon's own catalog, `messages/{locale}.json`: a key
|
|
5
|
+
* missing from `messages/en.json` fails the build.
|
|
6
|
+
*/
|
|
7
|
+
export const pikkuStudio = (manifest) => manifest;
|
|
@@ -10,7 +10,12 @@ export type RemoteAddonAuthBinding = {
|
|
|
10
10
|
} | {
|
|
11
11
|
secretId: string;
|
|
12
12
|
} | {
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* `null` says this call carries no credential, for a host whose surface
|
|
15
|
+
* is public to anonymous callers. An empty or undefined token still
|
|
16
|
+
* throws, so a resolver that lost its token does not go out anonymous.
|
|
17
|
+
*/
|
|
18
|
+
resolve: (services: CoreServices, wire: PikkuRawWire) => string | SecretValue<string> | null | Promise<string | SecretValue<string> | null>;
|
|
14
19
|
};
|
|
15
20
|
/**
|
|
16
21
|
* Thrown (401) when a remote addon's token cannot be resolved: the binding needs credential
|
|
@@ -21,5 +26,8 @@ export declare class RemoteAddonAuthError extends PikkuError {
|
|
|
21
26
|
readonly namespace: string;
|
|
22
27
|
constructor(namespace: string, detail: string);
|
|
23
28
|
}
|
|
24
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Returns `null` when no auth is bound or `resolve` answers `null`; any other
|
|
31
|
+
* empty token throws.
|
|
32
|
+
*/
|
|
25
33
|
export declare function resolveRemoteAddonToken(auth: RemoteAddonAuthBinding | undefined, services: CoreServices, wire: PikkuRawWire, namespace: string): Promise<string | null>;
|
|
@@ -16,7 +16,10 @@ addError(RemoteAddonAuthError, {
|
|
|
16
16
|
status: 401,
|
|
17
17
|
message: 'Remote addon authentication could not be resolved.',
|
|
18
18
|
});
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Returns `null` when no auth is bound or `resolve` answers `null`; any other
|
|
21
|
+
* empty token throws.
|
|
22
|
+
*/
|
|
20
23
|
export async function resolveRemoteAddonToken(auth, services, wire, namespace) {
|
|
21
24
|
if (!auth) {
|
|
22
25
|
return null;
|
|
@@ -33,6 +36,8 @@ export async function resolveRemoteAddonToken(auth, services, wire, namespace) {
|
|
|
33
36
|
}
|
|
34
37
|
else if (typeof auth.resolve === 'function') {
|
|
35
38
|
token = await auth.resolve(services, wire);
|
|
39
|
+
if (token === null)
|
|
40
|
+
return null;
|
|
36
41
|
}
|
|
37
42
|
// The wire is where a secret is meant to end up, so unwrap it here rather
|
|
38
43
|
// than at each branch — `resolve` and `getCredential` may hand one back too.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** A `wireRemoteAddon` `package` value split into the package name and the optional semver range after it. */
|
|
2
|
+
export type RemoteAddonPackageSpec = {
|
|
3
|
+
/** The package name, e.g. `@pikkustudio/addon-cloud-api`. */
|
|
4
|
+
name: string;
|
|
5
|
+
/** The semver range, e.g. `^1.4.0`. Absent when the spec names only the package. */
|
|
6
|
+
range?: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Splits `@scope/name@^1.4.0` into its name and range at the last `@` after
|
|
10
|
+
* the first character, so a scope's leading `@` is never taken for the
|
|
11
|
+
* separator. Throws when either half is empty.
|
|
12
|
+
*/
|
|
13
|
+
export declare const parseRemoteAddonPackage: (spec: string) => RemoteAddonPackageSpec;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Splits `@scope/name@^1.4.0` into its name and range at the last `@` after
|
|
3
|
+
* the first character, so a scope's leading `@` is never taken for the
|
|
4
|
+
* separator. Throws when either half is empty.
|
|
5
|
+
*/
|
|
6
|
+
export const parseRemoteAddonPackage = (spec) => {
|
|
7
|
+
const trimmed = spec.trim();
|
|
8
|
+
const at = trimmed.lastIndexOf('@');
|
|
9
|
+
if (at <= 0) {
|
|
10
|
+
if (!trimmed || trimmed === '@') {
|
|
11
|
+
throw new Error(`'${spec}' is not a package name`);
|
|
12
|
+
}
|
|
13
|
+
return { name: trimmed };
|
|
14
|
+
}
|
|
15
|
+
const name = trimmed.slice(0, at);
|
|
16
|
+
const range = trimmed.slice(at + 1).trim();
|
|
17
|
+
if (!range) {
|
|
18
|
+
throw new Error(`'${spec}' ends in '@' with no version range after it. Write '${name}@^1.0.0', or drop the '@'.`);
|
|
19
|
+
}
|
|
20
|
+
return { name, range };
|
|
21
|
+
};
|