@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
@@ -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 remote-RPC session key so the same deployment secret used for
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 REMOTE_SESSION_INFO = "pikku:remote-session";
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 REMOTE_SESSION_INFO}) yields two unrelated keys.
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.
@@ -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 remote-RPC session key so the same deployment secret used for
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 REMOTE_SESSION_INFO = 'pikku:remote-session';
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 REMOTE_SESSION_INFO}) yields two unrelated keys.
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.remote(...)`. The deploy planner binds the
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
- * `pikkuRemoteAuthMiddleware` on the receiving end. Used by every deployment
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 buildRemoteHeaders(jwt: JWTService | undefined, secrets: SecretService | undefined, funcName: string, session?: unknown, traceId?: string): Promise<Record<string, string>>;
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, REMOTE_SESSION_INFO, } from './crypto-utils.js';
1
+ import { assertStrongKeyMaterial, encryptWithKeyMaterial, MESH_SESSION_INFO, } from './crypto-utils.js';
2
2
  /**
3
- * An unset secret means remote auth is not configured, so the call goes out
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 readRemoteSecret = async (secrets) => {
7
+ const readMeshSecret = async (secrets) => {
8
8
  if (!secrets) {
9
9
  return undefined;
10
10
  }
11
11
  try {
12
- if (!(await secrets.hasSecret('PIKKU_REMOTE_SECRET'))) {
12
+ if (!(await secrets.hasSecret('PIKKU_MESH_SECRET'))) {
13
13
  return undefined;
14
14
  }
15
- return (await secrets.getSecret('PIKKU_REMOTE_SECRET')).reveal();
15
+ return (await secrets.getSecret('PIKKU_MESH_SECRET')).reveal();
16
16
  }
17
17
  catch (cause) {
18
- throw new Error('Could not read PIKKU_REMOTE_SECRET from the secrets service; refusing to send an unauthenticated remote call', { cause });
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
- * `pikkuRemoteAuthMiddleware` on the receiving end. Used by every deployment
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 buildRemoteHeaders(jwt, secrets, funcName, session, traceId) {
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 readRemoteSecret(secrets);
31
+ const secret = await readMeshSecret(secrets);
32
32
  if (secret && jwt) {
33
- assertStrongKeyMaterial('PIKKU_REMOTE_SECRET', secret);
33
+ assertStrongKeyMaterial('PIKKU_MESH_SECRET', secret);
34
34
  const sessionEnc = session
35
- ? await encryptWithKeyMaterial('PIKKU_REMOTE_SECRET', secret, REMOTE_SESSION_INFO, {
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-remote',
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, } from './addon-scope-cap.js';
4
+ export { addonScopeCap, ADDON_HEADER } from './addon-scope-cap.js';
5
5
  export { remoteJobsSecret } from './remote-jobs-secret.js';
6
- export { pikkuRemoteAuthMiddleware } from './remote-auth.js';
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,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, } from './addon-scope-cap.js';
4
+ export { addonScopeCap, ADDON_HEADER } from './addon-scope-cap.js';
5
5
  export { remoteJobsSecret } from './remote-jobs-secret.js';
6
- export { pikkuRemoteAuthMiddleware } from './remote-auth.js';
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
- * remote-RPC route. Once `PIKKU_REMOTE_SECRET` is set it requires a
4
- * `Bearer` JWT with audience `pikku-remote`; a token bound to a function (`fn`)
5
- * is only accepted on that function's `/remote/rpc/<fn>` path, and any session
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-remote paths through and refuses
9
- * `/remote/rpc/*`, so the remote surface fails closed.
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 pikkuRemoteAuthMiddleware: import("./middleware.types.js").CorePikkuMiddleware<import("../types/core.types.js").CoreSingletonServices<{
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, REMOTE_SESSION_INFO, } from '../crypto-utils.js';
4
- const REMOTE_RPC_PREFIX = '/remote/rpc/';
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 remote-RPC surface. Compared case-insensitively
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 `/Remote/RPC/fn` reaches the same handler as
9
- * `/remote/rpc/fn`. A case-sensitive check here let a case-varied path slip
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 isRemoteRpcPath = (path) => path.toLowerCase().startsWith(REMOTE_RPC_PREFIX);
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
- * remote-RPC route. Once `PIKKU_REMOTE_SECRET` is set it requires a
16
- * `Bearer` JWT with audience `pikku-remote`; a token bound to a function (`fn`)
17
- * is only accepted on that function's `/remote/rpc/<fn>` path, and any session
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-remote paths through and refuses
21
- * `/remote/rpc/*`, so the remote surface fails closed.
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 pikkuRemoteAuthMiddleware = pikkuMiddleware(async ({ secrets, jwt }, { http, setInitialSession }, next) => {
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('PIKKU_REMOTE_SECRET')).reveal();
34
+ secret = (await secrets.getSecret('PIKKU_MESH_SECRET')).reveal();
35
35
  }
36
36
  catch {
37
- if (isRemoteRpcPath(http.request.path())) {
37
+ if (isMeshRpcPath(http.request.path())) {
38
38
  throw new UnauthorizedError();
39
39
  }
40
40
  return next();
41
41
  }
42
- assertStrongKeyMaterial('PIKKU_REMOTE_SECRET', secret);
42
+ assertStrongKeyMaterial('PIKKU_MESH_SECRET', secret);
43
43
  if (!jwt) {
44
- throw new Error('PIKKU_REMOTE_SECRET set but JWT service missing');
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-remote') {
62
+ if (payload?.aud !== 'pikku-mesh') {
63
63
  throw new UnauthorizedError();
64
64
  }
65
- if (payload?.fn && isRemoteRpcPath(http.request.path())) {
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(REMOTE_RPC_PREFIX.length));
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('PIKKU_REMOTE_SECRET', secret, REMOTE_SESSION_INFO, payload.session);
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 remote RPC calls via the x-request-id header */
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['remote']> : PikkuChannel<unknown, Out, TypedRPC['remote']> | undefined;
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['remote']>;
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, CoreConfig, CoreSingletonServices, CoreServices, CoreUserSession } from './core.types.js';
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: unset/`true` keeps the addon's own `expose`, `false` none, a list exactly those */
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<CoreConfig>;
169
- createSingletonServices?: CreateSingletonServices<CoreConfig, CoreSingletonServices>;
170
- createWireServices?: CreateWireServices<CoreSingletonServices, CoreServices, CoreUserSession>;
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 { defineScreens } from './define-screens.js';
4
- export type { ScreensManifest, AddonScreen, AddonComponentScreen, AddonAppScreen, } from './define-screens.js';
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 { defineScreens } from './define-screens.js';
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
- resolve: (services: CoreServices, wire: PikkuRawWire) => string | SecretValue<string> | Promise<string | SecretValue<string>>;
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
- /** Returns `null` only when no auth is bound; an empty resolved token throws. */
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
- /** Returns `null` only when no auth is bound; an empty resolved token throws. */
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
+ };