@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 +66 -0
- package/dist/function/function-runner.js +53 -3
- package/dist/index.d.ts +1 -1
- package/dist/services/content-service.d.ts +66 -37
- package/dist/services/index.d.ts +1 -1
- package/dist/services/local-content.d.ts +10 -12
- package/dist/services/local-content.js +46 -37
- package/dist/wirings/rpc/rpc-runner.d.ts +3 -2
- package/dist/wirings/rpc/rpc-runner.js +32 -8
- package/package.json +1 -1
- package/src/function/function-runner.ts +68 -3
- package/src/index.ts +10 -1
- package/src/services/content-service.ts +79 -51
- package/src/services/index.ts +10 -1
- package/src/services/local-content.ts +68 -52
- package/src/wirings/rpc/rpc-runner.ts +45 -18
- package/tsconfig.tsbuildinfo +1 -1
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
|
-
|
|
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(
|
|
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(
|
|
63
|
+
signURL(args: SignURLArgs): Promise<string>;
|
|
16
64
|
/**
|
|
17
65
|
* Generates a signed URL for uploading a file directly to storage.
|
|
18
|
-
*
|
|
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(
|
|
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(
|
|
72
|
+
deleteFile(args: BucketKeyArgs<TBucket>): Promise<boolean>;
|
|
34
73
|
/**
|
|
35
|
-
* Uploads a file stream to storage under
|
|
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(
|
|
76
|
+
writeFile(args: WriteFileArgs<TBucket>): Promise<boolean>;
|
|
41
77
|
/**
|
|
42
|
-
* Copies a file from a local absolute path into storage
|
|
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(
|
|
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(
|
|
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(
|
|
88
|
+
readFileAsBuffer(args: BucketKeyArgs<TBucket>): Promise<Buffer>;
|
|
60
89
|
}
|
package/dist/services/index.d.ts
CHANGED
|
@@ -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(
|
|
18
|
-
signContentKey(
|
|
19
|
-
getUploadURL(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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(
|
|
14
|
+
safePath(bucket, key) {
|
|
15
15
|
const base = resolve(this.config.localFileUploadPath);
|
|
16
|
-
const
|
|
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(
|
|
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(
|
|
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}/${
|
|
47
|
-
: `${this.config.assetUrlPrefix}/${
|
|
48
|
-
return this.signURL(
|
|
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(
|
|
51
|
-
this.
|
|
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}/${
|
|
54
|
-
assetKey,
|
|
63
|
+
uploadUrl: `${this.config.uploadUrlPrefix}/${fullKey}`,
|
|
64
|
+
assetKey: fullKey,
|
|
55
65
|
};
|
|
56
66
|
}
|
|
57
|
-
async writeFile(
|
|
58
|
-
this.logger.debug(`Writing file: ${
|
|
59
|
-
const path = this.safePath(
|
|
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
|
-
|
|
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 ${
|
|
78
|
+
this.logger.error(`Error writing content ${args.bucket}/${args.key}`, e);
|
|
70
79
|
return false;
|
|
71
80
|
}
|
|
72
81
|
}
|
|
73
|
-
async copyFile(
|
|
74
|
-
this.logger.debug(`Writing file: ${
|
|
82
|
+
async copyFile(args) {
|
|
83
|
+
this.logger.debug(`Writing file: ${args.bucket}/${args.key}`);
|
|
75
84
|
try {
|
|
76
|
-
const path = this.safePath(
|
|
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 ${
|
|
92
|
+
this.logger.error(`Error inserting content ${args.bucket}/${args.key}`, e);
|
|
83
93
|
}
|
|
84
94
|
return false;
|
|
85
95
|
}
|
|
86
|
-
async readFile(
|
|
87
|
-
this.logger.debug(`Getting key: ${
|
|
88
|
-
const filePath = this.safePath(
|
|
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 ${
|
|
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 ${
|
|
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(
|
|
103
|
-
const filePath = this.safePath(
|
|
104
|
-
this.logger.debug(`Reading file as buffer: ${
|
|
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(
|
|
108
|
-
this.logger.debug(`deleting key: ${
|
|
116
|
+
async deleteFile(args) {
|
|
117
|
+
this.logger.debug(`deleting key: ${args.bucket}/${args.key}`);
|
|
109
118
|
try {
|
|
110
|
-
await promises.unlink(this.safePath(
|
|
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 ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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,
|