@pikku/core 0.12.26 → 0.12.28

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 (60) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/data-classification.d.ts +7 -3
  3. package/dist/errors/error-handler.js +1 -0
  4. package/dist/function/function-runner.d.ts +1 -2
  5. package/dist/function/function-runner.js +2 -22
  6. package/dist/function/functions.types.d.ts +2 -0
  7. package/dist/handle-error.js +1 -0
  8. package/dist/index.d.ts +3 -2
  9. package/dist/index.js +2 -1
  10. package/dist/pikku-state.d.ts +1 -0
  11. package/dist/pikku-state.js +3 -0
  12. package/dist/services/gopass-secrets.d.ts +1 -0
  13. package/dist/services/gopass-secrets.js +9 -0
  14. package/dist/services/local-secrets.d.ts +1 -0
  15. package/dist/services/local-secrets.js +9 -0
  16. package/dist/services/meta-service.js +18 -7
  17. package/dist/services/scoped-secret-service.d.ts +1 -0
  18. package/dist/services/scoped-secret-service.js +4 -0
  19. package/dist/services/secret-service.d.ts +7 -0
  20. package/dist/services/typed-secret-service.d.ts +1 -0
  21. package/dist/services/typed-secret-service.js +3 -0
  22. package/dist/test-utils.d.ts +18 -0
  23. package/dist/test-utils.js +33 -0
  24. package/dist/types/core.types.d.ts +2 -0
  25. package/dist/wirings/channel/channel-common.js +9 -2
  26. package/dist/wirings/channel/local/local-channel-handler.d.ts +7 -4
  27. package/dist/wirings/channel/local/local-channel-handler.js +13 -5
  28. package/dist/wirings/channel/local/local-channel-runner.js +1 -1
  29. package/dist/wirings/workflow/pikku-workflow-service.js +10 -1
  30. package/package.json +1 -1
  31. package/src/data-classification.ts +11 -4
  32. package/src/dev/hot-reload.test.ts +2 -0
  33. package/src/errors/error-handler.ts +1 -0
  34. package/src/function/function-runner.test.ts +2 -28
  35. package/src/function/function-runner.ts +2 -35
  36. package/src/function/functions.types.ts +2 -0
  37. package/src/handle-error.test.ts +1 -0
  38. package/src/handle-error.ts +1 -0
  39. package/src/index.ts +7 -1
  40. package/src/pikku-state.ts +4 -0
  41. package/src/services/gopass-secrets.ts +10 -0
  42. package/src/services/local-secrets.ts +10 -0
  43. package/src/services/meta-service.ts +17 -7
  44. package/src/services/scoped-secret-service.ts +5 -0
  45. package/src/services/secret-service.ts +7 -0
  46. package/src/services/typed-secret-service.ts +4 -0
  47. package/src/test-utils.ts +38 -0
  48. package/src/types/core.types.ts +2 -0
  49. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +2 -0
  50. package/src/wirings/ai-agent/ai-agent-runner.test.ts +28 -4
  51. package/src/wirings/ai-agent/ai-agent-stream.test.ts +3 -0
  52. package/src/wirings/channel/channel-common.ts +9 -2
  53. package/src/wirings/channel/local/local-channel-handler.ts +24 -9
  54. package/src/wirings/channel/local/local-channel-runner.ts +2 -1
  55. package/src/wirings/cli/cli-runner.test.ts +4 -0
  56. package/src/wirings/http/http-runner.test.ts +1 -0
  57. package/src/wirings/queue/queue-runner.test.ts +1 -0
  58. package/src/wirings/scheduler/scheduler-runner.test.ts +10 -0
  59. package/src/wirings/workflow/pikku-workflow-service.ts +10 -1
  60. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,33 @@
1
+ ## 0.12.28
2
+
3
+ ### Patch Changes
4
+
5
+ - 2cf67be: Add inline option to pikkuFunc/pikkuSessionlessFunc for workflow step dispatch
6
+
7
+ 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.
8
+
9
+ ## 0.12.27
10
+
11
+ ### Patch Changes
12
+
13
+ - 4b5c75b: feat(auth-js): wire OIDC config (issuer/tenantId) as variables, expand provider registry
14
+ - Move `issuer` and `tenantId` out of the secret blob for OIDC providers (auth0, okta, azure-ad, keycloak, cognito, microsoft-entra-id) — they are public config URLs, not secrets. Now registered via `wireVariable` and loaded at runtime via `services.variables.get()`.
15
+ - Expand provider registry from 13 to 31 providers: reddit, notion, instagram, zoom, figma, tiktok, threads, patreon, dropbox, bitbucket, hubspot, salesforce, atlassian, strava, keycloak, cognito, microsoft-entra-id added.
16
+ - `serialize-auth-gen` emits `wireVariable({...})` declarations and `services.variables.get()` calls in the generated factory for OIDC providers.
17
+ - Integration verifier exercises real `/auth/providers` endpoint with `LocalSecretService` + `LocalVariablesService`, including a spy test proving `services.variables.get('AUTH0_ISSUER')` is called at request time.
18
+
19
+ - 4b5c75b: Add end-to-end data classification for SQLite and Postgres projects.
20
+
21
+ **Core (`@pikku/core`):** New `Private<T>` and `Secret<T>` intersection brands, `ClassificationManifest`, `ColumnClassification`, and `AnonymizeStrategy` types exported from `data-classification.ts`.
22
+
23
+ **CLI (`@pikku/cli`):**
24
+ - SQL comment annotations: `-- @public`, `-- @private[:strategy]`, `-- @secret[:strategy]` on `CREATE TABLE` columns and `ALTER TABLE ... ADD COLUMN` statements. Unannotated columns default to `private`.
25
+ - `pikku db migrate` now emits a `classification.gen.ts` manifest alongside `schema.d.ts`.
26
+ - New `pikku db audit` command — prints a per-column classification summary and warns on `private`/`secret` columns with no anonymize strategy.
27
+ - Postgres dialect support in `resolveDb`, `PostgresMigrationExecutor`, and `PostgresIntrospector`.
28
+
29
+ **Inspector (`@pikku/inspector`):** New PKU910 check — `findPiiPaths()` walks inferred function return types looking for `__pii__` brands (including inside `Array<T>`, `Record<K,V>`, and index signatures) and fails the build if a function exposes branded fields in its output.
30
+
1
31
  ## 0.12.26
2
32
 
3
33
  ### Patch Changes
@@ -1,14 +1,18 @@
1
1
  export type Private<T> = T & {
2
- readonly __pii__: 'private';
2
+ readonly __classification__: 'private';
3
+ };
4
+ export type Pii<T> = T & {
5
+ readonly __classification__: 'pii';
3
6
  };
4
7
  export type Secret<T> = T & {
5
- readonly __pii__: 'secret';
8
+ readonly __classification__: 'secret';
6
9
  };
7
- export type Classification = 'public' | 'private' | 'secret';
10
+ export type Classification = 'public' | 'private' | 'pii' | 'secret';
8
11
  export type AnonymizeStrategy = 'fake:email' | 'fake:name' | 'hash' | 'keep' | null;
9
12
  export interface ColumnClassification {
10
13
  classification: Classification;
11
14
  anonymize_strategy: AnonymizeStrategy;
15
+ description?: string;
12
16
  }
13
17
  export type ClassificationManifest = {
14
18
  version: 1;
@@ -11,6 +11,7 @@ export class PikkuError extends Error {
11
11
  constructor(message = 'An error occurred') {
12
12
  super(message);
13
13
  Object.setPrototypeOf(this, new.target.prototype);
14
+ this.name = new.target.name;
14
15
  }
15
16
  }
16
17
  /**
@@ -1,4 +1,4 @@
1
- import type { CoreServices, CoreUserSession, CorePikkuMiddleware, PikkuWiringTypes, PikkuWire, MiddlewareMetadata, PermissionMetadata, CoreSingletonServices, CreateWireServices } from '../types/core.types.js';
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, createFunctionSessionWireProps, } from '../services/user-session-service.js';
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 if (funcMeta.sessionless === false) {
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
  };
@@ -23,6 +23,7 @@ export const handleHTTPError = (e, http, traceId, logger, logWarningsForStatusCo
23
23
  // Set status and response body
24
24
  http?.response?.status(errorResponse.status);
25
25
  http?.response?.json({
26
+ name: e instanceof Error ? e.name : undefined,
26
27
  message: e instanceof Error && e.message && e.message !== 'An error occurred'
27
28
  ? e.message
28
29
  : errorResponse.message,
package/dist/index.d.ts CHANGED
@@ -47,7 +47,8 @@ export type { PikkuPackageState } from './types/state.types.js';
47
47
  export { runMiddleware, addTagMiddleware, addTagMiddleware as addMiddleware, addGlobalMiddleware, } from './middleware-runner.js';
48
48
  export { addTagPermission, addTagPermission as addPermission, addGlobalPermission, checkAuthPermissions, } from './permissions.js';
49
49
  export { isSerializable, stopSingletonServices } from './utils.js';
50
- export { getSingletonServices, getCreateWireServices } from './pikku-state.js';
50
+ export { getSingletonServices, getCreateWireServices, setSingletonServices, } from './pikku-state.js';
51
+ export { clearPikkuRuntimeState } from './test-utils.js';
51
52
  export { type ScheduledTaskInfo, type ScheduledTaskSummary, } from './services/scheduler-service.js';
52
53
  export { SchedulerService } from './services/scheduler-service.js';
53
- export type { Private, Secret, Classification, AnonymizeStrategy, ColumnClassification, ClassificationManifest, } from './data-classification.js';
54
+ export type { Private, Pii, Secret, Classification, AnonymizeStrategy, ColumnClassification, ClassificationManifest, } from './data-classification.js';
package/dist/index.js CHANGED
@@ -17,5 +17,6 @@ export { wireAddon } from './wirings/rpc/wire-addon.js';
17
17
  export { runMiddleware, addTagMiddleware, addTagMiddleware as addMiddleware, addGlobalMiddleware, } from './middleware-runner.js';
18
18
  export { addTagPermission, addTagPermission as addPermission, addGlobalPermission, checkAuthPermissions, } from './permissions.js';
19
19
  export { isSerializable, stopSingletonServices } from './utils.js';
20
- export { getSingletonServices, getCreateWireServices } from './pikku-state.js';
20
+ export { getSingletonServices, getCreateWireServices, setSingletonServices, } from './pikku-state.js';
21
+ export { clearPikkuRuntimeState } from './test-utils.js';
21
22
  export { SchedulerService } from './services/scheduler-service.js';
@@ -24,6 +24,7 @@ export declare const pikkuState: <Type extends keyof PikkuPackageState, Content
24
24
  export declare const initializePikkuState: (packageName: string) => void;
25
25
  export declare const resetPikkuState: () => void;
26
26
  export declare const getSingletonServices: () => CoreSingletonServices;
27
+ export declare const setSingletonServices: (services: CoreSingletonServices) => void;
27
28
  export declare const getCreateWireServices: () => CreateWireServices | undefined;
28
29
  /**
29
30
  * Register service factories for an addon package.
@@ -157,6 +157,9 @@ export const getSingletonServices = () => {
157
157
  throw new Error('Singleton services not initialized');
158
158
  return services;
159
159
  };
160
+ export const setSingletonServices = (services) => {
161
+ pikkuState(null, 'package', 'singletonServices', services);
162
+ };
160
163
  export const getCreateWireServices = () => {
161
164
  return pikkuState(null, 'package', 'factories')?.createWireServices;
162
165
  };
@@ -11,4 +11,5 @@ export declare class GopassSecretService implements SecretService {
11
11
  hasSecret(key: string): Promise<boolean>;
12
12
  setSecret(key: string, value: unknown): Promise<void>;
13
13
  deleteSecret(key: string): Promise<void>;
14
+ getSecrets(keys: string[]): Promise<Record<string, unknown>>;
14
15
  }
@@ -64,4 +64,13 @@ export class GopassSecretService {
64
64
  // Ignore errors if secret doesn't exist
65
65
  }
66
66
  }
67
+ async getSecrets(keys) {
68
+ const results = await Promise.allSettled(keys.map((k) => this.getSecret(k)));
69
+ const out = {};
70
+ keys.forEach((key, i) => {
71
+ if (results[i].status === 'fulfilled')
72
+ out[key] = results[i].value;
73
+ });
74
+ return out;
75
+ }
67
76
  }
@@ -13,4 +13,5 @@ export declare class LocalSecretService implements SecretService {
13
13
  setSecret(key: string, value: unknown): Promise<void>;
14
14
  hasSecret(key: string): Promise<boolean>;
15
15
  deleteSecret(key: string): Promise<void>;
16
+ getSecrets(keys: string[]): Promise<Record<string, unknown>>;
16
17
  }
@@ -41,4 +41,13 @@ export class LocalSecretService {
41
41
  async deleteSecret(key) {
42
42
  this.localSecrets.delete(key);
43
43
  }
44
+ async getSecrets(keys) {
45
+ const results = await Promise.allSettled(keys.map((k) => this.getSecret(k)));
46
+ const out = {};
47
+ keys.forEach((key, i) => {
48
+ if (results[i].status === 'fulfilled')
49
+ out[key] = results[i].value;
50
+ });
51
+ return out;
52
+ }
44
53
  }
@@ -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
- this.readProjectFile(`${baseDir}/theme.json`),
291
- this.readProjectFile(`${baseDir}/locales/${locale}.json`),
292
- this.readProjectFile(`${baseDir}/partials/layout.html`),
293
- this.readProjectFile(`${baseDir}/partials/footer.html`),
294
- this.readProjectFile(`${baseDir}/templates/${templateName}.html`),
295
- this.readProjectFile(`${baseDir}/templates/${templateName}.subject.txt`),
296
- this.readProjectFile(`${baseDir}/templates/${templateName}.text.txt`),
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) : {},
@@ -12,4 +12,5 @@ export declare class ScopedSecretService implements SecretService {
12
12
  hasSecret(key: string): Promise<boolean>;
13
13
  setSecret(_key: string, _value: unknown): Promise<void>;
14
14
  deleteSecret(_key: string): Promise<void>;
15
+ getSecrets(keys: string[]): Promise<Record<string, unknown>>;
15
16
  }
@@ -28,4 +28,8 @@ export class ScopedSecretService {
28
28
  async deleteSecret(_key) {
29
29
  throw new Error('deleteSecret is not allowed in scoped secret service');
30
30
  }
31
+ async getSecrets(keys) {
32
+ const allowed = keys.filter((k) => this.allowedKeys.has(k));
33
+ return this.secrets.getSecrets(allowed);
34
+ }
31
35
  }
@@ -28,4 +28,11 @@ export interface SecretService {
28
28
  * @returns A promise that resolves when the secret is deleted.
29
29
  */
30
30
  deleteSecret(key: string): Promise<void>;
31
+ /**
32
+ * Retrieves multiple secrets in a single batch operation.
33
+ * Returns a map of key → value for successfully fetched secrets; missing
34
+ * keys are omitted rather than throwing.
35
+ * @param keys - The keys of the secrets to retrieve.
36
+ */
37
+ getSecrets(keys: string[]): Promise<Record<string, unknown>>;
31
38
  }
@@ -24,6 +24,7 @@ export declare class TypedSecretService<TMap = Record<string, unknown>> implemen
24
24
  hasSecret(key: string): Promise<boolean>;
25
25
  setSecret<K extends string>(key: K, value: K extends keyof TMap ? TMap[K] : unknown): Promise<void>;
26
26
  deleteSecret(key: string): Promise<void>;
27
+ getSecrets(keys: string[]): Promise<Record<string, unknown>>;
27
28
  getAllStatus(): Promise<CredentialStatus[]>;
28
29
  getMissing(): Promise<CredentialStatus[]>;
29
30
  }
@@ -17,6 +17,9 @@ export class TypedSecretService {
17
17
  async deleteSecret(key) {
18
18
  return this.secrets.deleteSecret(key);
19
19
  }
20
+ async getSecrets(keys) {
21
+ return this.secrets.getSecrets(keys);
22
+ }
20
23
  async getAllStatus() {
21
24
  const results = [];
22
25
  for (const [secretId, meta] of Object.entries(this.credentialsMeta)) {
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Clears all runtime caches between test scenarios without touching registered
3
+ * routes, functions, or middleware groups (which are set at module load time
4
+ * and must persist for the whole test run).
5
+ *
6
+ * Call this alongside your DB snapshot restore in your Before hook:
7
+ *
8
+ * ```typescript
9
+ * Before(async function() {
10
+ * await restoreSnapshot()
11
+ * clearPikkuRuntimeState()
12
+ * setSingletonServices(bundle.services)
13
+ * })
14
+ * ```
15
+ *
16
+ * Only works when NODE_ENV === 'test'. Logs an error and returns otherwise.
17
+ */
18
+ export declare const clearPikkuRuntimeState: () => void;
@@ -0,0 +1,33 @@
1
+ import { getAllPackageStates } from './pikku-state.js';
2
+ import { clearMiddlewareCache } from './middleware-runner.js';
3
+ import { clearPermissionsCache } from './permissions.js';
4
+ import { clearChannelMiddlewareCache } from './wirings/channel/channel-middleware-runner.js';
5
+ /**
6
+ * Clears all runtime caches between test scenarios without touching registered
7
+ * routes, functions, or middleware groups (which are set at module load time
8
+ * and must persist for the whole test run).
9
+ *
10
+ * Call this alongside your DB snapshot restore in your Before hook:
11
+ *
12
+ * ```typescript
13
+ * Before(async function() {
14
+ * await restoreSnapshot()
15
+ * clearPikkuRuntimeState()
16
+ * setSingletonServices(bundle.services)
17
+ * })
18
+ * ```
19
+ *
20
+ * Only works when NODE_ENV === 'test'. Logs an error and returns otherwise.
21
+ */
22
+ export const clearPikkuRuntimeState = () => {
23
+ if (process.env.NODE_ENV !== 'test') {
24
+ console.error('[pikku] clearPikkuRuntimeState() must only be called in test environments. Ignoring.');
25
+ return;
26
+ }
27
+ clearMiddlewareCache();
28
+ clearPermissionsCache();
29
+ clearChannelMiddlewareCache();
30
+ for (const packageState of getAllPackageStates().values()) {
31
+ packageState.package.singletonServices = null;
32
+ }
33
+ };
@@ -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 { runPikkuFuncDirectly } from '../../function/function-runner.js';
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 runPikkuFuncDirectly(meta.pikkuFuncId, services, wire, data);
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
- registerOnOpen(callback: () => void): void;
11
- open(): void;
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
- for (const cb of this.closeCallbacks) {
39
- cb();
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,
@@ -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 (this.isInline(runId) || !getSingletonServices()?.queueService) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/core",
3
- "version": "0.12.26",
3
+ "version": "0.12.28",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "module": "dist/index.js",
@@ -1,12 +1,19 @@
1
- export type Private<T> = T & { readonly __pii__: 'private' }
2
- export type Secret<T> = T & { readonly __pii__: 'secret' }
1
+ export type Private<T> = T & { readonly __classification__: 'private' }
2
+ export type Pii<T> = T & { readonly __classification__: 'pii' }
3
+ export type Secret<T> = T & { readonly __classification__: 'secret' }
3
4
 
4
- export type Classification = 'public' | 'private' | 'secret'
5
- export type AnonymizeStrategy = 'fake:email' | 'fake:name' | 'hash' | 'keep' | null
5
+ export type Classification = 'public' | 'private' | 'pii' | 'secret'
6
+ export type AnonymizeStrategy =
7
+ | 'fake:email'
8
+ | 'fake:name'
9
+ | 'hash'
10
+ | 'keep'
11
+ | null
6
12
 
7
13
  export interface ColumnClassification {
8
14
  classification: Classification
9
15
  anonymize_strategy: AnonymizeStrategy
16
+ description?: string
10
17
  }
11
18
 
12
19
  export type ClassificationManifest = {
@@ -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', {
@@ -12,6 +12,7 @@ export class PikkuError extends Error {
12
12
  constructor(message: string = 'An error occurred') {
13
13
  super(message)
14
14
  Object.setPrototypeOf(this, new.target.prototype)
15
+ this.name = new.target.name
15
16
  }
16
17
  }
17
18
 
@@ -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 use backward-compatible auth checks when sessionless metadata is absent', async () => {
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', {