@pikku/core 0.12.19 → 0.12.20

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 CHANGED
@@ -1,3 +1,69 @@
1
+ ## 0.12.20
2
+
3
+ ### Patch Changes
4
+
5
+ - 18acebe: feat(core): scope bare `rpc.invoke()` calls to the caller's addon package
6
+
7
+ Addon functions calling `rpc.invoke('foo')` (bare, no colon) previously only
8
+ resolved against root RPC meta and threw `RPCNotFoundError` for the addon's
9
+ own functions, forcing authors to prefix every call with their consumer-facing
10
+ namespace (`'cli:foo'`) — which couples the addon to its caller's `wireAddon({ name })`.
11
+
12
+ `ContextAwareRPCService` now carries an optional `packageName` passed through
13
+ from `runPikkuFunc` via `getContextRPCService`. For bare names from inside an
14
+ addon, resolution first checks the caller's package function meta, then falls
15
+ back to root. Applies to both `rpc.invoke()` and `rpc.rpcWithWire()`. Explicit
16
+ namespaced calls (`'stripe:createCharge'`) and root-namespace calls are unchanged.
17
+
18
+ - 66d1b4f: feat(content)!: bucket-aware ContentService with typed object args
19
+
20
+ BREAKING CHANGE: All `ContentService` methods now take object args with a
21
+ required `bucket` field. The interface is generic over `TBucket extends string`
22
+ so callers can constrain bucket names to a typed union.
23
+
24
+ Migration:
25
+
26
+ ```ts
27
+ // Before
28
+ content.getUploadURL(fileKey, contentType)
29
+ content.signContentKey(key, expiresAt)
30
+ content.writeFile(assetKey, stream)
31
+ content.readFile(assetKey)
32
+ content.deleteFile(assetKey)
33
+
34
+ // After
35
+ content.getUploadURL({ bucket, fileKey, contentType })
36
+ content.signContentKey({ bucket, contentKey, dateLessThan: expiresAt })
37
+ content.writeFile({ bucket, key, stream })
38
+ content.readFile({ bucket, key })
39
+ content.deleteFile({ bucket, key })
40
+ ```
41
+
42
+ - New exported types: `SignContentKeyArgs`, `SignURLArgs`, `GetUploadURLArgs`,
43
+ `UploadURLResult`, `BucketKeyArgs`, `WriteFileArgs`, `CopyFileArgs`.
44
+ - `LocalContent` stores objects under `<base>/<bucket>/<key>`.
45
+ - `S3Content` and `B2Content` treat the logical bucket as a key prefix within
46
+ the configured underlying storage bucket.
47
+ - `workflow-screenshot` addon takes `bucket?` / `key?` input; default bucket
48
+ resolved from `PIKKU_WORKFLOW_SCREENSHOT_BUCKET` variable, no hardcoded
49
+ fallback.
50
+
51
+ - 3e35b99: feat(core): scope bare workflow names to the caller's addon package
52
+
53
+ Parallel to the RPC scoping fix for addon functions. Addon code calling
54
+ `services.workflowService.runToCompletion('myWorkflow', ...)` (bare name,
55
+ no colon) previously missed workflows registered under the addon's package
56
+ scope and threw `WorkflowNotFoundError`, forcing authors to hard-code
57
+ the consumer-facing namespace (`'cli:myWorkflow'`) — which couples the
58
+ addon to its caller's `wireAddon({ name })`.
59
+
60
+ `getOrCreatePackageSingletonServices` in the function-runner now wraps
61
+ the package's `workflowService` with a Proxy that auto-prefixes bare
62
+ workflow names on `startWorkflow` / `runToCompletion` with the addon's
63
+ consumer-defined namespace (looked up from `pikkuState(null, 'addons',
64
+ 'packages')`). Explicit `'ns:name'` calls and root-namespace workflows
65
+ are unchanged.
66
+
1
67
  ## 0.12.19
2
68
 
3
69
  ### Patch Changes
@@ -37,6 +37,51 @@ async function resolveSession(wire, singletonServices, sessionService) {
37
37
  * @param parentServices - The parent/caller's singleton services (used as base)
38
38
  * @returns The package's singleton services
39
39
  */
40
+ /**
41
+ * Find the consumer-defined namespace (from wireAddon) for a given addon package.
42
+ * Returns null if the package isn't registered as an addon.
43
+ */
44
+ const findAddonNamespaceForPackage = (packageName) => {
45
+ const addons = pikkuState(null, 'addons', 'packages');
46
+ if (!addons)
47
+ return null;
48
+ for (const [namespace, cfg] of addons.entries()) {
49
+ if (cfg?.package === packageName)
50
+ return namespace;
51
+ }
52
+ return null;
53
+ };
54
+ /**
55
+ * Wrap a workflow service so that bare workflow names passed from inside an
56
+ * addon function are auto-prefixed with the addon's consumer-facing namespace.
57
+ * Without this, `runToCompletion('myWorkflow')` from inside an addon misses
58
+ * the workflow registered under the addon's package scope and throws
59
+ * WorkflowNotFoundError — forcing addons to hardcode their consumer-defined
60
+ * namespace, which couples the addon to its caller.
61
+ *
62
+ * Explicit `'ns:name'` and bare names that already exist in root meta are
63
+ * unaffected; only bare names that would otherwise miss resolution get
64
+ * prefixed.
65
+ */
66
+ const wrapWorkflowServiceForPackage = (service, packageName) => {
67
+ return new Proxy(service, {
68
+ get(target, prop, receiver) {
69
+ if (prop === 'startWorkflow' || prop === 'runToCompletion') {
70
+ const original = Reflect.get(target, prop, receiver);
71
+ return function (name, ...rest) {
72
+ if (typeof name === 'string' && !name.includes(':')) {
73
+ const namespace = findAddonNamespaceForPackage(packageName);
74
+ if (namespace) {
75
+ name = `${namespace}:${name}`;
76
+ }
77
+ }
78
+ return original.call(this, name, ...rest);
79
+ };
80
+ }
81
+ return Reflect.get(target, prop, receiver);
82
+ },
83
+ });
84
+ };
40
85
  const getOrCreatePackageSingletonServices = async (packageName, parentServices) => {
41
86
  // Check if we already have cached singleton services for this package
42
87
  const cachedServices = pikkuState(packageName, 'package', 'singletonServices');
@@ -56,6 +101,12 @@ const getOrCreatePackageSingletonServices = async (packageName, parentServices)
56
101
  }
57
102
  // Create singleton services for the package, passing parent services as existing
58
103
  const packageServices = await factories.createSingletonServices(config, parentServices);
104
+ // Wrap workflowService so that bare names used inside the addon's functions
105
+ // resolve to workflows registered under the addon's package scope.
106
+ if (packageServices.workflowService &&
107
+ typeof packageServices.workflowService === 'object') {
108
+ packageServices.workflowService = wrapWorkflowServiceForPackage(packageServices.workflowService, packageName);
109
+ }
59
110
  // Cache the services
60
111
  pikkuState(packageName, 'package', 'singletonServices', packageServices);
61
112
  return packageServices;
@@ -223,11 +274,10 @@ export const runPikkuFunc = async (wireType, wireId, funcName, { singletonServic
223
274
  const services = wireServices && Object.keys(wireServices).length > 0
224
275
  ? { ...resolvedSingletonServices, ...wireServices }
225
276
  : resolvedSingletonServices;
277
+ const callerPackageName = packageName;
226
278
  Object.defineProperty(resolvedWire, 'rpc', {
227
279
  get() {
228
- const rpc = rpcService.getContextRPCService(services, resolvedWire, {
229
- sessionService,
230
- });
280
+ const rpc = rpcService.getContextRPCService(services, resolvedWire, { sessionService }, 0, callerPackageName);
231
281
  Object.defineProperty(resolvedWire, 'rpc', {
232
282
  value: rpc,
233
283
  writable: true,
package/dist/index.d.ts CHANGED
@@ -24,7 +24,7 @@ export type { QueueService } from './wirings/queue/queue.types.js';
24
24
  export type { JWTService } from './services/jwt-service.js';
25
25
  export type { SecretService } from './services/secret-service.js';
26
26
  export type { VariablesService } from './services/variables-service.js';
27
- export type { ContentService } from './services/content-service.js';
27
+ export type { ContentService, SignContentKeyArgs, SignURLArgs, GetUploadURLArgs, UploadURLResult, BucketKeyArgs, WriteFileArgs, CopyFileArgs, } from './services/content-service.js';
28
28
  export type { DeploymentService } from './services/deployment-service.js';
29
29
  export type { WorkflowService } from './services/workflow-service.js';
30
30
  export type { GatewayService } from './services/gateway-service.js';
@@ -1,60 +1,89 @@
1
- export interface ContentService {
1
+ /**
2
+ * Arguments for signing a content key into a time-limited URL.
3
+ */
4
+ export interface SignContentKeyArgs<TBucket extends string = string> {
5
+ bucket: TBucket;
6
+ contentKey: string;
7
+ dateLessThan: Date;
8
+ dateGreaterThan?: Date;
9
+ }
10
+ /**
11
+ * Arguments for signing an arbitrary URL.
12
+ */
13
+ export interface SignURLArgs {
14
+ url: string;
15
+ dateLessThan: Date;
16
+ dateGreaterThan?: Date;
17
+ }
18
+ /**
19
+ * Arguments for minting a presigned upload URL.
20
+ */
21
+ export interface GetUploadURLArgs<TBucket extends string = string> {
22
+ bucket: TBucket;
23
+ fileKey: string;
24
+ contentType: string;
25
+ size?: number;
26
+ }
27
+ /**
28
+ * Result of minting a presigned upload URL.
29
+ */
30
+ export interface UploadURLResult {
31
+ uploadUrl: string;
32
+ assetKey: string;
33
+ uploadHeaders?: Record<string, string>;
34
+ uploadMethod?: 'PUT' | 'POST';
35
+ }
36
+ /**
37
+ * Arguments for an operation that targets a single object by key.
38
+ */
39
+ export interface BucketKeyArgs<TBucket extends string = string> {
40
+ bucket: TBucket;
41
+ key: string;
42
+ }
43
+ /**
44
+ * Arguments for writing a stream to storage.
45
+ */
46
+ export interface WriteFileArgs<TBucket extends string = string> extends BucketKeyArgs<TBucket> {
47
+ stream: ReadableStream | NodeJS.ReadableStream;
48
+ }
49
+ /**
50
+ * Arguments for copying a local file into storage.
51
+ */
52
+ export interface CopyFileArgs<TBucket extends string = string> extends BucketKeyArgs<TBucket> {
53
+ fromAbsolutePath: string;
54
+ }
55
+ export interface ContentService<TBucket extends string = string> {
2
56
  /**
3
57
  * Signs a content key to generate a secure, time-limited access URL.
4
- * @param contentKey - The key representing the content object.
5
- * @param dateLessThan - The expiration time for the signed URL.
6
- * @param dateGreaterThan - (Optional) Start time before which access is denied.
7
58
  */
8
- signContentKey(contentKey: string, dateLessThan: Date, dateGreaterThan?: Date): Promise<string>;
59
+ signContentKey(args: SignContentKeyArgs<TBucket>): Promise<string>;
9
60
  /**
10
61
  * Signs an arbitrary URL to generate a secure, time-limited access URL.
11
- * @param url - The full URL that needs signing.
12
- * @param dateLessThan - The expiration time for the signed URL.
13
- * @param dateGreaterThan - (Optional) Start time before which access is denied.
14
62
  */
15
- signURL(url: string, dateLessThan: Date, dateGreaterThan?: Date): Promise<string>;
63
+ signURL(args: SignURLArgs): Promise<string>;
16
64
  /**
17
65
  * Generates a signed URL for uploading a file directly to storage.
18
- * @param fileKey - The desired key/location of the uploaded file.
19
- * @param contentType - The MIME type of the file.
20
- * @returns A signed upload URL and the finalized asset key.
66
+ * Bucket policy (size limits, MIME allowlist) is enforced by the implementation.
21
67
  */
22
- getUploadURL(fileKey: string, contentType: string): Promise<{
23
- uploadUrl: string;
24
- assetKey: string;
25
- uploadHeaders?: Record<string, string>;
26
- uploadMethod?: 'PUT' | 'POST';
27
- }>;
68
+ getUploadURL(args: GetUploadURLArgs<TBucket>): Promise<UploadURLResult>;
28
69
  /**
29
70
  * Deletes a file from the storage backend.
30
- * @param fileName - The name or key of the file to delete.
31
- * @returns A boolean indicating success.
32
71
  */
33
- deleteFile(fileName: string): Promise<boolean>;
72
+ deleteFile(args: BucketKeyArgs<TBucket>): Promise<boolean>;
34
73
  /**
35
- * Uploads a file stream to storage under a specified asset key.
36
- * @param assetKey - The key where the file will be saved.
37
- * @param stream - A readable stream of the file contents.
38
- * @returns A boolean indicating success.
74
+ * Uploads a file stream to storage under the specified bucket + key.
39
75
  */
40
- writeFile(assetKey: string, stream: ReadableStream | NodeJS.ReadableStream): Promise<boolean>;
76
+ writeFile(args: WriteFileArgs<TBucket>): Promise<boolean>;
41
77
  /**
42
- * Copies a file from a local absolute path into storage under a new asset key.
43
- * @param assetKey - The destination key.
44
- * @param fromAbsolutePath - The local absolute file path.
45
- * @returns A boolean indicating success.
78
+ * Copies a file from a local absolute path into storage.
46
79
  */
47
- copyFile(assetKey: string, fromAbsolutePath: string): Promise<boolean>;
80
+ copyFile(args: CopyFileArgs<TBucket>): Promise<boolean>;
48
81
  /**
49
82
  * Reads a file from storage as a readable stream.
50
- * @param assetKey - The key of the file to read.
51
- * @returns A readable file stream.
52
83
  */
53
- readFile(assetKey: string): Promise<ReadableStream | NodeJS.ReadableStream>;
84
+ readFile(args: BucketKeyArgs<TBucket>): Promise<ReadableStream | NodeJS.ReadableStream>;
54
85
  /**
55
86
  * Reads an entire file from storage into a Buffer.
56
- * @param assetKey - The key of the file to read.
57
- * @returns The file contents as a Buffer.
58
87
  */
59
- readFileAsBuffer(assetKey: string): Promise<Buffer>;
88
+ readFileAsBuffer(args: BucketKeyArgs<TBucket>): Promise<Buffer>;
60
89
  }
@@ -16,7 +16,7 @@ export { InMemoryQueueService } from './in-memory-queue-service.js';
16
16
  export { InMemoryTriggerService } from './in-memory-trigger-service.js';
17
17
  export { InMemoryAIRunStateService } from './in-memory-ai-run-state-service.js';
18
18
  export { LocalGatewayService } from './local-gateway-service.js';
19
- export type { ContentService } from './content-service.js';
19
+ export type { ContentService, SignContentKeyArgs, SignURLArgs, GetUploadURLArgs, UploadURLResult, BucketKeyArgs, WriteFileArgs, CopyFileArgs, } from './content-service.js';
20
20
  export type { JWTService } from './jwt-service.js';
21
21
  export type { Logger } from './logger.js';
22
22
  export type { SecretService } from './secret-service.js';
@@ -1,4 +1,4 @@
1
- import type { ContentService, JWTService, Logger } from '@pikku/core/services';
1
+ import type { BucketKeyArgs, ContentService, CopyFileArgs, GetUploadURLArgs, JWTService, Logger, SignContentKeyArgs, SignURLArgs, UploadURLResult, WriteFileArgs } from '@pikku/core/services';
2
2
  export interface LocalContentConfig {
3
3
  localFileUploadPath: string;
4
4
  uploadUrlPrefix: string;
@@ -12,18 +12,16 @@ export declare class LocalContent implements ContentService {
12
12
  private jwt?;
13
13
  constructor(config: LocalContentConfig, logger: Logger, jwt?: JWTService | undefined);
14
14
  private safePath;
15
+ private joinKey;
15
16
  init(): Promise<void>;
16
17
  private signParams;
17
- signURL(url: string, dateLessThan: Date, dateGreaterThan?: Date): Promise<string>;
18
- signContentKey(assetKey: string, dateLessThan: Date, dateGreaterThan?: Date): Promise<string>;
19
- getUploadURL(assetKey: string): Promise<{
20
- uploadUrl: string;
21
- assetKey: string;
22
- }>;
23
- writeFile(assetKey: string, stream: ReadableStream | NodeJS.ReadableStream): Promise<boolean>;
24
- copyFile(assetKey: string, fromAbsolutePath: string): Promise<boolean>;
25
- readFile(assetKey: string): Promise<ReadableStream | NodeJS.ReadableStream>;
26
- readFileAsBuffer(assetKey: string): Promise<Buffer>;
27
- deleteFile(assetKey: string): Promise<boolean>;
18
+ signURL(args: SignURLArgs): Promise<string>;
19
+ signContentKey(args: SignContentKeyArgs): Promise<string>;
20
+ getUploadURL(args: GetUploadURLArgs): Promise<UploadURLResult>;
21
+ writeFile(args: WriteFileArgs): Promise<boolean>;
22
+ copyFile(args: CopyFileArgs): Promise<boolean>;
23
+ readFile(args: BucketKeyArgs): Promise<ReadableStream | NodeJS.ReadableStream>;
24
+ readFileAsBuffer(args: BucketKeyArgs): Promise<Buffer>;
25
+ deleteFile(args: BucketKeyArgs): Promise<boolean>;
28
26
  private createDirectoryForFile;
29
27
  }
@@ -11,14 +11,18 @@ export class LocalContent {
11
11
  this.logger = logger;
12
12
  this.jwt = jwt;
13
13
  }
14
- safePath(assetKey) {
14
+ safePath(bucket, key) {
15
15
  const base = resolve(this.config.localFileUploadPath);
16
- const target = resolve(base, normalize(assetKey));
16
+ const scoped = resolve(base, normalize(bucket));
17
+ const target = resolve(scoped, normalize(key));
17
18
  if (!target.startsWith(base + '/') && target !== base) {
18
19
  throw new Error('Invalid asset key');
19
20
  }
20
21
  return target;
21
22
  }
23
+ joinKey(bucket, key) {
24
+ return `${bucket}/${key}`;
25
+ }
22
26
  async init() { }
23
27
  async signParams(dateLessThan, dateGreaterThan) {
24
28
  const signedAt = Date.now();
@@ -37,81 +41,86 @@ export class LocalContent {
37
41
  }
38
42
  return params.toString();
39
43
  }
40
- async signURL(url, dateLessThan, dateGreaterThan) {
41
- const params = await this.signParams(dateLessThan, dateGreaterThan);
42
- return `${url}?${params}`;
44
+ async signURL(args) {
45
+ const params = await this.signParams(args.dateLessThan, args.dateGreaterThan);
46
+ return `${args.url}?${params}`;
43
47
  }
44
- async signContentKey(assetKey, dateLessThan, dateGreaterThan) {
48
+ async signContentKey(args) {
49
+ const fullKey = this.joinKey(args.bucket, args.contentKey);
45
50
  const base = this.config.server
46
- ? `${this.config.server}${this.config.assetUrlPrefix}/${assetKey}`
47
- : `${this.config.assetUrlPrefix}/${assetKey}`;
48
- return this.signURL(base, dateLessThan, dateGreaterThan);
51
+ ? `${this.config.server}${this.config.assetUrlPrefix}/${fullKey}`
52
+ : `${this.config.assetUrlPrefix}/${fullKey}`;
53
+ return this.signURL({
54
+ url: base,
55
+ dateLessThan: args.dateLessThan,
56
+ dateGreaterThan: args.dateGreaterThan,
57
+ });
49
58
  }
50
- async getUploadURL(assetKey) {
51
- this.logger.debug(`Going to upload with key: ${assetKey}`);
59
+ async getUploadURL(args) {
60
+ const fullKey = this.joinKey(args.bucket, args.fileKey);
61
+ this.logger.debug(`Going to upload with key: ${fullKey}`);
52
62
  return {
53
- uploadUrl: `${this.config.uploadUrlPrefix}/${assetKey}`,
54
- assetKey,
63
+ uploadUrl: `${this.config.uploadUrlPrefix}/${fullKey}`,
64
+ assetKey: fullKey,
55
65
  };
56
66
  }
57
- async writeFile(assetKey, stream) {
58
- this.logger.debug(`Writing file: ${assetKey}`);
59
- const path = this.safePath(assetKey);
67
+ async writeFile(args) {
68
+ this.logger.debug(`Writing file: ${args.bucket}/${args.key}`);
69
+ const path = this.safePath(args.bucket, args.key);
60
70
  try {
61
71
  await this.createDirectoryForFile(path);
62
72
  const fileStream = createWriteStream(path);
63
- // Use pipeline to properly manage stream piping and errors
64
- await pipeline(stream, fileStream);
73
+ await pipeline(args.stream, fileStream);
65
74
  return true;
66
75
  }
67
76
  catch (e) {
68
77
  console.error(e);
69
- this.logger.error(`Error writing content ${assetKey}`, e);
78
+ this.logger.error(`Error writing content ${args.bucket}/${args.key}`, e);
70
79
  return false;
71
80
  }
72
81
  }
73
- async copyFile(assetKey, fromAbsolutePath) {
74
- this.logger.debug(`Writing file: ${assetKey}`);
82
+ async copyFile(args) {
83
+ this.logger.debug(`Writing file: ${args.bucket}/${args.key}`);
75
84
  try {
76
- const path = this.safePath(assetKey);
85
+ const path = this.safePath(args.bucket, args.key);
77
86
  await this.createDirectoryForFile(path);
78
- await promises.copyFile(fromAbsolutePath, path);
87
+ await promises.copyFile(args.fromAbsolutePath, path);
88
+ return true;
79
89
  }
80
90
  catch (e) {
81
91
  console.error(e);
82
- this.logger.error(`Error inserting content ${assetKey}`, e);
92
+ this.logger.error(`Error inserting content ${args.bucket}/${args.key}`, e);
83
93
  }
84
94
  return false;
85
95
  }
86
- async readFile(assetKey) {
87
- this.logger.debug(`Getting key: ${assetKey}`);
88
- const filePath = this.safePath(assetKey);
96
+ async readFile(args) {
97
+ this.logger.debug(`Getting key: ${args.bucket}/${args.key}`);
98
+ const filePath = this.safePath(args.bucket, args.key);
89
99
  try {
90
100
  const stream = createReadStream(filePath);
91
- // Handle early stream errors (like file not found, permission denied, etc.)
92
101
  stream.on('error', (err) => {
93
- this.logger.error(`Error getting content ${assetKey}`, err);
102
+ this.logger.error(`Error getting content ${args.bucket}/${args.key}`, err);
94
103
  });
95
104
  return stream;
96
105
  }
97
106
  catch (e) {
98
- this.logger.error(`Error setting up stream for ${assetKey}`, e);
107
+ this.logger.error(`Error setting up stream for ${args.bucket}/${args.key}`, e);
99
108
  throw e;
100
109
  }
101
110
  }
102
- async readFileAsBuffer(assetKey) {
103
- const filePath = this.safePath(assetKey);
104
- this.logger.debug(`Reading file as buffer: ${assetKey}`);
111
+ async readFileAsBuffer(args) {
112
+ const filePath = this.safePath(args.bucket, args.key);
113
+ this.logger.debug(`Reading file as buffer: ${args.bucket}/${args.key}`);
105
114
  return readFile(filePath);
106
115
  }
107
- async deleteFile(assetKey) {
108
- this.logger.debug(`deleting key: ${assetKey}`);
116
+ async deleteFile(args) {
117
+ this.logger.debug(`deleting key: ${args.bucket}/${args.key}`);
109
118
  try {
110
- await promises.unlink(this.safePath(assetKey));
119
+ await promises.unlink(this.safePath(args.bucket, args.key));
111
120
  return true;
112
121
  }
113
122
  catch (e) {
114
- this.logger.error(`Error deleting content ${assetKey}`, e);
123
+ this.logger.error(`Error deleting content ${args.bucket}/${args.key}`, e);
115
124
  }
116
125
  return false;
117
126
  }
@@ -18,10 +18,11 @@ export declare class ContextAwareRPCService {
18
18
  private services;
19
19
  private wire;
20
20
  private options;
21
+ private packageName;
21
22
  constructor(services: CoreServices, wire: PikkuWire, options: {
22
23
  requiresAuth?: boolean;
23
24
  sessionService?: SessionService<CoreUserSession>;
24
- });
25
+ }, packageName?: string | null);
25
26
  rpcExposed(funcName: string, data: any): Promise<any>;
26
27
  rpc<In = any, Out = any>(funcName: string, data: In): Promise<Out>;
27
28
  /**
@@ -96,7 +97,7 @@ export declare class PikkuRPCService<Services extends CoreServices, TypedRPC = P
96
97
  getContextRPCService(services: Services, wire: PikkuWire, requiresAuthOrOptions?: boolean | {
97
98
  requiresAuth?: boolean;
98
99
  sessionService?: SessionService<CoreUserSession>;
99
- } | undefined, depth?: number): TypedRPC;
100
+ } | undefined, depth?: number, packageName?: string | null): TypedRPC;
100
101
  }
101
102
  export declare const rpcService: PikkuRPCService<import("../../types/core.types.js").CoreSingletonServices<{
102
103
  logLevel?: import("../../services/logger.js").LogLevel;
@@ -39,7 +39,22 @@ export const resolveNamespace = (namespacedFunction) => {
39
39
  addonConfig: pkgConfig,
40
40
  };
41
41
  };
42
- const getPikkuFunctionName = (rpcName) => {
42
+ /**
43
+ * Resolve a bare (non-namespaced) RPC name to its pikkuFuncId, preferring
44
+ * the caller's addon package when provided. Returns the function name plus
45
+ * the package scope that resolved it (null = root), so callers can thread
46
+ * the scope into runPikkuFunc without a second lookup.
47
+ */
48
+ const resolvePikkuFunction = (rpcName, packageName = null) => {
49
+ // Addon-scoped calls: try the caller's package function meta first.
50
+ // (RPC meta only lives in root; addon functions are registered under their package.)
51
+ if (packageName) {
52
+ const pkgFunctions = pikkuState(packageName, 'function', 'meta');
53
+ const pkgMeta = pkgFunctions?.[rpcName];
54
+ if (pkgMeta) {
55
+ return { pikkuFuncId: pkgMeta.pikkuFuncId || rpcName, packageName };
56
+ }
57
+ }
43
58
  const rpc = pikkuState(null, 'rpc', 'meta');
44
59
  let rpcMeta = rpc[rpcName];
45
60
  if (!rpcMeta) {
@@ -51,17 +66,19 @@ const getPikkuFunctionName = (rpcName) => {
51
66
  if (!rpcMeta) {
52
67
  throw new RPCNotFoundError(rpcName);
53
68
  }
54
- return rpcMeta;
69
+ return { pikkuFuncId: rpcMeta, packageName: null };
55
70
  };
56
71
  // Context-aware RPC client for use within services
57
72
  export class ContextAwareRPCService {
58
73
  services;
59
74
  wire;
60
75
  options;
61
- constructor(services, wire, options) {
76
+ packageName;
77
+ constructor(services, wire, options, packageName = null) {
62
78
  this.services = services;
63
79
  this.wire = wire;
64
80
  this.options = options;
81
+ this.packageName = packageName;
65
82
  }
66
83
  async rpcExposed(funcName, data) {
67
84
  let functionMeta;
@@ -105,13 +122,18 @@ export class ContextAwareRPCService {
105
122
  // Not an addon — fall through to local lookup
106
123
  }
107
124
  }
108
- // Try local function, then fall back to deployment service (remote)
125
+ // Bare name: resolve via caller's package scope first (if any), then root.
126
+ // Note: intra-addon bare calls do NOT re-apply the addon's external
127
+ // addonConfig.auth/tags — those gates are only applied on the external
128
+ // 'namespace:func' boundary via invokeAddonFunction.
109
129
  try {
110
- return await runPikkuFunc('rpc', funcName, getPikkuFunctionName(funcName), {
130
+ const resolved = resolvePikkuFunction(funcName, this.packageName);
131
+ return await runPikkuFunc('rpc', funcName, resolved.pikkuFuncId, {
111
132
  auth: this.options.requiresAuth,
112
133
  singletonServices: this.services,
113
134
  data: () => data,
114
135
  wire: updatedWire,
136
+ packageName: resolved.packageName,
115
137
  });
116
138
  }
117
139
  catch (e) {
@@ -181,11 +203,13 @@ export class ContextAwareRPCService {
181
203
  return this.invokeAddonFunction(rpcName, data, mergedWire);
182
204
  }
183
205
  try {
184
- return await runPikkuFunc('rpc', rpcName, getPikkuFunctionName(rpcName), {
206
+ const resolved = resolvePikkuFunction(rpcName, this.packageName);
207
+ return await runPikkuFunc('rpc', rpcName, resolved.pikkuFuncId, {
185
208
  auth: this.options.requiresAuth,
186
209
  singletonServices: this.services,
187
210
  data: () => data,
188
211
  wire: mergedWire,
212
+ packageName: resolved.packageName,
189
213
  });
190
214
  }
191
215
  catch (e) {
@@ -269,12 +293,12 @@ export class ContextAwareRPCService {
269
293
  // RPC Service class for the global interface
270
294
  export class PikkuRPCService {
271
295
  // Convenience function for initializing
272
- getContextRPCService(services, wire, requiresAuthOrOptions, depth = 0) {
296
+ getContextRPCService(services, wire, requiresAuthOrOptions, depth = 0, packageName = null) {
273
297
  const options = typeof requiresAuthOrOptions === 'object' &&
274
298
  requiresAuthOrOptions !== null
275
299
  ? requiresAuthOrOptions
276
300
  : { requiresAuth: requiresAuthOrOptions };
277
- const serviceRPC = new ContextAwareRPCService(services, wire, options);
301
+ const serviceRPC = new ContextAwareRPCService(services, wire, options, packageName);
278
302
  return {
279
303
  depth,
280
304
  global: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/core",
3
- "version": "0.12.19",
3
+ "version": "0.12.20",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "module": "dist/index.js",