@pikku/core 0.12.27 → 0.12.29
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 +14 -0
- package/dist/function/function-runner.d.ts +1 -2
- package/dist/function/function-runner.js +2 -22
- package/dist/function/functions.types.d.ts +2 -0
- package/dist/services/meta-service.js +18 -7
- package/dist/types/core.types.d.ts +2 -0
- package/dist/wirings/channel/channel-common.js +9 -2
- package/dist/wirings/channel/local/local-channel-handler.d.ts +7 -4
- package/dist/wirings/channel/local/local-channel-handler.js +13 -5
- package/dist/wirings/channel/local/local-channel-runner.js +1 -1
- package/dist/wirings/http/pikku-fetch-http-request.js +42 -8
- package/dist/wirings/workflow/pikku-workflow-service.js +10 -1
- package/package.json +1 -1
- package/src/dev/hot-reload.test.ts +2 -0
- package/src/function/function-runner.test.ts +2 -28
- package/src/function/function-runner.ts +2 -35
- package/src/function/functions.types.ts +2 -0
- package/src/services/meta-service.ts +17 -7
- package/src/types/core.types.ts +2 -0
- package/src/wirings/ai-agent/ai-agent-prepare.test.ts +2 -0
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +28 -4
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +3 -0
- package/src/wirings/channel/channel-common.ts +9 -2
- package/src/wirings/channel/local/local-channel-handler.ts +24 -9
- package/src/wirings/channel/local/local-channel-runner.ts +2 -1
- package/src/wirings/cli/cli-runner.test.ts +4 -0
- package/src/wirings/http/http-runner.test.ts +1 -0
- package/src/wirings/http/pikku-fetch-http-request.ts +43 -8
- package/src/wirings/queue/queue-runner.test.ts +1 -0
- package/src/wirings/scheduler/scheduler-runner.test.ts +10 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +10 -1
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## 0.12.29
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 294e365: Fix body stream caching in PikkuFetchHTTPRequest so that arrayBuffer() can be called after body() has already consumed the stream via text(). This is required for Auth.js CSRF validation to work correctly when integrated with Pikku's internal fetch.
|
|
6
|
+
|
|
7
|
+
## 0.12.28
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 2cf67be: Add inline option to pikkuFunc/pikkuSessionlessFunc for workflow step dispatch
|
|
12
|
+
|
|
13
|
+
By default, workflow steps now run inline (no queue hop). Set inline: false on a function to force dispatch through the queue for that step.
|
|
14
|
+
|
|
1
15
|
## 0.12.27
|
|
2
16
|
|
|
3
17
|
### Patch Changes
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { CoreUserSession, CorePikkuMiddleware, PikkuWiringTypes, PikkuWire, MiddlewareMetadata, PermissionMetadata, CoreSingletonServices, CreateWireServices } from '../types/core.types.js';
|
|
2
2
|
import type { CorePikkuChannelMiddleware } from '../wirings/channel/channel.types.js';
|
|
3
3
|
import type { CorePermissionGroup, CorePikkuFunctionConfig, CorePikkuPermission } from './functions.types.js';
|
|
4
4
|
import type { SessionService } from '../services/user-session-service.js';
|
|
@@ -6,7 +6,6 @@ import { PikkuCredentialWireService } from '../services/credential-wire-service.
|
|
|
6
6
|
export declare const addFunction: (funcName: string, funcConfig: CorePikkuFunctionConfig<any, any>, packageName?: string | null) => void;
|
|
7
7
|
export declare const getFunctionNames: (packageName?: string | null) => string[];
|
|
8
8
|
export declare const getAllFunctionNames: () => string[];
|
|
9
|
-
export declare const runPikkuFuncDirectly: <In, Out>(funcName: string, allServices: CoreServices, wire: PikkuWire, data: In, userSession?: SessionService<CoreUserSession>, packageName?: string | null) => Promise<Out>;
|
|
10
9
|
export declare const runPikkuFunc: <In = any, Out = any>(wireType: PikkuWiringTypes, wireId: string, funcName: string, { singletonServices, createWireServices, data, auth: wiringAuth, inheritedMiddleware, wireMiddleware, inheritedChannelMiddleware, wireChannelMiddleware, inheritedPermissions, wirePermissions, coerceDataFromSchema, tags, wire, sessionService, credentialWireService, packageName, }: {
|
|
11
10
|
singletonServices: CoreSingletonServices;
|
|
12
11
|
createWireServices?: CreateWireServices;
|
|
@@ -4,7 +4,7 @@ import { runPermissions } from '../permissions.js';
|
|
|
4
4
|
import { pikkuState } from '../pikku-state.js';
|
|
5
5
|
import { coerceTopLevelDataFromSchema, validateSchema } from '../schema.js';
|
|
6
6
|
import { parseVersionedId } from '../version.js';
|
|
7
|
-
import { PikkuSessionService
|
|
7
|
+
import { PikkuSessionService } from '../services/user-session-service.js';
|
|
8
8
|
import { ForbiddenError, ReadonlySessionError } from '../errors/errors.js';
|
|
9
9
|
import { PikkuCredentialWireService, createWireServicesCredentialWireProps, } from '../services/credential-wire-service.js';
|
|
10
10
|
import { defaultPikkuUserIdResolver } from '../services/pikku-user-id.js';
|
|
@@ -132,17 +132,6 @@ export const getAllFunctionNames = () => {
|
|
|
132
132
|
}
|
|
133
133
|
return functions;
|
|
134
134
|
};
|
|
135
|
-
export const runPikkuFuncDirectly = async (funcName, allServices, wire, data, userSession, packageName = null) => {
|
|
136
|
-
const funcConfig = pikkuState(packageName, 'function', 'functions').get(funcName);
|
|
137
|
-
if (!funcConfig) {
|
|
138
|
-
throw new Error(`Function not found: ${funcName}`);
|
|
139
|
-
}
|
|
140
|
-
const wireWithSession = {
|
|
141
|
-
...wire,
|
|
142
|
-
...(userSession && createFunctionSessionWireProps(userSession)),
|
|
143
|
-
};
|
|
144
|
-
return (await funcConfig.func(allServices, data, wireWithSession));
|
|
145
|
-
};
|
|
146
135
|
export const runPikkuFunc = async (wireType, wireId, funcName, { singletonServices, createWireServices, data, auth: wiringAuth, inheritedMiddleware, wireMiddleware, inheritedChannelMiddleware, wireChannelMiddleware, inheritedPermissions, wirePermissions, coerceDataFromSchema, tags = [], wire, sessionService, credentialWireService, packageName = null, }) => {
|
|
147
136
|
wire.wireType ??= wireType;
|
|
148
137
|
wire.wireId ??= wireId;
|
|
@@ -230,7 +219,7 @@ export const runPikkuFunc = async (wireType, wireId, funcName, { singletonServic
|
|
|
230
219
|
}
|
|
231
220
|
}
|
|
232
221
|
}
|
|
233
|
-
else
|
|
222
|
+
else {
|
|
234
223
|
if (wiringAuth === false || funcConfig.auth === false) {
|
|
235
224
|
resolvedSingletonServices.logger.warn(`Function '${funcName}' requires a session but auth was explicitly disabled — use pikkuSessionlessFunc instead.`);
|
|
236
225
|
}
|
|
@@ -238,15 +227,6 @@ export const runPikkuFunc = async (wireType, wireId, funcName, { singletonServic
|
|
|
238
227
|
throw new ForbiddenError('Authentication required');
|
|
239
228
|
}
|
|
240
229
|
}
|
|
241
|
-
else {
|
|
242
|
-
// TODO: Remove after a couple of releases — backward compat for
|
|
243
|
-
// generated metadata that doesn't include the `sessionless` field yet.
|
|
244
|
-
if (wiringAuth === true || funcConfig.auth === true) {
|
|
245
|
-
if (!session) {
|
|
246
|
-
throw new ForbiddenError('Authentication required');
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
230
|
if (session?.readonly && !funcMeta.readonly) {
|
|
251
231
|
throw new ReadonlySessionError();
|
|
252
232
|
}
|
|
@@ -149,6 +149,8 @@ export type CorePikkuFunctionConfig<PikkuFunction extends CorePikkuFunction<any,
|
|
|
149
149
|
readonly?: boolean;
|
|
150
150
|
deploy?: 'serverless' | 'server' | 'auto';
|
|
151
151
|
approvalRequired?: boolean;
|
|
152
|
+
/** When false, workflow steps calling this function are dispatched via the queue instead of running inline. Defaults to true (inline). */
|
|
153
|
+
inline?: boolean;
|
|
152
154
|
audit?: boolean | {
|
|
153
155
|
durability?: 'best-effort' | 'transactional';
|
|
154
156
|
};
|
|
@@ -285,15 +285,26 @@ export class LocalMetaService {
|
|
|
285
285
|
if (!emailsMeta.src) {
|
|
286
286
|
throw new Error('No generated email metadata found. Run `pikku emails generate`.');
|
|
287
287
|
}
|
|
288
|
+
// emailsMeta.src is an absolute path resolved by the CLI at generation time.
|
|
289
|
+
// Read directly rather than through readProjectFile (which prepends the project
|
|
290
|
+
// root and produces a wrong compound path when baseDir is absolute).
|
|
288
291
|
const baseDir = emailsMeta.src;
|
|
292
|
+
const readEmailFile = async (rel) => {
|
|
293
|
+
try {
|
|
294
|
+
return await readFile(join(baseDir, rel), 'utf-8');
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
};
|
|
289
300
|
const [themeRaw, localeRaw, layoutRaw, footerRaw, templateHtml, templateSubject, templateText,] = await Promise.all([
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
301
|
+
readEmailFile('theme.json'),
|
|
302
|
+
readEmailFile(`locales/${locale}.json`),
|
|
303
|
+
readEmailFile('partials/layout.html'),
|
|
304
|
+
readEmailFile('partials/footer.html'),
|
|
305
|
+
readEmailFile(`templates/${templateName}.html`),
|
|
306
|
+
readEmailFile(`templates/${templateName}.subject.txt`),
|
|
307
|
+
readEmailFile(`templates/${templateName}.text.txt`),
|
|
297
308
|
]);
|
|
298
309
|
return {
|
|
299
310
|
theme: themeRaw ? JSON.parse(themeRaw) : {},
|
|
@@ -82,6 +82,8 @@ export type FunctionRuntimeMeta = {
|
|
|
82
82
|
readonly?: boolean;
|
|
83
83
|
deploy?: 'serverless' | 'server' | 'auto';
|
|
84
84
|
sessionless?: boolean;
|
|
85
|
+
/** When false, workflow steps calling this function are dispatched via the queue. Defaults to true (inline). */
|
|
86
|
+
inline?: boolean;
|
|
85
87
|
version?: number;
|
|
86
88
|
approvalRequired?: boolean;
|
|
87
89
|
approvalDescription?: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { combineMiddleware, runMiddleware } from '../../middleware-runner.js';
|
|
2
|
-
import {
|
|
2
|
+
import { runPikkuFunc } from '../../function/function-runner.js';
|
|
3
3
|
import { combineChannelMiddleware, wrapChannelWithMiddleware, } from './channel-middleware-runner.js';
|
|
4
4
|
/**
|
|
5
5
|
* Runs a channel lifecycle function (onConnect or onDisconnect) with proper middleware handling.
|
|
@@ -37,7 +37,14 @@ export const runChannelLifecycleWithMiddleware = async ({ channelConfig, meta, l
|
|
|
37
37
|
wire = wrapChannelWithMiddleware(wire, services, allChannelMiddleware);
|
|
38
38
|
}
|
|
39
39
|
const runLifecycle = async () => {
|
|
40
|
-
return await
|
|
40
|
+
return await runPikkuFunc('channel', channelConfig.name, meta.pikkuFuncId, {
|
|
41
|
+
singletonServices: services,
|
|
42
|
+
data: () => data,
|
|
43
|
+
wire,
|
|
44
|
+
tags: meta.tags ?? [],
|
|
45
|
+
inheritedPermissions: meta.permissions,
|
|
46
|
+
packageName: meta.packageName ?? null,
|
|
47
|
+
});
|
|
41
48
|
};
|
|
42
49
|
if (allMiddleware.length > 0) {
|
|
43
50
|
return await runMiddleware(services, wire, allMiddleware, runLifecycle);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Logger } from '../../../services/logger.js';
|
|
1
2
|
import type { BinaryData } from '../channel.types.js';
|
|
2
3
|
import { PikkuAbstractChannelHandler } from '../pikku-abstract-channel-handler.js';
|
|
3
4
|
export declare class PikkuLocalChannelHandler<OpeningData = unknown, Out = unknown> extends PikkuAbstractChannelHandler<OpeningData, Out> {
|
|
@@ -7,14 +8,16 @@ export declare class PikkuLocalChannelHandler<OpeningData = unknown, Out = unkno
|
|
|
7
8
|
private closeCallbacks;
|
|
8
9
|
private sendCallback?;
|
|
9
10
|
private sendBinaryCallback?;
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
private logger?;
|
|
12
|
+
constructor(channelId: string, channelName: string, openingData: OpeningData, logger?: Logger);
|
|
13
|
+
registerOnOpen(callback: () => Promise<void> | void): void;
|
|
14
|
+
open(): Promise<void>;
|
|
12
15
|
registerOnMessage(callback: (data: any) => Promise<unknown>): void;
|
|
13
16
|
message(data: unknown): Promise<unknown>;
|
|
14
17
|
registerOnBinaryMessage(callback: (data: BinaryData) => Promise<BinaryData | void> | BinaryData | void): void;
|
|
15
18
|
binaryMessage(data: BinaryData): Promise<BinaryData | void>;
|
|
16
|
-
registerOnClose(callback: () => void): void;
|
|
17
|
-
close(): void
|
|
19
|
+
registerOnClose(callback: () => Promise<void> | void): void;
|
|
20
|
+
close(): Promise<void>;
|
|
18
21
|
registerOnSend(send: (message: Out) => void): void;
|
|
19
22
|
send(message: Out, isBinary?: boolean): void;
|
|
20
23
|
registerOnSendBinary(send: (data: BinaryData) => void): void;
|
|
@@ -6,13 +6,18 @@ export class PikkuLocalChannelHandler extends PikkuAbstractChannelHandler {
|
|
|
6
6
|
closeCallbacks = [];
|
|
7
7
|
sendCallback;
|
|
8
8
|
sendBinaryCallback;
|
|
9
|
+
logger;
|
|
10
|
+
constructor(channelId, channelName, openingData, logger) {
|
|
11
|
+
super(channelId, channelName, openingData);
|
|
12
|
+
this.logger = logger;
|
|
13
|
+
}
|
|
9
14
|
registerOnOpen(callback) {
|
|
10
15
|
this.openCallBack = callback;
|
|
11
16
|
}
|
|
12
|
-
open() {
|
|
17
|
+
async open() {
|
|
13
18
|
this.getChannel().state = 'open';
|
|
14
19
|
if (this.openCallBack) {
|
|
15
|
-
this.openCallBack();
|
|
20
|
+
await this.openCallBack();
|
|
16
21
|
}
|
|
17
22
|
}
|
|
18
23
|
registerOnMessage(callback) {
|
|
@@ -30,13 +35,16 @@ export class PikkuLocalChannelHandler extends PikkuAbstractChannelHandler {
|
|
|
30
35
|
registerOnClose(callback) {
|
|
31
36
|
this.closeCallbacks.push(callback);
|
|
32
37
|
}
|
|
33
|
-
close() {
|
|
38
|
+
async close() {
|
|
34
39
|
if (this.getChannel().state === 'closed') {
|
|
35
40
|
return;
|
|
36
41
|
}
|
|
37
42
|
super.close();
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
const results = await Promise.allSettled(this.closeCallbacks.map((cb) => cb()));
|
|
44
|
+
for (const result of results) {
|
|
45
|
+
if (result.status === 'rejected') {
|
|
46
|
+
this.logger?.error('Error in channel close callback:', result.reason);
|
|
47
|
+
}
|
|
40
48
|
}
|
|
41
49
|
}
|
|
42
50
|
registerOnSend(send) {
|
|
@@ -59,7 +59,7 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
|
|
|
59
59
|
}
|
|
60
60
|
const main = async () => {
|
|
61
61
|
try {
|
|
62
|
-
channelHandler = new PikkuLocalChannelHandler(channelId, channelConfig.name, openingData);
|
|
62
|
+
channelHandler = new PikkuLocalChannelHandler(channelId, channelConfig.name, openingData, singletonServices.logger);
|
|
63
63
|
const channel = channelHandler.getChannel();
|
|
64
64
|
const wire = {
|
|
65
65
|
channel,
|
|
@@ -11,6 +11,9 @@ export class PikkuFetchHTTPRequest {
|
|
|
11
11
|
#cookies;
|
|
12
12
|
#params = {};
|
|
13
13
|
#url;
|
|
14
|
+
#rawBodyText;
|
|
15
|
+
#rawBodyBuffer;
|
|
16
|
+
#bodyRead = false;
|
|
14
17
|
constructor(request) {
|
|
15
18
|
this.request = request;
|
|
16
19
|
this.#url = new URL(request.url);
|
|
@@ -25,15 +28,45 @@ export class PikkuFetchHTTPRequest {
|
|
|
25
28
|
* Retrieves the request body.
|
|
26
29
|
* @returns A promise that resolves to the request body.
|
|
27
30
|
*/
|
|
28
|
-
json() {
|
|
29
|
-
|
|
31
|
+
async json() {
|
|
32
|
+
const text = await this.#readRawText();
|
|
33
|
+
return JSON.parse(text);
|
|
30
34
|
}
|
|
31
35
|
/**
|
|
32
36
|
* Retrieves the raw request body as a Buffer.
|
|
33
37
|
* @returns A promise that resolves to the raw request body.
|
|
34
38
|
*/
|
|
35
|
-
arrayBuffer() {
|
|
36
|
-
|
|
39
|
+
async arrayBuffer() {
|
|
40
|
+
if (this.#rawBodyBuffer !== undefined) {
|
|
41
|
+
return this.#rawBodyBuffer;
|
|
42
|
+
}
|
|
43
|
+
if (this.#bodyRead) {
|
|
44
|
+
if (this.#rawBodyText !== undefined) {
|
|
45
|
+
const buf = new TextEncoder().encode(this.#rawBodyText)
|
|
46
|
+
.buffer;
|
|
47
|
+
this.#rawBodyBuffer = buf;
|
|
48
|
+
return buf;
|
|
49
|
+
}
|
|
50
|
+
return new ArrayBuffer(0);
|
|
51
|
+
}
|
|
52
|
+
const buf = await this.request.arrayBuffer();
|
|
53
|
+
this.#rawBodyBuffer = buf;
|
|
54
|
+
this.#bodyRead = true;
|
|
55
|
+
return buf;
|
|
56
|
+
}
|
|
57
|
+
async #readRawText() {
|
|
58
|
+
if (this.#rawBodyText !== undefined) {
|
|
59
|
+
return this.#rawBodyText;
|
|
60
|
+
}
|
|
61
|
+
if (this.#rawBodyBuffer !== undefined) {
|
|
62
|
+
const text = new TextDecoder().decode(this.#rawBodyBuffer);
|
|
63
|
+
this.#rawBodyText = text;
|
|
64
|
+
return text;
|
|
65
|
+
}
|
|
66
|
+
const text = await this.request.text();
|
|
67
|
+
this.#rawBodyText = text;
|
|
68
|
+
this.#bodyRead = true;
|
|
69
|
+
return text;
|
|
37
70
|
}
|
|
38
71
|
headers() {
|
|
39
72
|
return Object.fromEntries(this.request.headers.entries());
|
|
@@ -111,7 +144,8 @@ export class PikkuFetchHTTPRequest {
|
|
|
111
144
|
const contentType = this.header('content-type') || '';
|
|
112
145
|
try {
|
|
113
146
|
if (contentType.includes('application/json')) {
|
|
114
|
-
const
|
|
147
|
+
const text = await this.#readRawText();
|
|
148
|
+
const parsed = text ? JSON.parse(text) : null;
|
|
115
149
|
body =
|
|
116
150
|
typeof parsed === 'object' &&
|
|
117
151
|
parsed !== null &&
|
|
@@ -120,15 +154,15 @@ export class PikkuFetchHTTPRequest {
|
|
|
120
154
|
: { data: parsed };
|
|
121
155
|
}
|
|
122
156
|
else if (contentType.includes('text/')) {
|
|
123
|
-
const text = await this
|
|
157
|
+
const text = await this.#readRawText();
|
|
124
158
|
body = { data: text };
|
|
125
159
|
}
|
|
126
160
|
else if (contentType.includes('application/octet-stream')) {
|
|
127
|
-
const buffer = await this.
|
|
161
|
+
const buffer = await this.arrayBuffer();
|
|
128
162
|
body = { data: buffer };
|
|
129
163
|
}
|
|
130
164
|
else if (contentType === 'application/x-www-form-urlencoded') {
|
|
131
|
-
const text = await this
|
|
165
|
+
const text = await this.#readRawText();
|
|
132
166
|
const params = new URLSearchParams(text);
|
|
133
167
|
let count = 0;
|
|
134
168
|
for (const _ of params) {
|
|
@@ -484,7 +484,16 @@ export class PikkuWorkflowService {
|
|
|
484
484
|
* through to the inline execution path.
|
|
485
485
|
*/
|
|
486
486
|
async dispatchStep(runId, stepName, rpcName, data, stepOptions) {
|
|
487
|
-
if (
|
|
487
|
+
if (!getSingletonServices()?.queueService) {
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
// Functions default to inline execution. Only dispatch via queue when the
|
|
491
|
+
// function explicitly sets inline: false.
|
|
492
|
+
const functionsMeta = pikkuState(null, 'function', 'meta');
|
|
493
|
+
const rpcFuncId = pikkuState(null, 'rpc', 'meta')[rpcName];
|
|
494
|
+
const rpcMeta = typeof rpcFuncId === 'string' ? functionsMeta[rpcFuncId] : undefined;
|
|
495
|
+
const forceQueue = rpcMeta?.inline === false;
|
|
496
|
+
if (!forceQueue && this.isInline(runId)) {
|
|
488
497
|
return false;
|
|
489
498
|
}
|
|
490
499
|
const retries = stepOptions?.retries ?? 0;
|
package/package.json
CHANGED
|
@@ -308,6 +308,7 @@ describe('pikkuDevReloader', { concurrency: false }, () => {
|
|
|
308
308
|
pikkuFuncId: 'hotTask',
|
|
309
309
|
inputSchemaName: null,
|
|
310
310
|
outputSchemaName: null,
|
|
311
|
+
sessionless: true,
|
|
311
312
|
}
|
|
312
313
|
|
|
313
314
|
addFunction('hotTask', {
|
|
@@ -375,6 +376,7 @@ describe('pikkuDevReloader', { concurrency: false }, () => {
|
|
|
375
376
|
pikkuFuncId: 'queue_hot-queue',
|
|
376
377
|
inputSchemaName: null,
|
|
377
378
|
outputSchemaName: null,
|
|
379
|
+
sessionless: true,
|
|
378
380
|
}
|
|
379
381
|
|
|
380
382
|
addFunction('queue_hot-queue', {
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
getAllFunctionNames,
|
|
6
6
|
getFunctionNames,
|
|
7
7
|
runPikkuFunc,
|
|
8
|
-
runPikkuFuncDirectly,
|
|
9
8
|
} from './function-runner.js'
|
|
10
9
|
import { addTagMiddleware, addTagPermission } from '../index.js'
|
|
11
10
|
import { resetPikkuState, pikkuState } from '../pikku-state.js'
|
|
@@ -33,6 +32,7 @@ const addTestFunction = (funcName: string, funcConfig: any) => {
|
|
|
33
32
|
pikkuFuncId: funcName,
|
|
34
33
|
inputSchemaName: null,
|
|
35
34
|
outputSchemaName: null,
|
|
35
|
+
sessionless: true,
|
|
36
36
|
middleware,
|
|
37
37
|
permissions,
|
|
38
38
|
}
|
|
@@ -676,7 +676,7 @@ describe('runPikkuFunc - Integration Tests', () => {
|
|
|
676
676
|
)
|
|
677
677
|
})
|
|
678
678
|
|
|
679
|
-
test('should
|
|
679
|
+
test('should require auth when sessionless metadata is absent (undefined defaults to session-required)', async () => {
|
|
680
680
|
addTestFunction('legacyAuth', {
|
|
681
681
|
func: async () => 'ok',
|
|
682
682
|
auth: true,
|
|
@@ -1227,32 +1227,6 @@ describe('runPikkuFunc - Integration Tests', () => {
|
|
|
1227
1227
|
})
|
|
1228
1228
|
|
|
1229
1229
|
describe('function-runner helpers', () => {
|
|
1230
|
-
test('runPikkuFuncDirectly should pass through the provided wire and session helpers', async () => {
|
|
1231
|
-
let receivedWire: any
|
|
1232
|
-
const sessionService = new PikkuSessionService({
|
|
1233
|
-
get: async () => undefined,
|
|
1234
|
-
} as any)
|
|
1235
|
-
|
|
1236
|
-
addTestFunction('directFunc', {
|
|
1237
|
-
func: async (_services: any, _data: any, wire: any) => {
|
|
1238
|
-
receivedWire = wire
|
|
1239
|
-
return 'ok'
|
|
1240
|
-
},
|
|
1241
|
-
})
|
|
1242
|
-
|
|
1243
|
-
const result = await runPikkuFuncDirectly(
|
|
1244
|
-
'directFunc',
|
|
1245
|
-
mockServices,
|
|
1246
|
-
{ traceId: 'trace-1' },
|
|
1247
|
-
{ hello: 'world' },
|
|
1248
|
-
sessionService
|
|
1249
|
-
)
|
|
1250
|
-
|
|
1251
|
-
assert.equal(result, 'ok')
|
|
1252
|
-
assert.equal(receivedWire.traceId, 'trace-1')
|
|
1253
|
-
assert.equal(typeof receivedWire.getSession, 'function')
|
|
1254
|
-
})
|
|
1255
|
-
|
|
1256
1230
|
test('getFunctionNames and getAllFunctionNames should include addon namespaces', () => {
|
|
1257
1231
|
addTestFunction('rootFunc', { func: async () => 'ok' })
|
|
1258
1232
|
pikkuState(null, 'addons', 'packages').set('stripe', {
|
|
@@ -7,7 +7,6 @@ import { runPermissions } from '../permissions.js'
|
|
|
7
7
|
import { pikkuState } from '../pikku-state.js'
|
|
8
8
|
import { coerceTopLevelDataFromSchema, validateSchema } from '../schema.js'
|
|
9
9
|
import type {
|
|
10
|
-
CoreServices,
|
|
11
10
|
CoreUserSession,
|
|
12
11
|
CorePikkuMiddleware,
|
|
13
12
|
PikkuWiringTypes,
|
|
@@ -26,10 +25,7 @@ import type {
|
|
|
26
25
|
} from './functions.types.js'
|
|
27
26
|
import { parseVersionedId } from '../version.js'
|
|
28
27
|
import type { SessionService } from '../services/user-session-service.js'
|
|
29
|
-
import {
|
|
30
|
-
PikkuSessionService,
|
|
31
|
-
createFunctionSessionWireProps,
|
|
32
|
-
} from '../services/user-session-service.js'
|
|
28
|
+
import { PikkuSessionService } from '../services/user-session-service.js'
|
|
33
29
|
import { ForbiddenError, ReadonlySessionError } from '../errors/errors.js'
|
|
34
30
|
import {
|
|
35
31
|
PikkuCredentialWireService,
|
|
@@ -200,27 +196,6 @@ export const getAllFunctionNames = (): string[] => {
|
|
|
200
196
|
return functions
|
|
201
197
|
}
|
|
202
198
|
|
|
203
|
-
export const runPikkuFuncDirectly = async <In, Out>(
|
|
204
|
-
funcName: string,
|
|
205
|
-
allServices: CoreServices,
|
|
206
|
-
wire: PikkuWire,
|
|
207
|
-
data: In,
|
|
208
|
-
userSession?: SessionService<CoreUserSession>,
|
|
209
|
-
packageName: string | null = null
|
|
210
|
-
) => {
|
|
211
|
-
const funcConfig = pikkuState(packageName, 'function', 'functions').get(
|
|
212
|
-
funcName
|
|
213
|
-
)
|
|
214
|
-
if (!funcConfig) {
|
|
215
|
-
throw new Error(`Function not found: ${funcName}`)
|
|
216
|
-
}
|
|
217
|
-
const wireWithSession = {
|
|
218
|
-
...wire,
|
|
219
|
-
...(userSession && createFunctionSessionWireProps(userSession)),
|
|
220
|
-
}
|
|
221
|
-
return (await funcConfig.func(allServices, data, wireWithSession)) as Out
|
|
222
|
-
}
|
|
223
|
-
|
|
224
199
|
export const runPikkuFunc = async <In = any, Out = any>(
|
|
225
200
|
wireType: PikkuWiringTypes,
|
|
226
201
|
wireId: string,
|
|
@@ -381,7 +356,7 @@ export const runPikkuFunc = async <In = any, Out = any>(
|
|
|
381
356
|
throw new ForbiddenError('Authentication required')
|
|
382
357
|
}
|
|
383
358
|
}
|
|
384
|
-
} else
|
|
359
|
+
} else {
|
|
385
360
|
if (wiringAuth === false || funcConfig.auth === false) {
|
|
386
361
|
resolvedSingletonServices.logger.warn(
|
|
387
362
|
`Function '${funcName}' requires a session but auth was explicitly disabled — use pikkuSessionlessFunc instead.`
|
|
@@ -390,14 +365,6 @@ export const runPikkuFunc = async <In = any, Out = any>(
|
|
|
390
365
|
if (!session) {
|
|
391
366
|
throw new ForbiddenError('Authentication required')
|
|
392
367
|
}
|
|
393
|
-
} else {
|
|
394
|
-
// TODO: Remove after a couple of releases — backward compat for
|
|
395
|
-
// generated metadata that doesn't include the `sessionless` field yet.
|
|
396
|
-
if (wiringAuth === true || funcConfig.auth === true) {
|
|
397
|
-
if (!session) {
|
|
398
|
-
throw new ForbiddenError('Authentication required')
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
368
|
}
|
|
402
369
|
|
|
403
370
|
if ((session as any)?.readonly && !funcMeta.readonly) {
|
|
@@ -295,6 +295,8 @@ export type CorePikkuFunctionConfig<
|
|
|
295
295
|
readonly?: boolean
|
|
296
296
|
deploy?: 'serverless' | 'server' | 'auto'
|
|
297
297
|
approvalRequired?: boolean
|
|
298
|
+
/** When false, workflow steps calling this function are dispatched via the queue instead of running inline. Defaults to true (inline). */
|
|
299
|
+
inline?: boolean
|
|
298
300
|
audit?:
|
|
299
301
|
| boolean
|
|
300
302
|
| {
|
|
@@ -533,7 +533,17 @@ export class LocalMetaService implements MetaService {
|
|
|
533
533
|
)
|
|
534
534
|
}
|
|
535
535
|
|
|
536
|
+
// emailsMeta.src is an absolute path resolved by the CLI at generation time.
|
|
537
|
+
// Read directly rather than through readProjectFile (which prepends the project
|
|
538
|
+
// root and produces a wrong compound path when baseDir is absolute).
|
|
536
539
|
const baseDir = emailsMeta.src
|
|
540
|
+
const readEmailFile = async (rel: string): Promise<string | null> => {
|
|
541
|
+
try {
|
|
542
|
+
return await readFile(join(baseDir, rel), 'utf-8')
|
|
543
|
+
} catch {
|
|
544
|
+
return null
|
|
545
|
+
}
|
|
546
|
+
}
|
|
537
547
|
const [
|
|
538
548
|
themeRaw,
|
|
539
549
|
localeRaw,
|
|
@@ -543,13 +553,13 @@ export class LocalMetaService implements MetaService {
|
|
|
543
553
|
templateSubject,
|
|
544
554
|
templateText,
|
|
545
555
|
] = await Promise.all([
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
556
|
+
readEmailFile('theme.json'),
|
|
557
|
+
readEmailFile(`locales/${locale}.json`),
|
|
558
|
+
readEmailFile('partials/layout.html'),
|
|
559
|
+
readEmailFile('partials/footer.html'),
|
|
560
|
+
readEmailFile(`templates/${templateName}.html`),
|
|
561
|
+
readEmailFile(`templates/${templateName}.subject.txt`),
|
|
562
|
+
readEmailFile(`templates/${templateName}.text.txt`),
|
|
553
563
|
])
|
|
554
564
|
|
|
555
565
|
return {
|
package/src/types/core.types.ts
CHANGED
|
@@ -118,6 +118,8 @@ export type FunctionRuntimeMeta = {
|
|
|
118
118
|
readonly?: boolean
|
|
119
119
|
deploy?: 'serverless' | 'server' | 'auto'
|
|
120
120
|
sessionless?: boolean
|
|
121
|
+
/** When false, workflow steps calling this function are dispatched via the queue. Defaults to true (inline). */
|
|
122
|
+
inline?: boolean
|
|
121
123
|
version?: number
|
|
122
124
|
approvalRequired?: boolean
|
|
123
125
|
approvalDescription?: string
|
|
@@ -178,6 +178,7 @@ describe('ai-agent-prepare', () => {
|
|
|
178
178
|
description: 'Deploy the service',
|
|
179
179
|
approvalRequired: true,
|
|
180
180
|
inputSchemaName: 'DeployInput',
|
|
181
|
+
sessionless: true,
|
|
181
182
|
}
|
|
182
183
|
pikkuState(null, 'misc', 'schemas').set('DeployInput', {
|
|
183
184
|
type: 'object',
|
|
@@ -254,6 +255,7 @@ describe('ai-agent-prepare', () => {
|
|
|
254
255
|
description: 'Secret tool',
|
|
255
256
|
permissions: ['admin'],
|
|
256
257
|
inputSchemaName: 'SecretInput',
|
|
258
|
+
sessionless: true,
|
|
257
259
|
}
|
|
258
260
|
pikkuState(null, 'misc', 'schemas').set('SecretInput', {
|
|
259
261
|
type: 'object',
|
|
@@ -560,6 +560,7 @@ describe('runAIAgent', () => {
|
|
|
560
560
|
description: 'Deploy',
|
|
561
561
|
approvalRequired: true,
|
|
562
562
|
inputSchemaName: 'DeployInput',
|
|
563
|
+
sessionless: true,
|
|
563
564
|
}
|
|
564
565
|
pikkuState(null, 'misc', 'schemas').set('DeployInput', {
|
|
565
566
|
type: 'object',
|
|
@@ -716,6 +717,7 @@ describe('resumeAIAgentSync', () => {
|
|
|
716
717
|
pikkuState(null, 'function', 'meta').deploy = {
|
|
717
718
|
description: 'Deploy',
|
|
718
719
|
inputSchemaName: 'DeployInput',
|
|
720
|
+
sessionless: true,
|
|
719
721
|
}
|
|
720
722
|
pikkuState(null, 'misc', 'schemas').set('DeployInput', {
|
|
721
723
|
type: 'object',
|
|
@@ -901,6 +903,7 @@ describe('resumeAIAgentSync', () => {
|
|
|
901
903
|
pikkuState(null, 'function', 'meta').deploy = {
|
|
902
904
|
description: 'Deploy',
|
|
903
905
|
inputSchemaName: 'DeployInput',
|
|
906
|
+
sessionless: true,
|
|
904
907
|
}
|
|
905
908
|
pikkuState(null, 'misc', 'schemas').set('DeployInput', {
|
|
906
909
|
type: 'object',
|
|
@@ -986,7 +989,12 @@ describe('getCredential API key override', () => {
|
|
|
986
989
|
}
|
|
987
990
|
|
|
988
991
|
const mockServices = {
|
|
989
|
-
logger: {
|
|
992
|
+
logger: {
|
|
993
|
+
info: () => {},
|
|
994
|
+
warn: () => {},
|
|
995
|
+
error: () => {},
|
|
996
|
+
debug: () => {},
|
|
997
|
+
},
|
|
990
998
|
aiAgentRunner: {
|
|
991
999
|
run: async (): Promise<AIAgentStepResult> => {
|
|
992
1000
|
originalRunCalls.push('original')
|
|
@@ -1019,13 +1027,24 @@ describe('getCredential API key override', () => {
|
|
|
1019
1027
|
const originalRunCalls: string[] = []
|
|
1020
1028
|
|
|
1021
1029
|
const mockServices = {
|
|
1022
|
-
logger: {
|
|
1030
|
+
logger: {
|
|
1031
|
+
info: () => {},
|
|
1032
|
+
warn: () => {},
|
|
1033
|
+
error: () => {},
|
|
1034
|
+
debug: () => {},
|
|
1035
|
+
},
|
|
1023
1036
|
aiAgentRunner: {
|
|
1024
1037
|
run: async (): Promise<AIAgentStepResult> => {
|
|
1025
1038
|
originalRunCalls.push('original')
|
|
1026
1039
|
return makeStepResult({ text: 'from-original', finishReason: 'stop' })
|
|
1027
1040
|
},
|
|
1028
|
-
withApiKey: (_key: string) => ({
|
|
1041
|
+
withApiKey: (_key: string) => ({
|
|
1042
|
+
run: async () =>
|
|
1043
|
+
makeStepResult({
|
|
1044
|
+
text: 'should-not-be-called',
|
|
1045
|
+
finishReason: 'stop',
|
|
1046
|
+
}),
|
|
1047
|
+
}),
|
|
1029
1048
|
},
|
|
1030
1049
|
aiRunState: {
|
|
1031
1050
|
createRun: async () => 'run-no-cred',
|
|
@@ -1051,7 +1070,12 @@ describe('getCredential API key override', () => {
|
|
|
1051
1070
|
const originalRunCalls: string[] = []
|
|
1052
1071
|
|
|
1053
1072
|
const mockServices = {
|
|
1054
|
-
logger: {
|
|
1073
|
+
logger: {
|
|
1074
|
+
info: () => {},
|
|
1075
|
+
warn: () => {},
|
|
1076
|
+
error: () => {},
|
|
1077
|
+
debug: () => {},
|
|
1078
|
+
},
|
|
1055
1079
|
aiAgentRunner: {
|
|
1056
1080
|
run: async (): Promise<AIAgentStepResult> => {
|
|
1057
1081
|
originalRunCalls.push('original')
|