@pikku/inspector 0.12.42 → 0.12.44

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 (126) hide show
  1. package/CHANGELOG.md +266 -0
  2. package/dist/add/add-addon-bans.js +1 -0
  3. package/dist/add/add-ai-agent.d.ts +3 -1
  4. package/dist/add/add-ai-agent.js +82 -0
  5. package/dist/add/add-credential.js +5 -1
  6. package/dist/add/add-functions.js +10 -9
  7. package/dist/add/add-gateway.js +3 -0
  8. package/dist/add/add-http-route.js +2 -2
  9. package/dist/add/add-keyed-wiring.js +2 -0
  10. package/dist/add/add-mcp-prompt.js +0 -1
  11. package/dist/add/add-mcp-resource.js +0 -1
  12. package/dist/add/add-permission.d.ts +1 -1
  13. package/dist/add/add-permission.js +3 -177
  14. package/dist/add/add-queue-worker.js +3 -0
  15. package/dist/add/add-schedule.js +3 -0
  16. package/dist/add/add-scope.d.ts +2 -0
  17. package/dist/add/add-scope.js +146 -0
  18. package/dist/add/add-trigger.js +3 -0
  19. package/dist/add/add-wire-remote-addon.d.ts +10 -0
  20. package/dist/add/add-wire-remote-addon.js +66 -0
  21. package/dist/add/add-workflow-graph.js +32 -3
  22. package/dist/add/add-workflow.js +9 -4
  23. package/dist/error-codes.d.ts +3 -0
  24. package/dist/error-codes.js +4 -0
  25. package/dist/inspector.js +11 -5
  26. package/dist/types.d.ts +19 -13
  27. package/dist/utils/custom-types-generator.js +2 -1
  28. package/dist/utils/ensure-function-metadata.d.ts +15 -0
  29. package/dist/utils/ensure-function-metadata.js +24 -0
  30. package/dist/utils/filter-inspector-state.js +9 -0
  31. package/dist/utils/get-property-value.d.ts +7 -2
  32. package/dist/utils/get-property-value.js +48 -1
  33. package/dist/utils/load-addon-functions-meta.js +81 -6
  34. package/dist/utils/permissions.d.ts +1 -21
  35. package/dist/utils/permissions.js +17 -108
  36. package/dist/utils/post-process.d.ts +30 -1
  37. package/dist/utils/post-process.js +150 -72
  38. package/dist/utils/schema-generator.js +19 -4
  39. package/dist/utils/serialize-inspector-state.d.ts +13 -8
  40. package/dist/utils/serialize-inspector-state.js +12 -6
  41. package/dist/utils/serialize-permissions-groups-meta.d.ts +0 -2
  42. package/dist/utils/serialize-permissions-groups-meta.js +0 -26
  43. package/dist/utils/workflow/derive-workflow-plan.js +9 -3
  44. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +373 -91
  45. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +274 -100
  46. package/dist/utils/workflow/dsl/patterns.d.ts +4 -0
  47. package/dist/utils/workflow/dsl/patterns.js +11 -0
  48. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +76 -5
  49. package/dist/utils/workflow/graph/finalize-workflows.d.ts +7 -0
  50. package/dist/utils/workflow/graph/finalize-workflows.js +11 -2
  51. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +2 -0
  52. package/dist/utils/workflow/graph/serialize-workflow-graph.js +4 -0
  53. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +8 -4
  54. package/dist/visit.js +4 -0
  55. package/package.json +3 -3
  56. package/run-tests.sh +1 -1
  57. package/src/add/add-addon-bans.ts +1 -0
  58. package/src/add/add-ai-agent.test.ts +87 -0
  59. package/src/add/add-ai-agent.ts +122 -0
  60. package/src/add/add-credential.ts +8 -0
  61. package/src/add/add-functions.ts +10 -12
  62. package/src/add/add-gateway.ts +11 -0
  63. package/src/add/add-http-route.ts +2 -8
  64. package/src/add/add-keyed-wiring.ts +2 -0
  65. package/src/add/add-mcp-prompt.ts +0 -1
  66. package/src/add/add-mcp-resource.ts +0 -1
  67. package/src/add/add-permission.ts +4 -242
  68. package/src/add/add-queue-worker.ts +11 -0
  69. package/src/add/add-schedule.ts +11 -0
  70. package/src/add/add-scope.test.ts +346 -0
  71. package/src/add/add-scope.ts +225 -0
  72. package/src/add/add-trigger.ts +11 -0
  73. package/src/add/add-wire-remote-addon.ts +77 -0
  74. package/src/add/add-workflow-edge-cases.test.ts +201 -0
  75. package/src/add/add-workflow-fanout.test.ts +60 -0
  76. package/src/add/add-workflow-graph-input.test.ts +94 -0
  77. package/src/add/add-workflow-graph-notes.test.ts +74 -0
  78. package/src/add/add-workflow-graph.ts +35 -3
  79. package/src/add/add-workflow-on-error.test.ts +118 -0
  80. package/src/add/add-workflow-return-switch.test.ts +121 -0
  81. package/src/add/add-workflow-silent-drops.test.ts +163 -0
  82. package/src/add/add-workflow-sleep-expression.test.ts +97 -0
  83. package/src/add/add-workflow.ts +9 -4
  84. package/src/add/config-value-metadata.test.ts +169 -0
  85. package/src/add/inline-wiring-function.test.ts +104 -0
  86. package/src/add/validate-workflow-graph-addons.test.ts +102 -0
  87. package/src/error-codes.ts +5 -0
  88. package/src/inspector.ts +14 -4
  89. package/src/types.ts +16 -17
  90. package/src/utils/custom-types-generator.test.ts +26 -1
  91. package/src/utils/custom-types-generator.ts +2 -1
  92. package/src/utils/ensure-function-metadata.ts +42 -0
  93. package/src/utils/filter-inspector-state.test.ts +0 -2
  94. package/src/utils/filter-inspector-state.ts +9 -0
  95. package/src/utils/get-property-value.test.ts +141 -0
  96. package/src/utils/get-property-value.ts +62 -1
  97. package/src/utils/load-addon-functions-meta.test.ts +157 -0
  98. package/src/utils/load-addon-functions-meta.ts +94 -6
  99. package/src/utils/load-addon-scopes.test.ts +138 -0
  100. package/src/utils/permissions.test.ts +7 -232
  101. package/src/utils/permissions.ts +20 -160
  102. package/src/utils/post-process.test.ts +269 -4
  103. package/src/utils/post-process.ts +216 -95
  104. package/src/utils/schema-generator.test.ts +164 -0
  105. package/src/utils/schema-generator.ts +26 -3
  106. package/src/utils/serialize-inspector-state.ts +22 -25
  107. package/src/utils/serialize-permissions-groups-meta.ts +0 -28
  108. package/src/utils/workflow/derive-workflow-plan.test.ts +41 -3
  109. package/src/utils/workflow/derive-workflow-plan.ts +11 -4
  110. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +480 -119
  111. package/src/utils/workflow/dsl/deserialize-fanout-body.test.ts +71 -0
  112. package/src/utils/workflow/dsl/deserialize-fanout-fidelity.test.ts +99 -0
  113. package/src/utils/workflow/dsl/deserialize-graph-next.test.ts +161 -0
  114. package/src/utils/workflow/dsl/deserialize-roundtrip-bindings.test.ts +121 -0
  115. package/src/utils/workflow/dsl/deserialize-roundtrip-values.test.ts +116 -0
  116. package/src/utils/workflow/dsl/deserialize-roundtrip.test.ts +145 -0
  117. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +311 -105
  118. package/src/utils/workflow/dsl/patterns.ts +18 -0
  119. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +82 -10
  120. package/src/utils/workflow/graph/fanout-node-identity.test.ts +105 -0
  121. package/src/utils/workflow/graph/finalize-workflows.test.ts +50 -0
  122. package/src/utils/workflow/graph/finalize-workflows.ts +13 -2
  123. package/src/utils/workflow/graph/serialize-workflow-graph.ts +6 -0
  124. package/src/utils/workflow/graph/workflow-graph.types.ts +11 -2
  125. package/src/visit.ts +4 -0
  126. package/tsconfig.tsbuildinfo +1 -1
package/dist/inspector.js CHANGED
@@ -6,7 +6,7 @@ import { TypesMap } from './types-map.js';
6
6
  import { getFilesAndMethods } from './utils/get-files-and-methods.js';
7
7
  import { findCommonAncestor } from './utils/find-root-dir.js';
8
8
  import { createNestedProjectFilter } from './utils/nested-project-filter.js';
9
- import { aggregateRequiredServices, stampAuthHandlerServices, validateAgentModels, validateSecretOverrides, validateVariableOverrides, validateCredentialOverrides, computeResolvedIOTypes, computeMiddlewareGroupsMeta, computePermissionsGroupsMeta, computeRequiredSchemas, computeDiagnostics, validateSchemaWiringSeparation, } from './utils/post-process.js';
9
+ import { aggregateRequiredServices, stampAuthHandlerServices, validateAgentModels, validateSecretOverrides, validateVariableOverrides, validateCredentialOverrides, validateRemoteAddonDependencies, validateRemoteAddonAuth, validateScopeReferences, computeResolvedIOTypes, computeMiddlewareGroupsMeta, computePermissionsGroupsMeta, computeRequiredSchemas, computeDiagnostics, validateSchemaWiringSeparation, validateWorkflowGraphAddons, } from './utils/post-process.js';
10
10
  import { generateOpenAPISpec } from './utils/serialize-openapi-json.js';
11
11
  import { pikkuState } from '@pikku/core/internal';
12
12
  import { resolveLatestVersions } from './utils/resolve-versions.js';
@@ -59,7 +59,6 @@ export function getInitialInspectorState(rootDir) {
59
59
  },
60
60
  files: new Set(),
61
61
  routeMiddleware: new Map(),
62
- routePermissions: new Map(),
63
62
  },
64
63
  channels: {
65
64
  files: new Set(),
@@ -94,6 +93,8 @@ export function getInitialInspectorState(rootDir) {
94
93
  internalFiles: new Map(),
95
94
  exposedMeta: {},
96
95
  exposedFiles: new Map(),
96
+ remoteMeta: {},
97
+ remoteFiles: new Map(),
97
98
  invokedFunctions: new Set(),
98
99
  invokedFunctionsByFile: new Map(),
99
100
  usedAddons: new Set(),
@@ -135,6 +136,10 @@ export function getInitialInspectorState(rootDir) {
135
136
  definitions: [],
136
137
  files: new Set(),
137
138
  },
139
+ scopes: {
140
+ definitions: [],
141
+ files: new Set(),
142
+ },
138
143
  variables: {
139
144
  definitions: [],
140
145
  files: new Set(),
@@ -160,7 +165,6 @@ export function getInitialInspectorState(rootDir) {
160
165
  permissions: {
161
166
  definitions: {},
162
167
  instances: {},
163
- tagPermissions: new Map(),
164
168
  },
165
169
  serviceAggregation: {
166
170
  requiredServices: new Set(),
@@ -184,8 +188,6 @@ export function getInitialInspectorState(rootDir) {
184
188
  },
185
189
  permissionsGroupsMeta: {
186
190
  definitions: {},
187
- httpGroups: {},
188
- tagGroups: {},
189
191
  },
190
192
  requiredSchemas: new Set(),
191
193
  openAPISpec: null,
@@ -307,6 +309,7 @@ export const inspect = async (logger, routeFiles, options = {}) => {
307
309
  computePermissionsGroupsMeta(state);
308
310
  computeDiagnostics(state);
309
311
  validateSchemaWiringSeparation(logger, state);
312
+ validateWorkflowGraphAddons(logger, state);
310
313
  if (options.openAPI) {
311
314
  state.openAPISpec = await generateOpenAPISpec(logger, state.functions.meta, state.http.meta, state.schemas, options.openAPI.additionalInfo, pikkuState(null, 'misc', 'errors'));
312
315
  }
@@ -314,6 +317,9 @@ export const inspect = async (logger, routeFiles, options = {}) => {
314
317
  validateSecretOverrides(logger, state);
315
318
  validateVariableOverrides(logger, state);
316
319
  validateCredentialOverrides(logger, state);
320
+ validateRemoteAddonDependencies(logger, state);
321
+ validateRemoteAddonAuth(logger, state);
322
+ validateScopeReferences(logger, state);
317
323
  }
318
324
  state.program = program;
319
325
  return state;
package/dist/types.d.ts CHANGED
@@ -13,6 +13,7 @@ import type { CLICommandMeta } from '@pikku/core/cli';
13
13
  import type { NodesMeta } from '@pikku/core/node';
14
14
  import type { SecretDefinitions } from '@pikku/core/secret';
15
15
  import type { CredentialDefinitions } from '@pikku/core/credential';
16
+ import type { ScopeDefinitions } from '@pikku/core/scope';
16
17
  import type { VariableDefinitions } from '@pikku/core/variable';
17
18
  import type { TypesMap } from './types-map.js';
18
19
  import type { FunctionsMeta, FunctionServicesMeta, FunctionWiresMeta, JSONValue } from '@pikku/core';
@@ -39,21 +40,11 @@ export interface MiddlewareGroupMeta {
39
40
  instanceIds: string[];
40
41
  isFactory: boolean;
41
42
  }
42
- export interface PermissionGroupMeta {
43
- exportName: string | null;
44
- sourceFile: string;
45
- position: number;
46
- services: FunctionServicesMeta;
47
- count: number;
48
- instanceIds: string[];
49
- isFactory: boolean;
50
- }
51
43
  export interface InspectorHTTPState {
52
44
  metaInputTypes: MetaInputTypes;
53
45
  meta: HTTPWiringsMeta;
54
46
  files: Set<string>;
55
47
  routeMiddleware: Map<string, MiddlewareGroupMeta>;
56
- routePermissions: Map<string, PermissionGroupMeta>;
57
48
  }
58
49
  /**
59
50
  * Schema vendor types for Standard Schema compliant validators
@@ -184,7 +175,6 @@ export interface InspectorPermissionInstance {
184
175
  export interface InspectorPermissionState {
185
176
  definitions: Record<string, InspectorPermissionDefinition>;
186
177
  instances: Record<string, InspectorPermissionInstance>;
187
- tagPermissions: Map<string, PermissionGroupMeta>;
188
178
  }
189
179
  export type InspectorFilters = {
190
180
  names?: string[];
@@ -418,6 +408,12 @@ export interface InspectorState {
418
408
  path: string;
419
409
  exportedName: string;
420
410
  }>;
411
+ /** Functions marked `remote: true` — the surface a wireRemoteAddon consumer imports */
412
+ remoteMeta: Record<string, string>;
413
+ remoteFiles: Map<string, {
414
+ path: string;
415
+ exportedName: string;
416
+ }>;
421
417
  invokedFunctions: Set<string>;
422
418
  invokedFunctionsByFile: Map<string, Set<string>>;
423
419
  usedAddons: Set<string>;
@@ -425,6 +421,14 @@ export interface InspectorState {
425
421
  package: string;
426
422
  rpcEndpoint?: string;
427
423
  mcp?: boolean;
424
+ /** True when declared via `wireRemoteAddon` — import the addon's `.remote.gen` map, not `.internal.gen` */
425
+ remote?: boolean;
426
+ /** wireRemoteAddon: whether an `auth` binding was supplied (vs a public surface) */
427
+ hasAuth?: boolean;
428
+ /** wireRemoteAddon: statically-known `auth.credentialId`, for verify */
429
+ authCredentialId?: string;
430
+ /** wireRemoteAddon: statically-known `auth.secretId`, for verify */
431
+ authSecretId?: string;
428
432
  secretOverrides?: Record<string, string>;
429
433
  variableOverrides?: Record<string, string>;
430
434
  credentialOverrides?: Record<string, string>;
@@ -480,6 +484,10 @@ export interface InspectorState {
480
484
  definitions: CredentialDefinitions;
481
485
  files: Set<string>;
482
486
  };
487
+ scopes: {
488
+ definitions: ScopeDefinitions;
489
+ files: Set<string>;
490
+ };
483
491
  variables: {
484
492
  definitions: VariableDefinitions;
485
493
  files: Set<string>;
@@ -518,8 +526,6 @@ export interface InspectorState {
518
526
  };
519
527
  permissionsGroupsMeta: {
520
528
  definitions: Record<string, InspectorPermissionDefinition>;
521
- httpGroups: Record<string, PermissionGroupMeta>;
522
- tagGroups: Record<string, PermissionGroupMeta>;
523
529
  };
524
530
  requiredSchemas: Set<string>;
525
531
  openAPISpec: Record<string, any> | null;
@@ -5,7 +5,8 @@
5
5
  * ts-json-schema-generator can discover inline/custom types from typesMap.
6
6
  */
7
7
  export function sanitizeTypeName(name) {
8
- return name.replace(/[^a-zA-Z0-9_$]/g, '_');
8
+ const sanitized = name.replace(/[^a-zA-Z0-9_$]/g, '_');
9
+ return /^[0-9]/.test(sanitized) ? `_${sanitized}` : sanitized;
9
10
  }
10
11
  const CLASSIFICATION_WRAPPERS = new Set(['Private', 'Pii', 'Secret']);
11
12
  function findMatchingAngleBracket(type, startIndex) {
@@ -7,3 +7,18 @@ import type { InspectorState } from '../types.js';
7
7
  * extracts tags/middleware/permissions from the pikkuFunc() config.
8
8
  */
9
9
  export declare function ensureFunctionMetadata(state: InspectorState, pikkuFuncId: string, fallbackName?: string, funcInitializer?: ts.Node, checker?: ts.TypeChecker, isHelper?: boolean): void;
10
+ /**
11
+ * Register metadata for a `func:` inlined into a wiring (queue/scheduler/
12
+ * trigger/gateway/…).
13
+ *
14
+ * An inline function has no exported name, so `addFunctions` skips it (it bails
15
+ * on `__temp_` ids) and only the wiring's context-based id exists. Without this
16
+ * the wiring points at a function that was never registered and the worker
17
+ * resolves to nothing at runtime ("Missing generated metadata for ...").
18
+ *
19
+ * The stub also carries no session info, so the runtime would treat it as
20
+ * session-required and every invocation would fail with "Authentication
21
+ * required". Derive `sessionless` from the helper the function was built with —
22
+ * the same rule `addFunctions` applies to named ones (`!== 'pikkuFunc'`).
23
+ */
24
+ export declare function ensureInlineWiringFunction(state: InspectorState, pikkuFuncId: string, fallbackName: string | undefined, funcInitializer: ts.Node, checker: ts.TypeChecker, isHelper?: boolean): void;
@@ -207,6 +207,30 @@ export function ensureFunctionMetadata(state, pikkuFuncId, fallbackName, funcIni
207
207
  }
208
208
  }
209
209
  }
210
+ /**
211
+ * Register metadata for a `func:` inlined into a wiring (queue/scheduler/
212
+ * trigger/gateway/…).
213
+ *
214
+ * An inline function has no exported name, so `addFunctions` skips it (it bails
215
+ * on `__temp_` ids) and only the wiring's context-based id exists. Without this
216
+ * the wiring points at a function that was never registered and the worker
217
+ * resolves to nothing at runtime ("Missing generated metadata for ...").
218
+ *
219
+ * The stub also carries no session info, so the runtime would treat it as
220
+ * session-required and every invocation would fail with "Authentication
221
+ * required". Derive `sessionless` from the helper the function was built with —
222
+ * the same rule `addFunctions` applies to named ones (`!== 'pikkuFunc'`).
223
+ */
224
+ export function ensureInlineWiringFunction(state, pikkuFuncId, fallbackName, funcInitializer, checker, isHelper) {
225
+ ensureFunctionMetadata(state, pikkuFuncId, fallbackName, funcInitializer, checker, isHelper);
226
+ const meta = state.functions.meta[pikkuFuncId];
227
+ if (meta &&
228
+ meta.sessionless === undefined &&
229
+ ts.isCallExpression(funcInitializer) &&
230
+ ts.isIdentifier(funcInitializer.expression)) {
231
+ meta.sessionless = funcInitializer.expression.text !== 'pikkuFunc';
232
+ }
233
+ }
210
234
  function populateTypesLookup(state, pikkuFuncId, funcInitializer, checker) {
211
235
  const typeArgs = funcInitializer.typeArguments;
212
236
  if (typeArgs && typeArgs.length >= 1) {
@@ -334,6 +334,8 @@ export function filterInspectorState(state, filters, logger) {
334
334
  internalFiles: new Map(state.rpc.internalFiles),
335
335
  exposedMeta: { ...state.rpc.exposedMeta },
336
336
  exposedFiles: new Map(state.rpc.exposedFiles),
337
+ remoteMeta: { ...state.rpc.remoteMeta },
338
+ remoteFiles: new Map(state.rpc.remoteFiles),
337
339
  invokedFunctions: new Set(state.rpc.invokedFunctions),
338
340
  },
339
341
  cli: {
@@ -746,6 +748,13 @@ export function filterInspectorState(state, filters, logger) {
746
748
  filteredState.rpc.exposedFiles.delete(key);
747
749
  }
748
750
  }
751
+ for (const key of Object.keys(filteredState.rpc.remoteMeta)) {
752
+ const targetFuncId = filteredState.rpc.remoteMeta[key];
753
+ if (!survivingFuncIds.has(targetFuncId) && !survivingFuncIds.has(key)) {
754
+ delete filteredState.rpc.remoteMeta[key];
755
+ filteredState.rpc.remoteFiles.delete(key);
756
+ }
757
+ }
749
758
  // Prune invokedFunctions to match surviving functions
750
759
  for (const funcId of filteredState.rpc.invokedFunctions) {
751
760
  if (!survivingFuncIds.has(funcId)) {
@@ -1,9 +1,14 @@
1
1
  import * as ts from 'typescript';
2
2
  import { ErrorCode } from '../error-codes.js';
3
+ export declare const getArrayPropertyValue: (obj: ts.ObjectLiteralExpression, propertyName: string) => string[] | null;
3
4
  /**
4
- * Extracts an array of strings from an object property.
5
+ * Extracts a string->string record from an object property.
6
+ *
7
+ * `getPropertyValue` falls through to `getText()` for an object literal, which
8
+ * returns the raw source text rather than the record — so callers that need the
9
+ * entries (e.g. oauth2 `additionalParams`) must use this instead.
5
10
  */
6
- export declare const getArrayPropertyValue: (obj: ts.ObjectLiteralExpression, propertyName: string) => string[] | null;
11
+ export declare const getRecordPropertyValue: (obj: ts.ObjectLiteralExpression, propertyName: string) => Record<string, string> | null;
7
12
  /**
8
13
  * Wiring identity fields (`name`, `secretId`, `variableId`, …) are read
9
14
  * STATICALLY from source — a const or variable reference is keyed by its
@@ -4,12 +4,21 @@ import { extractStringLiteral } from './extract-node-value.js';
4
4
  /**
5
5
  * Extracts an array of strings from an object property.
6
6
  */
7
+ /**
8
+ * Unwraps `x as const` / `x as any` / `x satisfies T` down to the underlying
9
+ * expression. Without this a cast makes the value invisible to the inspector,
10
+ * so it is silently dropped from meta rather than validated — and `as const` on
11
+ * an array is idiomatic enough that this is easy to hit by accident.
12
+ */
13
+ const unwrapAs = (node) => ts.isAsExpression(node) || ts.isSatisfiesExpression(node)
14
+ ? unwrapAs(node.expression)
15
+ : node;
7
16
  export const getArrayPropertyValue = (obj, propertyName) => {
8
17
  const property = obj.properties.find((p) => ts.isPropertyAssignment(p) &&
9
18
  ts.isIdentifier(p.name) &&
10
19
  p.name.text === propertyName);
11
20
  if (property && ts.isPropertyAssignment(property)) {
12
- const initializer = property.initializer;
21
+ const initializer = unwrapAs(property.initializer);
13
22
  if (ts.isArrayLiteralExpression(initializer)) {
14
23
  return initializer.elements
15
24
  .filter(ts.isStringLiteral)
@@ -18,6 +27,44 @@ export const getArrayPropertyValue = (obj, propertyName) => {
18
27
  }
19
28
  return null;
20
29
  };
30
+ /**
31
+ * Extracts a string->string record from an object property.
32
+ *
33
+ * `getPropertyValue` falls through to `getText()` for an object literal, which
34
+ * returns the raw source text rather than the record — so callers that need the
35
+ * entries (e.g. oauth2 `additionalParams`) must use this instead.
36
+ */
37
+ export const getRecordPropertyValue = (obj, propertyName) => {
38
+ const property = obj.properties.find((p) => ts.isPropertyAssignment(p) &&
39
+ ts.isIdentifier(p.name) &&
40
+ p.name.text === propertyName);
41
+ if (!property || !ts.isPropertyAssignment(property)) {
42
+ return null;
43
+ }
44
+ const initializer = property.initializer;
45
+ if (!ts.isObjectLiteralExpression(initializer)) {
46
+ return null;
47
+ }
48
+ const record = {};
49
+ for (const prop of initializer.properties) {
50
+ if (!ts.isPropertyAssignment(prop)) {
51
+ continue;
52
+ }
53
+ const key = ts.isIdentifier(prop.name)
54
+ ? prop.name.text
55
+ : ts.isStringLiteral(prop.name)
56
+ ? prop.name.text
57
+ : null;
58
+ if (key === null) {
59
+ continue;
60
+ }
61
+ if (ts.isStringLiteral(prop.initializer) ||
62
+ ts.isNoSubstitutionTemplateLiteral(prop.initializer)) {
63
+ record[key] = prop.initializer.text;
64
+ }
65
+ }
66
+ return Object.keys(record).length > 0 ? record : null;
67
+ };
21
68
  /**
22
69
  * Wiring identity fields (`name`, `secretId`, `variableId`, …) are read
23
70
  * STATICALLY from source — a const or variable reference is keyed by its
@@ -80,6 +80,11 @@ export async function loadAddonFunctionsMeta(logger, state) {
80
80
  return;
81
81
  const require = createRequire(join(state.rootDir, 'package.json'));
82
82
  for (const [namespace, decl] of wireAddonDeclarations) {
83
+ // Remote addons (wireRemoteAddon) ship as a devDependency: types only.
84
+ // Their functions, secrets, variables, schemas and services live on the
85
+ // HOST that runs them — never load or require any of that here.
86
+ if (decl.remote)
87
+ continue;
83
88
  try {
84
89
  const metaPath = require.resolve(`${decl.package}/.pikku/function/pikku-functions-meta.gen.json`);
85
90
  const raw = await readFile(metaPath, 'utf-8');
@@ -108,32 +113,99 @@ export async function loadAddonFunctionsMeta(logger, state) {
108
113
  const secretsRaw = await readFile(secretsMetaPath, 'utf-8');
109
114
  const secretsMeta = JSON.parse(secretsRaw);
110
115
  for (const [key, def] of Object.entries(secretsMeta)) {
111
- const existing = state.secrets.definitions.find((d) => d.name === key);
116
+ // secretOverrides key on the SECRET ID (the string the addon passes to
117
+ // getSecret — its typed map is keyed by secretId, e.g.
118
+ // `getSecret('MAILGUN_CREDENTIALS')`), NOT the logical meta key, so the
119
+ // runtime aliaser (which also keys on secretId) and this merge agree.
120
+ // The resolved id is the real project secret; the addon's secretId is
121
+ // the default when no override is given. Two instances with different
122
+ // overrides therefore surface two distinct project secrets. `key` is
123
+ // the fallback for older meta that predates the secretId field.
124
+ const secretId = def.secretId ?? key;
125
+ const resolvedSecretId = decl.secretOverrides?.[secretId] ?? secretId;
126
+ const existing = state.secrets.definitions.find((d) => (d.secretId ?? d.name) === resolvedSecretId);
112
127
  if (!existing) {
113
- state.secrets.definitions.push(def);
114
- logger.debug(`Loaded addon secret '${key}' from ${decl.package}`);
128
+ state.secrets.definitions.push({
129
+ ...def,
130
+ secretId: resolvedSecretId,
131
+ });
132
+ logger.debug(`Loaded addon secret '${resolvedSecretId}' from ${decl.package}`);
115
133
  }
116
134
  }
117
135
  }
118
136
  catch {
119
137
  // No secrets meta — that's fine
120
138
  }
139
+ // Load addon scopes meta. Without this an addon's own scopes never reach
140
+ // the host's ScopeId union or its declared set, so the pikku_scopes FK
141
+ // would refuse to grant one.
142
+ try {
143
+ const scopesMetaPath = require.resolve(`${decl.package}/.pikku/scopes/pikku-scopes-meta.gen.json`);
144
+ const scopesRaw = await readFile(scopesMetaPath, 'utf-8');
145
+ const scopesMeta = JSON.parse(scopesRaw);
146
+ for (const [key, def] of Object.entries(scopesMeta)) {
147
+ const existing = state.scopes.definitions.find((d) => d.name === key);
148
+ if (!existing) {
149
+ state.scopes.definitions.push(def);
150
+ logger.debug(`Loaded addon scope '${key}' from ${decl.package}`);
151
+ }
152
+ }
153
+ }
154
+ catch {
155
+ // No scopes meta — that's fine
156
+ }
121
157
  // Load addon variables meta
122
158
  try {
123
159
  const variablesMetaPath = require.resolve(`${decl.package}/.pikku/variables/pikku-variables-meta.gen.json`);
124
160
  const variablesRaw = await readFile(variablesMetaPath, 'utf-8');
125
161
  const variablesMeta = JSON.parse(variablesRaw);
126
162
  for (const [key, def] of Object.entries(variablesMeta)) {
127
- const existing = state.variables.definitions.find((d) => d.name === key);
163
+ // variableOverrides key on the VARIABLE ID (the string the addon reads
164
+ // via its typed map, keyed by variableId), same as secretOverrides key
165
+ // on secretId — so the runtime aliaser and this merge agree. `key` is
166
+ // the fallback for older meta without a variableId field.
167
+ const variableId = def.variableId ?? key;
168
+ const resolvedVariableId = decl.variableOverrides?.[variableId] ?? variableId;
169
+ const existing = state.variables.definitions.find((d) => (d.variableId ?? d.name) === resolvedVariableId);
128
170
  if (!existing) {
129
- state.variables.definitions.push(def);
130
- logger.debug(`Loaded addon variable '${key}' from ${decl.package}`);
171
+ state.variables.definitions.push({
172
+ ...def,
173
+ variableId: resolvedVariableId,
174
+ });
175
+ logger.debug(`Loaded addon variable '${resolvedVariableId}' from ${decl.package}`);
131
176
  }
132
177
  }
133
178
  }
134
179
  catch {
135
180
  // No variables meta — that's fine
136
181
  }
182
+ // Load addon credentials meta. Without this an addon's OAuth2/wire
183
+ // credentials never reach the consuming app's CREDENTIAL_OAUTH2_CONFIGS,
184
+ // so the credential-oauth provider and BetterAuthCredentialService can't
185
+ // resolve them — the addon's Connect flow and status silently no-op.
186
+ try {
187
+ const credentialsMetaPath = require.resolve(`${decl.package}/.pikku/credentials/pikku-credentials-meta.gen.json`);
188
+ const credentialsRaw = await readFile(credentialsMetaPath, 'utf-8');
189
+ const credentialsMeta = JSON.parse(credentialsRaw);
190
+ for (const [key, def] of Object.entries(credentialsMeta)) {
191
+ // Each instance's credentialOverrides remap the addon's logical
192
+ // credential name to a project credential — same mechanic as secrets
193
+ // and variables above. The credential name doubles as the better-auth
194
+ // providerId (accounts keyed by providerId+userId), so two instances
195
+ // with different overrides surface two distinct OAuth providers rather
196
+ // than sharing one account pool. The logical name is the default when
197
+ // no override is given.
198
+ const resolvedName = decl.credentialOverrides?.[key] ?? key;
199
+ const existing = state.credentials.definitions.find((d) => d.name === resolvedName);
200
+ if (!existing) {
201
+ state.credentials.definitions.push({ ...def, name: resolvedName });
202
+ logger.debug(`Loaded addon credential '${resolvedName}' from ${decl.package}`);
203
+ }
204
+ }
205
+ }
206
+ catch {
207
+ // No credentials meta — that's fine
208
+ }
137
209
  // Load addon serverlessIncompatible service names from pikku-addon-meta.gen.json
138
210
  let loadedParentServices = false;
139
211
  try {
@@ -215,6 +287,9 @@ export async function loadAddonSchemas(logger, state) {
215
287
  return;
216
288
  const require = createRequire(join(state.rootDir, 'package.json'));
217
289
  for (const [namespace, decl] of wireAddonDeclarations) {
290
+ // Remote addons carry no local schemas — their funcs run on the host.
291
+ if (decl.remote)
292
+ continue;
218
293
  try {
219
294
  const metaPath = require.resolve(`${decl.package}/.pikku/function/pikku-functions-meta.gen.json`);
220
295
  const schemasDir = join(dirname(metaPath), '..', 'schemas', 'schemas');
@@ -16,28 +16,8 @@ export declare function extractPermissionPikkuNames(node: ts.Expression, checker
16
16
  * Returns the initializer node for the 'permissions' property if it exists
17
17
  */
18
18
  export declare function getPermissionsNode(obj: ts.ObjectLiteralExpression): ts.Expression | undefined;
19
- /**
20
- * Check if a route matches a pattern with wildcards
21
- * Pattern can be exact match or use * as wildcard
22
- * e.g., '/api/*' matches '/api/users', '/api/posts/123', etc.
23
- */
24
- export declare function routeMatchesPattern(route: string, pattern: string): boolean;
25
- /**
26
- * Resolve permissions for an HTTP wiring based on:
27
- * 1. Global HTTP permissions (addHTTPPermission('*', [...]))
28
- * 2. Route-specific HTTP permissions (addHTTPPermission('/pattern', [...]))
29
- * 3. Tag-based permissions (addPermission('tag', [...]))
30
- * 4. Explicit wiring permissions (wireHTTP({ permissions: [...] }))
31
- * Returns undefined if no permissions are found, otherwise returns array with at least one item
32
- */
33
- export declare function resolveHTTPPermissions(state: InspectorState, route: string, tags: string[] | undefined, explicitPermissionsNode: ts.Expression | undefined, checker: ts.TypeChecker): PermissionMetadata[] | undefined;
34
19
  /**
35
20
  * Convenience wrapper: Extract permissions node from object and resolve
36
21
  * Use this in add-* files for cleaner code
37
22
  */
38
- export declare function resolvePermissions(state: InspectorState, obj: ts.ObjectLiteralExpression, tags: string[] | undefined, checker: ts.TypeChecker): PermissionMetadata[] | undefined;
39
- /**
40
- * Convenience wrapper for HTTP: Extract permissions and resolve with HTTP-specific logic
41
- * Use this in add-http-route.ts for cleaner code
42
- */
43
- export declare function resolveHTTPPermissionsFromObject(state: InspectorState, route: string, obj: ts.ObjectLiteralExpression, tags: string[] | undefined, checker: ts.TypeChecker): PermissionMetadata[] | undefined;
23
+ export declare function resolvePermissions(state: InspectorState, obj: ts.ObjectLiteralExpression, _tags: string[] | undefined, checker: ts.TypeChecker): PermissionMetadata[] | undefined;
@@ -57,122 +57,31 @@ export function getPermissionsNode(obj) {
57
57
  return permissionsProp?.initializer;
58
58
  }
59
59
  /**
60
- * Check if a route matches a pattern with wildcards
61
- * Pattern can be exact match or use * as wildcard
62
- * e.g., '/api/*' matches '/api/users', '/api/posts/123', etc.
60
+ * Resolve the explicit permission references declared on a function (or agent).
61
+ * Permissions are function-scoped only: a `permissions: { group: [fn] }` object
62
+ * is flattened to `{ type: 'wire', name }` references used at filter time.
63
+ * Returns undefined if none are declared, otherwise an array with ≥1 item.
63
64
  */
64
- export function routeMatchesPattern(route, pattern) {
65
- if (route === pattern)
66
- return true;
67
- // Convert pattern to regex: replace * with .*
68
- const regexPattern = pattern
69
- .replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars except *
70
- .replace(/\*/g, '.*'); // Replace * with .*
71
- const regex = new RegExp(`^${regexPattern}$`);
72
- return regex.test(route);
73
- }
74
- /**
75
- * Resolve permissions for an HTTP wiring based on:
76
- * 1. Global HTTP permissions (addHTTPPermission('*', [...]))
77
- * 2. Route-specific HTTP permissions (addHTTPPermission('/pattern', [...]))
78
- * 3. Tag-based permissions (addPermission('tag', [...]))
79
- * 4. Explicit wiring permissions (wireHTTP({ permissions: [...] }))
80
- * Returns undefined if no permissions are found, otherwise returns array with at least one item
81
- */
82
- export function resolveHTTPPermissions(state, route, tags, explicitPermissionsNode, checker) {
83
- const resolved = [];
84
- // 1. HTTP route permission groups (includes '*' for global)
85
- for (const [pattern, _groupMeta] of state.http.routePermissions.entries()) {
86
- if (routeMatchesPattern(route, pattern)) {
87
- // Just reference the group by route pattern
88
- resolved.push({
89
- type: 'http',
90
- route: pattern,
91
- });
92
- }
93
- }
94
- // 2. Tag-based permission groups
95
- if (tags && tags.length > 0) {
96
- for (const tag of tags) {
97
- if (state.permissions.tagPermissions.has(tag)) {
98
- // Just reference the group by tag
99
- resolved.push({
100
- type: 'tag',
101
- tag,
102
- });
103
- }
104
- }
105
- }
106
- // 3. Explicit wire permissions (inline is OK here)
107
- if (explicitPermissionsNode) {
108
- const permissionNames = extractPermissionPikkuNames(explicitPermissionsNode, checker, state.rootDir);
109
- for (const name of permissionNames) {
110
- const def = state.permissions.definitions[name];
111
- resolved.push({
112
- type: 'wire',
113
- name,
114
- inline: def?.exportedName === null,
115
- });
116
- }
65
+ function resolveExplicitPermissions(state, explicitPermissionsNode, checker) {
66
+ if (!explicitPermissionsNode) {
67
+ return undefined;
117
68
  }
118
- return resolved.length > 0 ? resolved : undefined;
119
- }
120
- /**
121
- * Resolve tag-based and explicit permissions (common logic for wires and functions)
122
- * 1. Tag-based permissions (addPermission('tag', [...]))
123
- * 2. Explicit permissions (wireHTTP/pikkuFunc({ permissions: [...] }))
124
- */
125
- function resolveTagAndExplicitPermissions(state, tags, explicitPermissionsNode, checker) {
126
69
  const resolved = [];
127
- // 1. Tag-based permission groups
128
- if (tags && tags.length > 0) {
129
- for (const tag of tags) {
130
- if (state.permissions.tagPermissions.has(tag)) {
131
- // Just reference the group by tag
132
- resolved.push({
133
- type: 'tag',
134
- tag,
135
- });
136
- }
137
- }
138
- }
139
- // 2. Explicit permissions (inline is OK here - used directly in wire/function)
140
- if (explicitPermissionsNode) {
141
- const permissionNames = extractPermissionPikkuNames(explicitPermissionsNode, checker, state.rootDir);
142
- for (const name of permissionNames) {
143
- const def = state.permissions.definitions[name];
144
- resolved.push({
145
- type: 'wire',
146
- name,
147
- inline: def?.exportedName === null,
148
- });
149
- }
70
+ const permissionNames = extractPermissionPikkuNames(explicitPermissionsNode, checker, state.rootDir);
71
+ for (const name of permissionNames) {
72
+ const def = state.permissions.definitions[name];
73
+ resolved.push({
74
+ type: 'wire',
75
+ name,
76
+ inline: def?.exportedName === null,
77
+ });
150
78
  }
151
- return resolved;
152
- }
153
- /**
154
- * Resolve permissions for a function based on:
155
- * 1. Tag-based permissions (addPermission('tag', [...]))
156
- * 2. Explicit function permissions (pikkuFunc({ permissions: [...] }))
157
- * Returns undefined if no permissions are found, otherwise returns array with at least one item
158
- */
159
- function resolveFunctionPermissionsInternal(state, tags, explicitPermissionsNode, checker) {
160
- const resolved = resolveTagAndExplicitPermissions(state, tags, explicitPermissionsNode, checker);
161
79
  return resolved.length > 0 ? resolved : undefined;
162
80
  }
163
81
  /**
164
82
  * Convenience wrapper: Extract permissions node from object and resolve
165
83
  * Use this in add-* files for cleaner code
166
84
  */
167
- export function resolvePermissions(state, obj, tags, checker) {
168
- const explicitPermissionsNode = getPermissionsNode(obj);
169
- return resolveFunctionPermissionsInternal(state, tags, explicitPermissionsNode, checker);
170
- }
171
- /**
172
- * Convenience wrapper for HTTP: Extract permissions and resolve with HTTP-specific logic
173
- * Use this in add-http-route.ts for cleaner code
174
- */
175
- export function resolveHTTPPermissionsFromObject(state, route, obj, tags, checker) {
176
- const explicitPermissionsNode = getPermissionsNode(obj);
177
- return resolveHTTPPermissions(state, route, tags, explicitPermissionsNode, checker);
85
+ export function resolvePermissions(state, obj, _tags, checker) {
86
+ return resolveExplicitPermissions(state, getPermissionsNode(obj), checker);
178
87
  }