@pikku/core 0.12.138 → 0.12.141
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/CHANGELOG.md +23 -0
- package/dist/lifecycle.d.ts +6 -0
- package/dist/lifecycle.js +58 -0
- package/dist/middleware/addon-scope-cap.d.ts +19 -0
- package/dist/middleware/addon-scope-cap.js +34 -0
- package/dist/middleware/index.d.ts +1 -0
- package/dist/middleware/index.js +1 -0
- package/dist/scopes.d.ts +8 -0
- package/dist/scopes.js +23 -0
- package/dist/services/content-service.d.ts +13 -1
- package/dist/services/index.d.ts +2 -1
- package/dist/services/index.js +1 -0
- package/dist/services/local-content-request-handler.js +21 -60
- package/dist/services/local-content.d.ts +45 -1
- package/dist/services/local-content.js +202 -18
- package/dist/services/scoped-content-service.d.ts +35 -0
- package/dist/services/scoped-content-service.js +171 -0
- package/dist/services/scoped-secret-service.d.ts +1 -0
- package/dist/services/scoped-secret-service.js +3 -0
- package/dist/services/secret-service.d.ts +6 -0
- package/dist/services/typed-secret-service.d.ts +1 -0
- package/dist/services/typed-secret-service.js +7 -0
- package/dist/testing/service-tests/secret-service-tests.js +9 -0
- package/dist/types/state.types.d.ts +8 -0
- package/dist/utils.d.ts +2 -5
- package/dist/utils.js +1 -38
- package/dist/wirings/addon/addon-reach.d.ts +3 -0
- package/dist/wirings/addon/addon-reach.js +21 -0
- package/dist/wirings/addon/addon-runner.d.ts +6 -0
- package/dist/wirings/addon/addon-runner.js +13 -0
- package/dist/wirings/addon/define-screens.d.ts +32 -0
- package/dist/wirings/addon/define-screens.js +5 -0
- package/dist/wirings/addon/index.d.ts +2 -0
- package/dist/wirings/addon/index.js +1 -0
- package/dist/wirings/addon/wire-addon.d.ts +27 -0
- package/dist/wirings/addon/wire-addon.js +4 -0
- package/dist/wirings/agent/agent-prepare.js +3 -0
- package/dist/wirings/cli/cli-runner.js +24 -9
- package/dist/wirings/http/http-runner.js +4 -0
- package/dist/wirings/http/http.types.d.ts +1 -0
- package/dist/wirings/http/pikku-fetch-http-response.d.ts +1 -0
- package/dist/wirings/http/pikku-fetch-http-response.js +12 -0
- package/dist/wirings/http/web-request.js +7 -0
- package/dist/wirings/rpc/rpc-runner.d.ts +4 -0
- package/dist/wirings/rpc/rpc-runner.js +41 -2
- package/dist/wirings/rpc/rpc-types.d.ts +6 -0
- package/dist/wirings/scope/index.d.ts +1 -1
- package/dist/wirings/scope/index.js +1 -1
- package/package.json +2 -1
- package/src/public-surface.json +64 -15
package/dist/utils.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { getSingletonServices, getAllPackageStates } from './pikku-state.js';
|
|
2
1
|
import { BadRequestError } from './errors/errors.js';
|
|
3
2
|
export const closeWireServices = async (logger, wireServices) => {
|
|
4
3
|
await Promise.all(Object.values(wireServices).map(async (service) => {
|
|
@@ -77,42 +76,6 @@ export const freezeDedupe = (arr) => {
|
|
|
77
76
|
}
|
|
78
77
|
return Object.freeze(out);
|
|
79
78
|
};
|
|
80
|
-
|
|
81
|
-
const stop = service?.stop;
|
|
82
|
-
if (stop) {
|
|
83
|
-
logger.info(`Stopping singleton service: ${name}`);
|
|
84
|
-
try {
|
|
85
|
-
await stop.call(service);
|
|
86
|
-
}
|
|
87
|
-
catch (e) {
|
|
88
|
-
logger.error(`Error stopping service ${name}:`, e);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
/** Stops addon package services first, then the parent singleton services. */
|
|
93
|
-
export const stopSingletonServices = async () => {
|
|
94
|
-
const singletonServices = getSingletonServices();
|
|
95
|
-
const logger = singletonServices.logger;
|
|
96
|
-
const stateMap = getAllPackageStates();
|
|
97
|
-
if (stateMap.size > 0) {
|
|
98
|
-
for (const [packageName, packageState] of stateMap) {
|
|
99
|
-
if (packageName === '__main__')
|
|
100
|
-
continue;
|
|
101
|
-
const packageServices = packageState.package?.singletonServices;
|
|
102
|
-
if (packageServices) {
|
|
103
|
-
logger.info(`Stopping singleton services for package: ${packageName}`);
|
|
104
|
-
for (const [name, service] of Object.entries(packageServices)) {
|
|
105
|
-
await stopService(logger, `${packageName}/${name}`, service);
|
|
106
|
-
}
|
|
107
|
-
packageState.package.singletonServices = null;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
for (const [name, service] of Object.entries(singletonServices)) {
|
|
112
|
-
await stopService(logger, name, service);
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
/** Wrap server lifecycle hooks so the inspector can discover them. */
|
|
116
|
-
export const pikkuServerLifecycle = (lifecycle) => lifecycle;
|
|
79
|
+
export { stopSingletonServices, pikkuServerLifecycle } from './lifecycle.js';
|
|
117
80
|
export { getDurationInMilliseconds, getRelativeTimeOffset, getRelativeTimeOffsetFromNow, parseDurationString, } from './time-utils.js';
|
|
118
81
|
export { formatVersionedId, isVersionedId, parseVersionedId, } from './version.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ForbiddenError } from '../../errors/errors.js';
|
|
2
|
+
import { ADDON_HEADER } from '../../middleware/addon-scope-cap.js';
|
|
3
|
+
import { pikkuState } from '../../pikku-state.js';
|
|
4
|
+
export const addonMayReach = (caller, funcName) => {
|
|
5
|
+
const target = funcName.slice(0, Math.max(funcName.indexOf(':'), 0));
|
|
6
|
+
if (!target)
|
|
7
|
+
return false;
|
|
8
|
+
if (target === caller)
|
|
9
|
+
return true;
|
|
10
|
+
const uses = pikkuState(null, 'addons', 'packages').get(caller)?.uses;
|
|
11
|
+
return !!uses && Object.values(uses).includes(target);
|
|
12
|
+
};
|
|
13
|
+
export const addonCallerOf = (wire) => {
|
|
14
|
+
const name = wire.http?.request?.header(ADDON_HEADER);
|
|
15
|
+
if (!name)
|
|
16
|
+
return undefined;
|
|
17
|
+
if (!pikkuState(null, 'addons', 'packages').has(name)) {
|
|
18
|
+
throw new ForbiddenError(`Unknown addon '${name}'`);
|
|
19
|
+
}
|
|
20
|
+
return name;
|
|
21
|
+
};
|
|
@@ -13,6 +13,12 @@ export type AddonInstance = {
|
|
|
13
13
|
globalSecrets?: string;
|
|
14
14
|
/** Set by the consuming app to opt this instance out of credential scoping. */
|
|
15
15
|
globalCredentials?: string;
|
|
16
|
+
/** Set by the consuming app: the folder of the content store this instance owns. */
|
|
17
|
+
contentBucket?: string;
|
|
18
|
+
/** Set by the consuming app: paths outside its folder this instance may read or write. */
|
|
19
|
+
contentGrants?: Record<string, 'read' | 'write'>;
|
|
20
|
+
/** Set by the consuming app to opt this instance out of content scoping. */
|
|
21
|
+
globalContent?: string;
|
|
16
22
|
};
|
|
17
23
|
export declare const getOrCreatePackageSingletonServices: (packageName: string, parentServices: CoreSingletonServices, addonInstance?: AddonInstance) => Promise<CoreSingletonServices>;
|
|
18
24
|
export declare const addonInstanceForNamespace: (namespace: string | undefined, expectedPackage: string) => AddonInstance | undefined;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { pikkuState } from '../../pikku-state.js';
|
|
2
2
|
import { ScopedSecretService } from '../../services/scoped-secret-service.js';
|
|
3
3
|
import { ScopedCredentialService } from '../../services/scoped-credential-service.js';
|
|
4
|
+
import { ScopedContentService } from '../../services/scoped-content-service.js';
|
|
4
5
|
/**
|
|
5
6
|
* What the addon declared, plus what the host lent it. Scoping runs outside the
|
|
6
7
|
* aliaser, so every name here is the one the addon reads — which is why an
|
|
@@ -105,6 +106,7 @@ const scopeWebhookService = (service, namespace) => {
|
|
|
105
106
|
return scoped;
|
|
106
107
|
};
|
|
107
108
|
export const getOrCreatePackageSingletonServices = async (packageName, parentServices, addonInstance) => {
|
|
109
|
+
addonInstance ??= addonInstanceForNamespace(findAddonNamespaceForPackage(packageName) ?? undefined, packageName);
|
|
108
110
|
const cacheKey = addonInstance?.namespace ?? packageName;
|
|
109
111
|
const cachedServices = pikkuState(cacheKey, 'package', 'singletonServices');
|
|
110
112
|
if (cachedServices) {
|
|
@@ -137,6 +139,14 @@ export const getOrCreatePackageSingletonServices = async (packageName, parentSer
|
|
|
137
139
|
};
|
|
138
140
|
}
|
|
139
141
|
const namespace = addonInstance?.namespace ?? findAddonNamespaceForPackage(packageName);
|
|
142
|
+
if (!addonInstance?.globalContent && existingServices.content) {
|
|
143
|
+
existingServices = {
|
|
144
|
+
...existingServices,
|
|
145
|
+
content: new ScopedContentService(existingServices.content, addonInstance?.contentBucket ??
|
|
146
|
+
namespace ??
|
|
147
|
+
packageName.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, ''), addonInstance?.contentGrants),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
140
150
|
if (namespace && existingServices.webhookService) {
|
|
141
151
|
existingServices = {
|
|
142
152
|
...existingServices,
|
|
@@ -176,5 +186,8 @@ export const addonInstanceForNamespace = (namespace, expectedPackage) => {
|
|
|
176
186
|
credentialGrants: cfg.credentialGrants,
|
|
177
187
|
globalSecrets: cfg.globalSecrets,
|
|
178
188
|
globalCredentials: cfg.globalCredentials,
|
|
189
|
+
contentBucket: cfg.contentBucket,
|
|
190
|
+
contentGrants: cfg.contentGrants,
|
|
191
|
+
globalContent: cfg.globalContent,
|
|
179
192
|
};
|
|
180
193
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
export type AddonScreen = AddonComponentScreen | AddonAppScreen;
|
|
22
|
+
export type ScreensManifest = {
|
|
23
|
+
title: string;
|
|
24
|
+
icon?: string;
|
|
25
|
+
screens: AddonScreen[];
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Declares the screens an addon ships, which the host mounts when it is wired
|
|
29
|
+
* with `ui: true`. Read statically by `pikku all`, so `path`, `title`, `nav`, `scopes` and `app` must be literals.
|
|
30
|
+
*/
|
|
31
|
+
export declare const defineScreens: (manifest: ScreensManifest) => ScreensManifest;
|
|
32
|
+
export {};
|
|
@@ -1,5 +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
5
|
export { wireRemoteAddon } from './wire-remote-addon.js';
|
|
4
6
|
export type { WireRemoteAddonConfig, RemoteAddonAuth, } from './wire-remote-addon.js';
|
|
5
7
|
export { resolveRemoteAddonToken, RemoteAddonAuthError, } from './remote-addon-auth.js';
|
|
@@ -41,8 +41,21 @@ export type WireAddonConfig = {
|
|
|
41
41
|
mcpEndpoint?: boolean | string;
|
|
42
42
|
/** Filters this addon in and out of a build — see the `tags` option on `pikku all`. It has no effect at runtime. */
|
|
43
43
|
tags?: string[];
|
|
44
|
+
/**
|
|
45
|
+
* Mounts the screens the addon ships (see `defineScreens`) in the host under
|
|
46
|
+
* `/addons/<name>`. Without it the addon's screens are ignored. The package
|
|
47
|
+
* must declare at least one, or `pikku all` rejects the wiring.
|
|
48
|
+
*/
|
|
49
|
+
ui?: boolean;
|
|
44
50
|
/** Required of every function in the addon, on top of the function's own. */
|
|
45
51
|
scopes?: string[];
|
|
52
|
+
/**
|
|
53
|
+
* The other addons this one may call, as `{ '<package>': '<name>' }`: the
|
|
54
|
+
* package it was written against, and the name that package is wired under
|
|
55
|
+
* in this app. The addon calls `'<package>:<function>'`; anything it did not
|
|
56
|
+
* list is refused, whatever else the app has wired.
|
|
57
|
+
*/
|
|
58
|
+
uses?: Record<string, string>;
|
|
46
59
|
/** Points a secret the addon reads at a different key in this deployment, so two instances can hold different credentials. */
|
|
47
60
|
secretOverrides?: Record<string, string>;
|
|
48
61
|
/** Points a variable the addon reads at a different key in this deployment. */
|
|
@@ -65,6 +78,20 @@ export type WireAddonConfig = {
|
|
|
65
78
|
* deploy manifest, and only the consuming app can grant it.
|
|
66
79
|
*/
|
|
67
80
|
globalCredentials?: string;
|
|
81
|
+
/**
|
|
82
|
+
* The folder of the content store this instance owns. Everything it stores
|
|
83
|
+
* lands under it and it cannot see past it. Defaults to `name`.
|
|
84
|
+
*/
|
|
85
|
+
contentBucket?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Paths outside its own folder this instance may use, by path prefix:
|
|
88
|
+
* `read` allows reads and signed downloads, `write` adds uploads, writes and
|
|
89
|
+
* deletes. The longest matching prefix decides. The addon names a granted
|
|
90
|
+
* path by starting its bucket with `@`.
|
|
91
|
+
*/
|
|
92
|
+
contentGrants?: Record<string, 'read' | 'write'>;
|
|
93
|
+
/** Hands over the whole `ContentService` unscoped. The value is the reason, recorded in the deploy manifest. */
|
|
94
|
+
globalContent?: string;
|
|
68
95
|
};
|
|
69
96
|
/**
|
|
70
97
|
* Installs an addon into this project: its functions, wirings and scopes become
|
|
@@ -14,6 +14,7 @@ export const wireAddon = (config) => {
|
|
|
14
14
|
tags: config.tags,
|
|
15
15
|
...(config.expose !== undefined ? { expose: config.expose } : {}),
|
|
16
16
|
...(config.scopes ? { scopes: config.scopes } : {}),
|
|
17
|
+
...(config.uses ? { uses: config.uses } : {}),
|
|
17
18
|
...(config.secretOverrides
|
|
18
19
|
? { secretOverrides: config.secretOverrides }
|
|
19
20
|
: {}),
|
|
@@ -31,6 +32,9 @@ export const wireAddon = (config) => {
|
|
|
31
32
|
...(config.globalCredentials
|
|
32
33
|
? { globalCredentials: config.globalCredentials }
|
|
33
34
|
: {}),
|
|
35
|
+
...(config.contentBucket ? { contentBucket: config.contentBucket } : {}),
|
|
36
|
+
...(config.contentGrants ? { contentGrants: config.contentGrants } : {}),
|
|
37
|
+
...(config.globalContent ? { globalContent: config.globalContent } : {}),
|
|
34
38
|
});
|
|
35
39
|
};
|
|
36
40
|
/**
|
|
@@ -389,6 +389,9 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
389
389
|
credentialGrants: capturedAddonConfig?.credentialGrants,
|
|
390
390
|
globalSecrets: capturedAddonConfig?.globalSecrets,
|
|
391
391
|
globalCredentials: capturedAddonConfig?.globalCredentials,
|
|
392
|
+
contentBucket: capturedAddonConfig?.contentBucket,
|
|
393
|
+
contentGrants: capturedAddonConfig?.contentGrants,
|
|
394
|
+
globalContent: capturedAddonConfig?.globalContent,
|
|
392
395
|
}
|
|
393
396
|
: undefined;
|
|
394
397
|
services = await getOrCreatePackageSingletonServices(capturedPkg, singletonServices, addonInstance);
|
|
@@ -95,11 +95,26 @@ function registerCLICommands(commands, path = [], inheritedOptions = {}, program
|
|
|
95
95
|
const unwrapped = unwrapFunc(command);
|
|
96
96
|
const commandAuth = typeof command === 'object' ? command.auth : undefined;
|
|
97
97
|
const commandPermissions = typeof command === 'object' ? command.permissions : undefined;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
if (currentMeta?.packageName && !funcName.includes(':')) {
|
|
99
|
+
if (commandAuth !== undefined || commandPermissions !== undefined) {
|
|
100
|
+
const registered = pikkuState(currentMeta.packageName, 'function', 'functions');
|
|
101
|
+
const existing = registered.get(funcName);
|
|
102
|
+
if (existing) {
|
|
103
|
+
registered.set(funcName, {
|
|
104
|
+
...existing,
|
|
105
|
+
auth: commandAuth ?? existing.auth,
|
|
106
|
+
permissions: commandPermissions ?? existing.permissions,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (!currentMeta?.packageName || funcName.includes(':')) {
|
|
112
|
+
addFunction(funcName, {
|
|
113
|
+
...unwrapped,
|
|
114
|
+
auth: commandAuth ?? unwrapped.auth,
|
|
115
|
+
permissions: commandPermissions ?? unwrapped.permissions,
|
|
116
|
+
}, currentMeta?.packageName);
|
|
117
|
+
}
|
|
103
118
|
if (typeof command === 'object' && command.render) {
|
|
104
119
|
if (programs[program]) {
|
|
105
120
|
programs[program].renderers[commandId] = command.render;
|
|
@@ -110,11 +125,11 @@ function registerCLICommands(commands, path = [], inheritedOptions = {}, program
|
|
|
110
125
|
}
|
|
111
126
|
}
|
|
112
127
|
}
|
|
113
|
-
function pluckCLIData(mergedData, funcName, availableOptions) {
|
|
114
|
-
const funcMeta = pikkuState(null, 'function', 'meta')[funcName];
|
|
128
|
+
function pluckCLIData(mergedData, funcName, availableOptions, packageName) {
|
|
129
|
+
const funcMeta = pikkuState(packageName ?? null, 'function', 'meta')[funcName];
|
|
115
130
|
const schemaName = funcMeta?.inputSchemaName;
|
|
116
131
|
const schema = schemaName
|
|
117
|
-
? pikkuState(funcMeta?.packageName ?? null, 'misc', 'schemas').get(schemaName)
|
|
132
|
+
? pikkuState(packageName ?? funcMeta?.packageName ?? null, 'misc', 'schemas').get(schemaName)
|
|
118
133
|
: null;
|
|
119
134
|
if (schema && schema.properties) {
|
|
120
135
|
const result = {};
|
|
@@ -172,7 +187,7 @@ export async function runCLICommand({ program, commandPath, data, singletonServi
|
|
|
172
187
|
}
|
|
173
188
|
const commandId = commandPath.join('.');
|
|
174
189
|
const availableOptions = programData?.commandOptions?.[commandId] || {};
|
|
175
|
-
const pluckedData = () => pluckCLIData(data, funcName, availableOptions);
|
|
190
|
+
const pluckedData = () => pluckCLIData(data, funcName, availableOptions, currentCommand.packageName);
|
|
176
191
|
const renderer = programData?.renderers[commandId] || programData?.defaultRenderer;
|
|
177
192
|
let cliState;
|
|
178
193
|
const channel = {
|
|
@@ -164,6 +164,10 @@ const executeRoute = async (services, matchedRoute, http, options) => {
|
|
|
164
164
|
await originalClose();
|
|
165
165
|
};
|
|
166
166
|
}
|
|
167
|
+
response.onClose?.(() => {
|
|
168
|
+
channel.state = 'closed';
|
|
169
|
+
singletonServices.eventHub?.onChannelClosed(channelId);
|
|
170
|
+
});
|
|
167
171
|
}
|
|
168
172
|
const wire = {
|
|
169
173
|
traceId: requestId,
|
|
@@ -185,6 +185,7 @@ export interface PikkuHTTPResponse<Out = unknown> {
|
|
|
185
185
|
send?(data: string | ArrayBuffer | ArrayBufferView): this;
|
|
186
186
|
redirect(location: string, status?: number): this;
|
|
187
187
|
close?: () => void;
|
|
188
|
+
onClose?: (callback: () => void) => void;
|
|
188
189
|
setMode?: (mode: 'stream') => void;
|
|
189
190
|
flushHeaders?: () => void;
|
|
190
191
|
}
|
|
@@ -14,6 +14,7 @@ export declare class PikkuFetchHTTPResponse implements PikkuHTTPResponse {
|
|
|
14
14
|
send(data: any): this;
|
|
15
15
|
redirect(location: string, status?: number): this;
|
|
16
16
|
close(): this;
|
|
17
|
+
onClose(callback: () => void): void;
|
|
17
18
|
toResponse(args?: Record<string, any>): Response;
|
|
18
19
|
private createStream;
|
|
19
20
|
}
|
|
@@ -8,6 +8,7 @@ export class PikkuFetchHTTPResponse {
|
|
|
8
8
|
#send = null;
|
|
9
9
|
#close = null;
|
|
10
10
|
#cancelStream = null;
|
|
11
|
+
#onClose = [];
|
|
11
12
|
setMode(mode) {
|
|
12
13
|
this.#responseMode = 'stream';
|
|
13
14
|
if (mode === 'stream') {
|
|
@@ -95,6 +96,15 @@ export class PikkuFetchHTTPResponse {
|
|
|
95
96
|
}
|
|
96
97
|
return this;
|
|
97
98
|
}
|
|
99
|
+
onClose(callback) {
|
|
100
|
+
this.#onClose.push(callback);
|
|
101
|
+
}
|
|
102
|
+
#closed() {
|
|
103
|
+
const callbacks = this.#onClose;
|
|
104
|
+
this.#onClose = [];
|
|
105
|
+
for (const callback of callbacks)
|
|
106
|
+
callback();
|
|
107
|
+
}
|
|
98
108
|
toResponse(args) {
|
|
99
109
|
const cookieHeader = Array.from(this.#cookies.entries()).map(([name, { value, flags }]) => stringifySetCookie({ name, value, ...flags }, flags));
|
|
100
110
|
cookieHeader.forEach((cookie) => this.#headers.append('Set-Cookie', cookie));
|
|
@@ -119,9 +129,11 @@ export class PikkuFetchHTTPResponse {
|
|
|
119
129
|
return;
|
|
120
130
|
closed = true;
|
|
121
131
|
controller.close();
|
|
132
|
+
this.#closed();
|
|
122
133
|
};
|
|
123
134
|
this.#cancelStream = () => {
|
|
124
135
|
closed = true;
|
|
136
|
+
this.#closed();
|
|
125
137
|
};
|
|
126
138
|
this.#send = send;
|
|
127
139
|
this.#close = close;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PikkuFetchHTTPResponse } from './pikku-fetch-http-response.js';
|
|
1
2
|
export function toWebRequest(req, baseUrl) {
|
|
2
3
|
const proto = req.header('x-forwarded-proto') ?? 'http';
|
|
3
4
|
const host = req.header('x-forwarded-host') ?? req.header('host') ?? 'localhost';
|
|
@@ -120,6 +121,12 @@ export async function applyWebResponse(res, webResponse) {
|
|
|
120
121
|
if (setCookieValues.length > 0) {
|
|
121
122
|
res.header('Set-Cookie', setCookieValues);
|
|
122
123
|
}
|
|
124
|
+
if (webResponse.body &&
|
|
125
|
+
res instanceof PikkuFetchHTTPResponse &&
|
|
126
|
+
webResponse.headers.get('content-type')?.startsWith('text/event-stream')) {
|
|
127
|
+
res.send(webResponse.body);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
123
130
|
const body = await readBody(webResponse);
|
|
124
131
|
if (body !== null) {
|
|
125
132
|
if (res.send) {
|
|
@@ -14,6 +14,9 @@ export declare class RemoteAddonRequestError extends PikkuError {
|
|
|
14
14
|
readonly httpStatus: number;
|
|
15
15
|
constructor(namespace: string, fnName: string, status: number, detail: string);
|
|
16
16
|
}
|
|
17
|
+
export declare class AddonNotUsedError extends PikkuError {
|
|
18
|
+
constructor(caller: string, target: string);
|
|
19
|
+
}
|
|
17
20
|
export declare const resolveNamespace: (namespacedFunction: string) => ResolvedFunction | null;
|
|
18
21
|
export declare class ContextAwareRPCService {
|
|
19
22
|
private services;
|
|
@@ -26,6 +29,7 @@ export declare class ContextAwareRPCService {
|
|
|
26
29
|
}, packageName?: string | null);
|
|
27
30
|
rpcExposed(funcName: string, data: any): Promise<any>;
|
|
28
31
|
rpc<In = any, Out = any>(funcName: string, data: In): Promise<Out>;
|
|
32
|
+
private mapUsedAddon;
|
|
29
33
|
/**
|
|
30
34
|
* Resolution half of an addon RPC call: never executes anything, so an
|
|
31
35
|
* `RPCNotFoundError` thrown by the function itself can never be mistaken for
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { addonCallerOf, addonMayReach } from '../addon/addon-reach.js';
|
|
1
2
|
import { forwardStepName, isCompensationStepName, } from '../../function/compensation-name.js';
|
|
2
3
|
import { runPikkuFunc } from '../../function/function-runner.js';
|
|
3
4
|
import { addonInstanceForNamespace } from '../addon/addon-runner.js';
|
|
@@ -43,6 +44,15 @@ addError(RemoteAddonRequestError, {
|
|
|
43
44
|
status: 502,
|
|
44
45
|
message: 'Remote addon request failed.',
|
|
45
46
|
});
|
|
47
|
+
export class AddonNotUsedError extends PikkuError {
|
|
48
|
+
constructor(caller, target) {
|
|
49
|
+
super(`Addon '${caller}' called '${target}' but does not list it in its wireAddon 'uses'.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
addError(AddonNotUsedError, {
|
|
53
|
+
status: 403,
|
|
54
|
+
message: 'Addon called another addon it does not use.',
|
|
55
|
+
});
|
|
46
56
|
export const resolveNamespace = (namespacedFunction) => {
|
|
47
57
|
const colonIndex = namespacedFunction.indexOf(':');
|
|
48
58
|
if (colonIndex === -1) {
|
|
@@ -68,6 +78,7 @@ const resolvePikkuFunction = (rpcName, packageName = null) => {
|
|
|
68
78
|
if (pkgMeta) {
|
|
69
79
|
return { pikkuFuncId: pkgMeta.pikkuFuncId || rpcName, packageName };
|
|
70
80
|
}
|
|
81
|
+
throw new RPCNotFoundError(rpcName);
|
|
71
82
|
}
|
|
72
83
|
const rpc = pikkuState(null, 'rpc', 'meta');
|
|
73
84
|
let rpcMeta = rpc[rpcName];
|
|
@@ -119,6 +130,10 @@ export class ContextAwareRPCService {
|
|
|
119
130
|
this.packageName = packageName;
|
|
120
131
|
}
|
|
121
132
|
async rpcExposed(funcName, data) {
|
|
133
|
+
const addonCaller = addonCallerOf(this.wire);
|
|
134
|
+
if (addonCaller && !addonMayReach(addonCaller, funcName)) {
|
|
135
|
+
throw new AddonNotUsedError(addonCaller, funcName);
|
|
136
|
+
}
|
|
122
137
|
let functionMeta;
|
|
123
138
|
let resolvedAddon = null;
|
|
124
139
|
if (funcName.includes(':')) {
|
|
@@ -159,6 +174,7 @@ export class ContextAwareRPCService {
|
|
|
159
174
|
...this.wire,
|
|
160
175
|
};
|
|
161
176
|
if (funcName.includes(':')) {
|
|
177
|
+
funcName = this.mapUsedAddon(funcName);
|
|
162
178
|
const addonCall = this.resolveAddonFunction(funcName);
|
|
163
179
|
if (addonCall !== NOT_RESOLVED) {
|
|
164
180
|
return await this.executeAddonFunction(addonCall, data, updatedWire);
|
|
@@ -170,7 +186,7 @@ export class ContextAwareRPCService {
|
|
|
170
186
|
}
|
|
171
187
|
catch (e) {
|
|
172
188
|
if (e instanceof RPCNotFoundError) {
|
|
173
|
-
if (this.services.deploymentService) {
|
|
189
|
+
if (!this.packageName && this.services.deploymentService) {
|
|
174
190
|
const session = await resolveWireSession(this.wire);
|
|
175
191
|
return this.services.deploymentService.invoke(funcName, data, session, this.wire.traceId);
|
|
176
192
|
}
|
|
@@ -189,6 +205,20 @@ export class ContextAwareRPCService {
|
|
|
189
205
|
addonInstance,
|
|
190
206
|
});
|
|
191
207
|
}
|
|
208
|
+
mapUsedAddon(funcName) {
|
|
209
|
+
const caller = this.wire.addonNamespace;
|
|
210
|
+
if (!caller)
|
|
211
|
+
return funcName;
|
|
212
|
+
const colon = funcName.indexOf(':');
|
|
213
|
+
const prefix = funcName.slice(0, colon);
|
|
214
|
+
if (prefix === caller)
|
|
215
|
+
return funcName;
|
|
216
|
+
const uses = pikkuState(null, 'addons', 'packages').get(caller)?.uses;
|
|
217
|
+
const mapped = uses?.[prefix];
|
|
218
|
+
if (mapped)
|
|
219
|
+
return `${mapped}${funcName.slice(colon)}`;
|
|
220
|
+
throw new AddonNotUsedError(caller, funcName);
|
|
221
|
+
}
|
|
192
222
|
/**
|
|
193
223
|
* Resolution half of an addon RPC call: never executes anything, so an
|
|
194
224
|
* `RPCNotFoundError` thrown by the function itself can never be mistaken for
|
|
@@ -243,6 +273,9 @@ export class ContextAwareRPCService {
|
|
|
243
273
|
credentialGrants: resolved.addonConfig?.credentialGrants,
|
|
244
274
|
globalSecrets: resolved.addonConfig?.globalSecrets,
|
|
245
275
|
globalCredentials: resolved.addonConfig?.globalCredentials,
|
|
276
|
+
contentBucket: resolved.addonConfig?.contentBucket,
|
|
277
|
+
contentGrants: resolved.addonConfig?.contentGrants,
|
|
278
|
+
globalContent: resolved.addonConfig?.globalContent,
|
|
246
279
|
},
|
|
247
280
|
};
|
|
248
281
|
}
|
|
@@ -305,6 +338,7 @@ export class ContextAwareRPCService {
|
|
|
305
338
|
...wire,
|
|
306
339
|
};
|
|
307
340
|
if (rpcName.includes(':')) {
|
|
341
|
+
rpcName = this.mapUsedAddon(rpcName);
|
|
308
342
|
const addonCall = this.resolveAddonFunction(rpcName);
|
|
309
343
|
if (addonCall !== NOT_RESOLVED) {
|
|
310
344
|
return await this.executeAddonFunction(addonCall, data, mergedWire);
|
|
@@ -315,7 +349,9 @@ export class ContextAwareRPCService {
|
|
|
315
349
|
resolved = resolvePikkuFunction(rpcName, this.packageName);
|
|
316
350
|
}
|
|
317
351
|
catch (e) {
|
|
318
|
-
if (e instanceof RPCNotFoundError &&
|
|
352
|
+
if (e instanceof RPCNotFoundError &&
|
|
353
|
+
!this.packageName &&
|
|
354
|
+
this.services.deploymentService) {
|
|
319
355
|
const session = await resolveWireSession(mergedWire);
|
|
320
356
|
return this.services.deploymentService.invoke(rpcName, data, session, mergedWire.traceId);
|
|
321
357
|
}
|
|
@@ -337,6 +373,9 @@ export class ContextAwareRPCService {
|
|
|
337
373
|
if (!this.services.workflowService) {
|
|
338
374
|
throw new Error('WorkflowService service not available');
|
|
339
375
|
}
|
|
376
|
+
if (workflowName.includes(':')) {
|
|
377
|
+
workflowName = this.mapUsedAddon(workflowName);
|
|
378
|
+
}
|
|
340
379
|
const parentRunId = this.wire.workflowStep?.runId ?? this.wire.graph?.runId;
|
|
341
380
|
const wire = options?.wire ?? {
|
|
342
381
|
type: this.wire.wireType ?? 'unknown',
|
|
@@ -59,6 +59,12 @@ export interface ResolvedFunction {
|
|
|
59
59
|
globalSecrets?: string;
|
|
60
60
|
/** Set by the consuming app: hand this instance the unscoped `CredentialService` */
|
|
61
61
|
globalCredentials?: string;
|
|
62
|
+
/** Set by the consuming app: the content folder this instance owns */
|
|
63
|
+
contentBucket?: string;
|
|
64
|
+
/** Set by the consuming app: paths outside its folder this instance may read or write */
|
|
65
|
+
contentGrants?: Record<string, 'read' | 'write'>;
|
|
66
|
+
/** Set by the consuming app: hand this instance the unscoped `ContentService` */
|
|
67
|
+
globalContent?: string;
|
|
62
68
|
/** Set by `wireRemoteAddon`: dispatch this namespace's RPCs over HTTP */
|
|
63
69
|
remote?: boolean;
|
|
64
70
|
serverUrl?: string | ((services: any) => string | Promise<string>);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { defineScope, declareScopes } from './define-scope.js';
|
|
2
2
|
export { flattenScopeDefinitions, validateAndBuildScopeDefinitionsMeta, } from './validate-scope-definitions.js';
|
|
3
3
|
export type { CoreScopes, CoreScopeNode, FlatScope, ScopeDefinitionMeta, ScopeDefinitionsMeta, ScopeDefinitions, ScopeNodeMeta, ScopeOrigin, } from './scope.types.js';
|
|
4
|
-
export { hasScopes, verifyScopes } from '../../scopes.js';
|
|
4
|
+
export { hasScopes, intersectScopes, verifyScopes } from '../../scopes.js';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { defineScope, declareScopes } from './define-scope.js';
|
|
2
2
|
export { flattenScopeDefinitions, validateAndBuildScopeDefinitionsMeta, } from './validate-scope-definitions.js';
|
|
3
|
-
export { hasScopes, verifyScopes } from '../../scopes.js';
|
|
3
|
+
export { hasScopes, intersectScopes, verifyScopes } from '../../scopes.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/core",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.141",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/pikkujs/pikku.git",
|
|
@@ -87,6 +87,7 @@
|
|
|
87
87
|
"./state": "./dist/pikku-state.js",
|
|
88
88
|
"./classification": "./dist/classification/index.js",
|
|
89
89
|
"./utils": "./dist/utils.js",
|
|
90
|
+
"./lifecycle": "./dist/lifecycle.js",
|
|
90
91
|
"./schema": "./dist/schema.js",
|
|
91
92
|
"./testing": "./dist/testing/index.js",
|
|
92
93
|
"./dev": "./dist/dev/hot-reload.js"
|