@apifuse/provider-sdk 2.2.0-beta.56 → 2.2.0-beta.58

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,5 +1,13 @@
1
1
  # @apifuse/provider-sdk Changelog
2
2
 
3
+ ## 2.2.0-beta.58
4
+
5
+ - Release candidate for main commit e137940caa8bc32ce768571a6197651e581c2270.
6
+
7
+ ## 2.2.0-beta.57
8
+
9
+ - Release candidate for main commit 4087dafdeab771eb91691884ce7315b8c399f863.
10
+
3
11
  ## 2.2.0-beta.56
4
12
 
5
13
  - Release candidate for main commit 390167b3f51488be0beba25a1ea5eb425d3bf839.
package/README.md CHANGED
@@ -115,9 +115,10 @@ the bad request path; provider/runtime failures include `code`, `message`, and
115
115
  - **`invalid_request` on `/v1/{operation}`**: confirm the request body includes
116
116
  `requestId` and `input`. Omit `connection` for public/no-auth operations;
117
117
  never send `connection: null`.
118
- - **Credential-backed operations**: declare `credential.keys`, then pass matching
119
- local-only values through `connection.secrets`. Read them in handlers with
120
- `ctx.credential.get("key")` or `ctx.credential.getAccessToken()`.
118
+ - **Credential-backed operations**: declare `credential.keys`, except when
119
+ `auth.mode` is `platform-managed`; that auth declaration implies an unfiltered
120
+ credential capability. Pass local-only values through `connection.secrets` and
121
+ read them with `ctx.credential.get("key")` or `ctx.credential.getAccessToken()`.
121
122
  - **Provider env secrets**: declare `secrets[]`, set values in your shell or
122
123
  `.env`, and read only those names through `ctx.env.get("NAME")`. The SDK
123
124
  enforces presence of `required: true` declarations before handlers and auth
package/SUBMISSION.md CHANGED
@@ -46,7 +46,8 @@ Fix all blockers before submitting:
46
46
  - Missing fixture request or response.
47
47
  - Fixture data does not parse against schemas.
48
48
  - Missing `healthCheck` or `healthCheckUnsupported` on any Operation.
49
- - Credential-backed auth mode without declared credential keys.
49
+ - User-managed credential auth mode without declared credential keys
50
+ (`platform-managed` auth instead implies the credential capability and forbids keys).
50
51
  - High-confidence secret or token material in source, README, package metadata, or fixtures.
51
52
  - SDK-native source blockers: prefixed Provider ids, `vendor/` SDK shims or imports, raw `.describe()` prose instead of `describeKey`, raw global `fetch()` calls, and excessive `as Type` assertions.
52
53
 
@@ -45,6 +45,7 @@ export type OperationDeclarationRepositoryResult = {
45
45
  readonly providerRoot: string;
46
46
  readonly changedFiles: readonly string[];
47
47
  readonly operationCount: number;
48
+ readonly notes: readonly OperationDeclarationNote[];
48
49
  readonly sidecar?: string;
49
50
  readonly localeTodoCount: number;
50
51
  } | {
@@ -53,9 +54,15 @@ export type OperationDeclarationRepositoryResult = {
53
54
  readonly refusals: readonly OperationDeclarationRefusal[];
54
55
  /** Operations that were independently migratable before repository-atomic refusal. */
55
56
  readonly operationCount: number;
57
+ readonly notes: readonly OperationDeclarationNote[];
56
58
  readonly changedFiles: readonly string[];
57
59
  readonly localeTodoCount: number;
58
60
  };
61
+ export type OperationDeclarationNote = {
62
+ readonly code: "runtime_composed_registry";
63
+ readonly path: string;
64
+ readonly initializer: string;
65
+ };
59
66
  /** Run the file transform repository-wide, committing writes only if every file is provable. */
60
67
  export declare function migrateOperationDeclarationRepository(providerRootInput: string, options?: {
61
68
  readonly check?: boolean;
@@ -68,7 +68,8 @@ function migrateOperationDeclarationInternal(sourceText, fileName, options, cont
68
68
  }
69
69
  const constObjects = collectModuleConstObjects(source);
70
70
  const constArrays = collectModuleConstArrays(source);
71
- const discovery = discoverOperationSites(source, fileName, constObjects, options.operationIds, context.operationSites, context.excludedBindings);
71
+ const discovery = discoverOperationSites(source, fileName, constObjects, options.operationIds, context.operationSites, context.excludedBindings, context.runtimeComposedInitializers);
72
+ context.recordDiscoveredSites?.(discovery.discoveredCount);
72
73
  if (discovery.refusals.length > 0) {
73
74
  return { status: "refused", refusals: discovery.refusals };
74
75
  }
@@ -599,14 +600,15 @@ function replaceExampleLocaleMember(member, newName, localeKey, source) {
599
600
  text: `${newName}: ${JSON.stringify(localeKey)}`,
600
601
  };
601
602
  }
602
- function discoverOperationSites(source, fileName, constObjects, operationIds, operationSites, excludedBindings) {
603
+ function discoverOperationSites(source, fileName, constObjects, operationIds, operationSites, excludedBindings, runtimeComposedInitializers) {
603
604
  const sitesByStart = new Map();
605
+ const discoveredStarts = new Set();
604
606
  const localIds = new Map(operationIds ?? []);
605
607
  const localExcludedBindings = new Set(excludedBindings ?? []);
606
608
  const refusals = [];
607
609
  const operationFactories = collectSimpleOperationFactories(source);
608
610
  const factoryCallIds = new Map();
609
- for (const map of collectOperationsMaps(source, constObjects, fileName, refusals)) {
611
+ for (const map of collectOperationsMaps(source, constObjects, fileName, refusals, runtimeComposedInitializers)) {
610
612
  for (const property of map.properties) {
611
613
  if (ts.isSpreadAssignment(property))
612
614
  continue;
@@ -680,12 +682,15 @@ function discoverOperationSites(source, fileName, constObjects, operationIds, op
680
682
  if (ts.isCallExpression(node) && isOperationHelperCall(node)) {
681
683
  const argument = operationArgument(node);
682
684
  const bindingName = enclosingBindingName(node);
685
+ const unwrappedArgument = unwrapExpression(argument);
686
+ if (unwrappedArgument !== undefined && ts.isObjectLiteralExpression(unwrappedArgument)) {
687
+ discoveredStarts.add(unwrappedArgument.getStart(source));
688
+ }
683
689
  if (bindingName !== undefined && localExcludedBindings.has(bindingName))
684
690
  return;
685
691
  const operationKey = (bindingName === undefined ? undefined : localIds.get(bindingName)) ??
686
692
  bindingName ??
687
693
  "<anonymous>";
688
- const unwrappedArgument = unwrapExpression(argument);
689
694
  if (unwrappedArgument === undefined || !ts.isObjectLiteralExpression(unwrappedArgument)) {
690
695
  refusals.push(refusal(fileName, operationKey, "non_literal", "Operation helper argument must be an object literal."));
691
696
  }
@@ -711,8 +716,11 @@ function discoverOperationSites(source, fileName, constObjects, operationIds, op
711
716
  ts.forEachChild(node, visit);
712
717
  };
713
718
  visit(source);
719
+ for (const start of sitesByStart.keys())
720
+ discoveredStarts.add(start);
714
721
  return {
715
722
  sites: [...sitesByStart.values()].sort((left, right) => left.object.getStart(source) - right.object.getStart(source)),
723
+ discoveredCount: discoveredStarts.size,
716
724
  refusals,
717
725
  };
718
726
  }
@@ -741,7 +749,7 @@ function collectSimpleOperationFactories(source) {
741
749
  }
742
750
  return factories;
743
751
  }
744
- function collectOperationsMaps(source, constObjects, fileName, refusals) {
752
+ function collectOperationsMaps(source, constObjects, fileName, refusals, runtimeComposedInitializers) {
745
753
  const maps = new Map();
746
754
  const inspect = (expression, label) => {
747
755
  const unwrapped = unwrapExpression(expression);
@@ -758,6 +766,8 @@ function collectOperationsMaps(source, constObjects, fileName, refusals) {
758
766
  return;
759
767
  }
760
768
  if (ts.isCallExpression(unwrapped)) {
769
+ if (runtimeComposedInitializers?.has(expression.getStart(source)) === true)
770
+ return;
761
771
  refusals.push(refusal(fileName, label, "factory_composed_operations", "The operations map is produced by a factory call and cannot be statically enumerated."));
762
772
  }
763
773
  };
@@ -1274,6 +1284,7 @@ export function migrateOperationDeclarationRepository(providerRootInput, options
1274
1284
  const todos = [];
1275
1285
  const refusals = [...repositoryIndex.refusals];
1276
1286
  let operationCount = 0;
1287
+ let repositoryDiscoveredCount = 0;
1277
1288
  for (const sourcePath of sourceFiles) {
1278
1289
  const relativePath = slash(relative(providerRoot, sourcePath));
1279
1290
  const result = migrateOperationDeclarationInternal(readFileSync(sourcePath, "utf8"), relativePath, {
@@ -1283,6 +1294,10 @@ export function migrateOperationDeclarationRepository(providerRootInput, options
1283
1294
  operationSites: repositoryIndex.operationSites.get(sourcePath),
1284
1295
  excludedBindings: repositoryIndex.excludedBindings.get(sourcePath),
1285
1296
  staticObjectResolver: repositoryIndex.staticObjectResolverFor(sourcePath),
1297
+ runtimeComposedInitializers: repositoryIndex.runtimeComposedInitializers.get(sourcePath),
1298
+ recordDiscoveredSites: (count) => {
1299
+ repositoryDiscoveredCount += count;
1300
+ },
1286
1301
  });
1287
1302
  if (result.status === "refused") {
1288
1303
  refusals.push(...result.refusals);
@@ -1295,7 +1310,18 @@ export function migrateOperationDeclarationRepository(providerRootInput, options
1295
1310
  todos.push(...result.localeTodos);
1296
1311
  }
1297
1312
  }
1298
- if (repositoryIndex.declarations.length > 0 && repositoryIndex.discoveredCount === 0) {
1313
+ const notes = repositoryDiscoveredCount === 0
1314
+ ? []
1315
+ : repositoryIndex.declarations.flatMap((declaration) => declaration.runtimeInitializer === undefined
1316
+ ? []
1317
+ : [
1318
+ {
1319
+ code: "runtime_composed_registry",
1320
+ path: slash(relative(providerRoot, declaration.path)),
1321
+ initializer: declaration.runtimeInitializer,
1322
+ },
1323
+ ]);
1324
+ if (repositoryIndex.declarations.length > 0 && repositoryDiscoveredCount === 0) {
1299
1325
  for (const declaration of repositoryIndex.declarations) {
1300
1326
  refusals.push(refusal(slash(relative(providerRoot, declaration.path)), "<operations>", "no_operations_discovered", `Provider construct ${declaration.construct} declares operations via unresolved initializer ${JSON.stringify(declaration.initializer.getText(declaration.initializer.getSourceFile()))}, but repository-wide discovery found zero operation sites.`));
1301
1327
  }
@@ -1309,6 +1335,7 @@ export function migrateOperationDeclarationRepository(providerRootInput, options
1309
1335
  providerRoot,
1310
1336
  refusals,
1311
1337
  operationCount,
1338
+ notes,
1312
1339
  changedFiles,
1313
1340
  localeTodoCount: todos.length,
1314
1341
  };
@@ -1322,6 +1349,7 @@ export function migrateOperationDeclarationRepository(providerRootInput, options
1322
1349
  providerRoot,
1323
1350
  changedFiles: [],
1324
1351
  operationCount,
1352
+ notes,
1325
1353
  localeTodoCount: 0,
1326
1354
  };
1327
1355
  }
@@ -1338,6 +1366,7 @@ export function migrateOperationDeclarationRepository(providerRootInput, options
1338
1366
  providerRoot,
1339
1367
  changedFiles,
1340
1368
  operationCount,
1369
+ notes,
1341
1370
  sidecar,
1342
1371
  localeTodoCount: todos.length,
1343
1372
  };
@@ -1489,12 +1518,12 @@ function buildRepositoryOperationIndex(sourceFiles, providerRoot) {
1489
1518
  const operationIds = buildOperationIdIndex(sourceFiles);
1490
1519
  const operationSites = new Map();
1491
1520
  const excludedBindings = new Map();
1521
+ const runtimeComposedInitializers = new Map();
1492
1522
  const refusals = [];
1493
1523
  const declarations = [];
1494
1524
  const indexedProviderProperties = new Set();
1495
1525
  const factoryIds = new Map();
1496
1526
  const bindingCandidates = new Map();
1497
- let discoveredCount = 0;
1498
1527
  const relativePath = (path) => slash(relative(providerRoot, path));
1499
1528
  const resolutionRefusal = (path, operationKey, detail) => {
1500
1529
  refusals.push(refusal(relativePath(path), operationKey, "non_literal", detail));
@@ -1749,7 +1778,6 @@ function buildRepositoryOperationIndex(sourceFiles, providerRoot) {
1749
1778
  }
1750
1779
  sites.set(start, operationId);
1751
1780
  operationSites.set(path, sites);
1752
- discoveredCount += 1;
1753
1781
  };
1754
1782
  const classifyEntry = (entry) => {
1755
1783
  const resolved = resolveLocatedExpression(entry.value, new Set());
@@ -1793,7 +1821,6 @@ function buildRepositoryOperationIndex(sourceFiles, providerRoot) {
1793
1821
  };
1794
1822
  factory.ids.add(entry.operationId);
1795
1823
  factoryIds.set(key, factory);
1796
- discoveredCount += 1;
1797
1824
  return;
1798
1825
  }
1799
1826
  resolutionRefusal(resolved.value.path, entry.operationId, `Operation initializer ${JSON.stringify(expression.getText(resolved.value.source))} is not a raw object, operation helper, or simple same-file factory call.`);
@@ -1803,10 +1830,23 @@ function buildRepositoryOperationIndex(sourceFiles, providerRoot) {
1803
1830
  if (indexedProviderProperties.has(key))
1804
1831
  return;
1805
1832
  indexedProviderProperties.add(key);
1806
- declarations.push({ path, construct, initializer: node.initializer });
1833
+ const resolvedInitializer = resolveLocatedExpression({ path, source, expression: node.initializer }, new Set());
1834
+ let runtimeInitializer;
1835
+ if (resolvedInitializer.status === "resolved") {
1836
+ const resolvedExpression = unwrapExpression(resolvedInitializer.value.expression);
1837
+ if (resolvedExpression !== undefined && ts.isCallExpression(resolvedExpression)) {
1838
+ runtimeInitializer = resolvedExpression.getText(resolvedInitializer.value.source);
1839
+ const initializers = runtimeComposedInitializers.get(path) ?? new Set();
1840
+ initializers.add(node.initializer.getStart(source));
1841
+ runtimeComposedInitializers.set(path, initializers);
1842
+ }
1843
+ }
1844
+ declarations.push({ path, construct, initializer: node.initializer, runtimeInitializer });
1807
1845
  const flattened = flattenRegistry({ path, source, expression: node.initializer }, new Set());
1808
1846
  if ("detail" in flattened) {
1809
- resolutionRefusal(path, "<operations>", flattened.detail);
1847
+ if (runtimeInitializer === undefined) {
1848
+ resolutionRefusal(path, "<operations>", flattened.detail);
1849
+ }
1810
1850
  }
1811
1851
  else {
1812
1852
  for (const entry of flattened.entries.values())
@@ -1919,9 +1959,9 @@ function buildRepositoryOperationIndex(sourceFiles, providerRoot) {
1919
1959
  operationIds,
1920
1960
  operationSites,
1921
1961
  excludedBindings,
1962
+ runtimeComposedInitializers,
1922
1963
  refusals,
1923
1964
  declarations,
1924
- discoveredCount,
1925
1965
  staticObjectResolverFor: (currentPath) => (expression, source) => resolveStaticObject(expression, sources.has(source.fileName) ? source.fileName : currentPath),
1926
1966
  };
1927
1967
  }
@@ -109,8 +109,10 @@ Structured errors return an `error` object with `code`, `message`,
109
109
 
110
110
  - `invalid_request`: include `requestId` and `input`; omit `connection` for
111
111
  public/no-auth operations and never send `connection: null`.
112
- - Credentials: declare `credential.keys`, pass local-only values through
113
- `connection.secrets`, and read them with `ctx.credential`.
112
+ - Credentials: declare `credential.keys` unless `auth.mode` is `platform-managed`;
113
+ that mode implies an unfiltered credential capability and forbids key declarations.
114
+ Pass local-only values through `connection.secrets` and read them with
115
+ `ctx.credential`.
114
116
  - Auth flow: call `/auth/start`, then `/auth/continue` with the same `flowId`;
115
117
  carry returned `contextPatch` values into the next request's `context`.
116
118
  - Stealth/browser runtime: keep access-sensitive operations on `ctx.stealth.fetch()` with
package/dist/define.d.ts CHANGED
@@ -78,6 +78,7 @@ export interface ProviderDeclaration {
78
78
  secrets?: ProviderSecretDeclaration[];
79
79
  /** Declares the environment capability binding. A bare object states use without configuration. */
80
80
  env?: Record<string, never> | true;
81
+ /** Declares credential keys; platform-managed auth declares this capability without key filtering. */
81
82
  credential?: CredentialDeclaration;
82
83
  /** Declares provider context metadata; this does not add a `ctx.context` member. */
83
84
  context?: ContextDeclaration;
package/dist/engine.js CHANGED
@@ -75,6 +75,8 @@ export const PROVIDER_CAPABILITY_KEYS = [
75
75
  ];
76
76
  const CAPABILITY_KEY_SET = new Set(PROVIDER_CAPABILITY_KEYS);
77
77
  function declaresCapability(provider, capability) {
78
+ if (capability === "credential" && provider.auth?.mode === "platform-managed")
79
+ return true;
78
80
  return Object.hasOwn(provider, capability) && provider[capability] !== undefined;
79
81
  }
80
82
  function attachmentError(provider, capability) {
package/dist/lint.js CHANGED
@@ -246,7 +246,7 @@ function lintAuthModel(provider) {
246
246
  rule: "platform-managed-no-credential-keys",
247
247
  level: "error",
248
248
  field: "credential.keys",
249
- message: `${providerLabel} must not declare credential.keys for platform-managed auth mode.`,
249
+ message: `${providerLabel} must not declare credential.keys because platform-managed auth mode already implies the unfiltered credential capability.`,
250
250
  });
251
251
  }
252
252
  const authFlowSource = getAuthFlowSource(provider);
package/dist/types.d.ts CHANGED
@@ -1860,8 +1860,9 @@ export interface ProviderRuntimeState {
1860
1860
  /**
1861
1861
  * The operation context exposed for one provider declaration. Capability
1862
1862
  * bindings are present only when their corresponding declaration is present;
1863
- * trace and request remain ambient runtime bindings. Omitting the type
1864
- * parameter preserves the legacy full context shape for existing annotations.
1863
+ * platform-managed auth itself declares the credential binding. Trace and
1864
+ * request remain ambient runtime bindings. Omitting the type parameter
1865
+ * preserves the legacy full context shape for existing annotations.
1865
1866
  */
1866
1867
  export type ProviderContext<TConfig = Record<string, unknown>> = {
1867
1868
  request?: ProviderRequestContext;
@@ -1870,6 +1871,12 @@ export type ProviderContext<TConfig = Record<string, unknown>> = {
1870
1871
  env: EnvContext;
1871
1872
  } : Record<never, never>) & ("credential" extends keyof TConfig ? {
1872
1873
  credential: CredentialContext;
1874
+ } : TConfig extends {
1875
+ auth: {
1876
+ mode: "platform-managed";
1877
+ };
1878
+ } ? {
1879
+ credential: CredentialContext;
1873
1880
  } : Record<never, never>) & ("http" extends keyof TConfig ? {
1874
1881
  http: HttpClient;
1875
1882
  } : Record<never, never>) & ("files" extends keyof TConfig ? string extends keyof TConfig ? {
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.2.0-beta.56",
2
+ "version": "2.2.0-beta.58",
3
3
  "name": "@apifuse/provider-sdk",
4
4
  "private": false,
5
5
  "type": "module",
@@ -0,0 +1,3 @@
1
+ import { makeRegistry } from "./operations/registry";
2
+
3
+ export default buildProvider({ operations: makeRegistry({ publicOnly: true }) });
@@ -0,0 +1,6 @@
1
+ export const firstOperation = defineOperation<ProviderContext>()({
2
+ annotations: { readOnly: true },
3
+ input: FirstInputSchema,
4
+ output: FirstOutputSchema,
5
+ handler: firstHandler,
6
+ });
@@ -0,0 +1,6 @@
1
+ export const secondOperation = defineOperation<ProviderContext>()({
2
+ annotations: { readOnly: true },
3
+ input: SecondInputSchema,
4
+ output: SecondOutputSchema,
5
+ handler: secondHandler,
6
+ });
@@ -0,0 +1,14 @@
1
+ import { makeRegistry } from "./operations/registry";
2
+
3
+ const literalOperation = defineOperation<ProviderContext>()({
4
+ annotations: { readOnly: true },
5
+ input: LiteralInputSchema,
6
+ output: LiteralOutputSchema,
7
+ handler: literalHandler,
8
+ });
9
+
10
+ export const literalProvider = buildProvider({
11
+ operations: { literal: literalOperation },
12
+ });
13
+
14
+ export default buildProvider({ operations: makeRegistry({ publicOnly: true }) });
@@ -0,0 +1,3 @@
1
+ export function makeRegistry(_options: { publicOnly: boolean }) {
2
+ return {};
3
+ }
@@ -141,6 +141,8 @@ type RepositoryMigrationContext = {
141
141
  readonly operationSites?: ReadonlyMap<number, string>;
142
142
  readonly excludedBindings?: ReadonlySet<string>;
143
143
  readonly staticObjectResolver?: StaticObjectResolver;
144
+ readonly runtimeComposedInitializers?: ReadonlySet<number>;
145
+ readonly recordDiscoveredSites?: (count: number) => void;
144
146
  };
145
147
 
146
148
  type OperationSite = {
@@ -194,7 +196,9 @@ function migrateOperationDeclarationInternal(
194
196
  options.operationIds,
195
197
  context.operationSites,
196
198
  context.excludedBindings,
199
+ context.runtimeComposedInitializers,
197
200
  );
201
+ context.recordDiscoveredSites?.(discovery.discoveredCount);
198
202
  if (discovery.refusals.length > 0) {
199
203
  return { status: "refused", refusals: discovery.refusals };
200
204
  }
@@ -1047,18 +1051,27 @@ function discoverOperationSites(
1047
1051
  operationIds: ReadonlyMap<string, string> | undefined,
1048
1052
  operationSites: ReadonlyMap<number, string> | undefined,
1049
1053
  excludedBindings: ReadonlySet<string> | undefined,
1054
+ runtimeComposedInitializers: ReadonlySet<number> | undefined,
1050
1055
  ): {
1051
1056
  readonly sites: OperationSite[];
1057
+ readonly discoveredCount: number;
1052
1058
  readonly refusals: OperationDeclarationRefusal[];
1053
1059
  } {
1054
1060
  const sitesByStart = new Map<number, OperationSite>();
1061
+ const discoveredStarts = new Set<number>();
1055
1062
  const localIds = new Map(operationIds ?? []);
1056
1063
  const localExcludedBindings = new Set(excludedBindings ?? []);
1057
1064
  const refusals: OperationDeclarationRefusal[] = [];
1058
1065
  const operationFactories = collectSimpleOperationFactories(source);
1059
1066
  const factoryCallIds = new Map<string, Set<string>>();
1060
1067
 
1061
- for (const map of collectOperationsMaps(source, constObjects, fileName, refusals)) {
1068
+ for (const map of collectOperationsMaps(
1069
+ source,
1070
+ constObjects,
1071
+ fileName,
1072
+ refusals,
1073
+ runtimeComposedInitializers,
1074
+ )) {
1062
1075
  for (const property of map.properties) {
1063
1076
  if (ts.isSpreadAssignment(property)) continue;
1064
1077
  const key = staticPropertyName(property.name);
@@ -1145,12 +1158,15 @@ function discoverOperationSites(
1145
1158
  if (ts.isCallExpression(node) && isOperationHelperCall(node)) {
1146
1159
  const argument = operationArgument(node);
1147
1160
  const bindingName = enclosingBindingName(node);
1161
+ const unwrappedArgument = unwrapExpression(argument);
1162
+ if (unwrappedArgument !== undefined && ts.isObjectLiteralExpression(unwrappedArgument)) {
1163
+ discoveredStarts.add(unwrappedArgument.getStart(source));
1164
+ }
1148
1165
  if (bindingName !== undefined && localExcludedBindings.has(bindingName)) return;
1149
1166
  const operationKey =
1150
1167
  (bindingName === undefined ? undefined : localIds.get(bindingName)) ??
1151
1168
  bindingName ??
1152
1169
  "<anonymous>";
1153
- const unwrappedArgument = unwrapExpression(argument);
1154
1170
  if (unwrappedArgument === undefined || !ts.isObjectLiteralExpression(unwrappedArgument)) {
1155
1171
  refusals.push(
1156
1172
  refusal(
@@ -1181,10 +1197,12 @@ function discoverOperationSites(
1181
1197
  ts.forEachChild(node, visit);
1182
1198
  };
1183
1199
  visit(source);
1200
+ for (const start of sitesByStart.keys()) discoveredStarts.add(start);
1184
1201
  return {
1185
1202
  sites: [...sitesByStart.values()].sort(
1186
1203
  (left, right) => left.object.getStart(source) - right.object.getStart(source),
1187
1204
  ),
1205
+ discoveredCount: discoveredStarts.size,
1188
1206
  refusals,
1189
1207
  };
1190
1208
  }
@@ -1223,6 +1241,7 @@ function collectOperationsMaps(
1223
1241
  constObjects: ReadonlyMap<string, TS.ObjectLiteralExpression>,
1224
1242
  fileName: string,
1225
1243
  refusals: OperationDeclarationRefusal[],
1244
+ runtimeComposedInitializers?: ReadonlySet<number>,
1226
1245
  ): TS.ObjectLiteralExpression[] {
1227
1246
  const maps = new Map<number, TS.ObjectLiteralExpression>();
1228
1247
  const inspect = (expression: TS.Expression, label: string): void => {
@@ -1238,6 +1257,7 @@ function collectOperationsMaps(
1238
1257
  return;
1239
1258
  }
1240
1259
  if (ts.isCallExpression(unwrapped)) {
1260
+ if (runtimeComposedInitializers?.has(expression.getStart(source)) === true) return;
1241
1261
  refusals.push(
1242
1262
  refusal(
1243
1263
  fileName,
@@ -1893,6 +1913,7 @@ export type OperationDeclarationRepositoryResult =
1893
1913
  readonly providerRoot: string;
1894
1914
  readonly changedFiles: readonly string[];
1895
1915
  readonly operationCount: number;
1916
+ readonly notes: readonly OperationDeclarationNote[];
1896
1917
  readonly sidecar?: string;
1897
1918
  readonly localeTodoCount: number;
1898
1919
  }
@@ -1902,10 +1923,17 @@ export type OperationDeclarationRepositoryResult =
1902
1923
  readonly refusals: readonly OperationDeclarationRefusal[];
1903
1924
  /** Operations that were independently migratable before repository-atomic refusal. */
1904
1925
  readonly operationCount: number;
1926
+ readonly notes: readonly OperationDeclarationNote[];
1905
1927
  readonly changedFiles: readonly string[];
1906
1928
  readonly localeTodoCount: number;
1907
1929
  };
1908
1930
 
1931
+ export type OperationDeclarationNote = {
1932
+ readonly code: "runtime_composed_registry";
1933
+ readonly path: string;
1934
+ readonly initializer: string;
1935
+ };
1936
+
1909
1937
  /** Run the file transform repository-wide, committing writes only if every file is provable. */
1910
1938
  export function migrateOperationDeclarationRepository(
1911
1939
  providerRootInput: string,
@@ -1920,6 +1948,7 @@ export function migrateOperationDeclarationRepository(
1920
1948
  const todos: LocaleTodo[] = [];
1921
1949
  const refusals: OperationDeclarationRefusal[] = [...repositoryIndex.refusals];
1922
1950
  let operationCount = 0;
1951
+ let repositoryDiscoveredCount = 0;
1923
1952
 
1924
1953
  for (const sourcePath of sourceFiles) {
1925
1954
  const relativePath = slash(relative(providerRoot, sourcePath));
@@ -1934,6 +1963,10 @@ export function migrateOperationDeclarationRepository(
1934
1963
  operationSites: repositoryIndex.operationSites.get(sourcePath),
1935
1964
  excludedBindings: repositoryIndex.excludedBindings.get(sourcePath),
1936
1965
  staticObjectResolver: repositoryIndex.staticObjectResolverFor(sourcePath),
1966
+ runtimeComposedInitializers: repositoryIndex.runtimeComposedInitializers.get(sourcePath),
1967
+ recordDiscoveredSites: (count) => {
1968
+ repositoryDiscoveredCount += count;
1969
+ },
1937
1970
  },
1938
1971
  );
1939
1972
  if (result.status === "refused") {
@@ -1947,7 +1980,21 @@ export function migrateOperationDeclarationRepository(
1947
1980
  todos.push(...result.localeTodos);
1948
1981
  }
1949
1982
  }
1950
- if (repositoryIndex.declarations.length > 0 && repositoryIndex.discoveredCount === 0) {
1983
+ const notes: OperationDeclarationNote[] =
1984
+ repositoryDiscoveredCount === 0
1985
+ ? []
1986
+ : repositoryIndex.declarations.flatMap((declaration) =>
1987
+ declaration.runtimeInitializer === undefined
1988
+ ? []
1989
+ : [
1990
+ {
1991
+ code: "runtime_composed_registry" as const,
1992
+ path: slash(relative(providerRoot, declaration.path)),
1993
+ initializer: declaration.runtimeInitializer,
1994
+ },
1995
+ ],
1996
+ );
1997
+ if (repositoryIndex.declarations.length > 0 && repositoryDiscoveredCount === 0) {
1951
1998
  for (const declaration of repositoryIndex.declarations) {
1952
1999
  refusals.push(
1953
2000
  refusal(
@@ -1970,6 +2017,7 @@ export function migrateOperationDeclarationRepository(
1970
2017
  providerRoot,
1971
2018
  refusals,
1972
2019
  operationCount,
2020
+ notes,
1973
2021
  changedFiles,
1974
2022
  localeTodoCount: todos.length,
1975
2023
  };
@@ -1984,6 +2032,7 @@ export function migrateOperationDeclarationRepository(
1984
2032
  providerRoot,
1985
2033
  changedFiles: [],
1986
2034
  operationCount,
2035
+ notes,
1987
2036
  localeTodoCount: 0,
1988
2037
  };
1989
2038
  }
@@ -2000,6 +2049,7 @@ export function migrateOperationDeclarationRepository(
2000
2049
  providerRoot,
2001
2050
  changedFiles,
2002
2051
  operationCount,
2052
+ notes,
2003
2053
  sidecar,
2004
2054
  localeTodoCount: todos.length,
2005
2055
  };
@@ -2180,15 +2230,16 @@ type ProviderOperationsDeclaration = {
2180
2230
  readonly path: string;
2181
2231
  readonly construct: string;
2182
2232
  readonly initializer: TS.Expression;
2233
+ readonly runtimeInitializer?: string;
2183
2234
  };
2184
2235
 
2185
2236
  type RepositoryOperationIndex = {
2186
2237
  readonly operationIds: Map<string, Map<string, string>>;
2187
2238
  readonly operationSites: Map<string, Map<number, string>>;
2188
2239
  readonly excludedBindings: Map<string, Set<string>>;
2240
+ readonly runtimeComposedInitializers: Map<string, Set<number>>;
2189
2241
  readonly refusals: OperationDeclarationRefusal[];
2190
2242
  readonly declarations: ProviderOperationsDeclaration[];
2191
- readonly discoveredCount: number;
2192
2243
  readonly staticObjectResolverFor: (path: string) => StaticObjectResolver;
2193
2244
  };
2194
2245
 
@@ -2205,12 +2256,12 @@ function buildRepositoryOperationIndex(
2205
2256
  const operationIds = buildOperationIdIndex(sourceFiles);
2206
2257
  const operationSites = new Map<string, Map<number, string>>();
2207
2258
  const excludedBindings = new Map<string, Set<string>>();
2259
+ const runtimeComposedInitializers = new Map<string, Set<number>>();
2208
2260
  const refusals: OperationDeclarationRefusal[] = [];
2209
2261
  const declarations: ProviderOperationsDeclaration[] = [];
2210
2262
  const indexedProviderProperties = new Set<string>();
2211
2263
  const factoryIds = new Map<string, { path: string; name: string; ids: Set<string> }>();
2212
2264
  const bindingCandidates = new Map<string, { path: string; name: string; ids: Set<string> }>();
2213
- let discoveredCount = 0;
2214
2265
 
2215
2266
  const relativePath = (path: string): string => slash(relative(providerRoot, path));
2216
2267
  const resolutionRefusal = (path: string, operationKey: string, detail: string): void => {
@@ -2489,7 +2540,6 @@ function buildRepositoryOperationIndex(
2489
2540
  }
2490
2541
  sites.set(start, operationId);
2491
2542
  operationSites.set(path, sites);
2492
- discoveredCount += 1;
2493
2543
  };
2494
2544
 
2495
2545
  const classifyEntry = (entry: RegistryEntry): void => {
@@ -2543,7 +2593,6 @@ function buildRepositoryOperationIndex(
2543
2593
  };
2544
2594
  factory.ids.add(entry.operationId);
2545
2595
  factoryIds.set(key, factory);
2546
- discoveredCount += 1;
2547
2596
  return;
2548
2597
  }
2549
2598
  resolutionRefusal(
@@ -2562,10 +2611,26 @@ function buildRepositoryOperationIndex(
2562
2611
  const key = `${path}\0${node.getStart(source)}`;
2563
2612
  if (indexedProviderProperties.has(key)) return;
2564
2613
  indexedProviderProperties.add(key);
2565
- declarations.push({ path, construct, initializer: node.initializer });
2614
+ const resolvedInitializer = resolveLocatedExpression(
2615
+ { path, source, expression: node.initializer },
2616
+ new Set(),
2617
+ );
2618
+ let runtimeInitializer: string | undefined;
2619
+ if (resolvedInitializer.status === "resolved") {
2620
+ const resolvedExpression = unwrapExpression(resolvedInitializer.value.expression);
2621
+ if (resolvedExpression !== undefined && ts.isCallExpression(resolvedExpression)) {
2622
+ runtimeInitializer = resolvedExpression.getText(resolvedInitializer.value.source);
2623
+ const initializers = runtimeComposedInitializers.get(path) ?? new Set<number>();
2624
+ initializers.add(node.initializer.getStart(source));
2625
+ runtimeComposedInitializers.set(path, initializers);
2626
+ }
2627
+ }
2628
+ declarations.push({ path, construct, initializer: node.initializer, runtimeInitializer });
2566
2629
  const flattened = flattenRegistry({ path, source, expression: node.initializer }, new Set());
2567
2630
  if ("detail" in flattened) {
2568
- resolutionRefusal(path, "<operations>", flattened.detail);
2631
+ if (runtimeInitializer === undefined) {
2632
+ resolutionRefusal(path, "<operations>", flattened.detail);
2633
+ }
2569
2634
  } else {
2570
2635
  for (const entry of flattened.entries.values()) classifyEntry(entry);
2571
2636
  }
@@ -2691,9 +2756,9 @@ function buildRepositoryOperationIndex(
2691
2756
  operationIds,
2692
2757
  operationSites,
2693
2758
  excludedBindings,
2759
+ runtimeComposedInitializers,
2694
2760
  refusals,
2695
2761
  declarations,
2696
- discoveredCount,
2697
2762
  staticObjectResolverFor: (currentPath) => (expression, source) =>
2698
2763
  resolveStaticObject(expression, sources.has(source.fileName) ? source.fileName : currentPath),
2699
2764
  };
@@ -109,8 +109,10 @@ Structured errors return an `error` object with `code`, `message`,
109
109
 
110
110
  - `invalid_request`: include `requestId` and `input`; omit `connection` for
111
111
  public/no-auth operations and never send `connection: null`.
112
- - Credentials: declare `credential.keys`, pass local-only values through
113
- `connection.secrets`, and read them with `ctx.credential`.
112
+ - Credentials: declare `credential.keys` unless `auth.mode` is `platform-managed`;
113
+ that mode implies an unfiltered credential capability and forbids key declarations.
114
+ Pass local-only values through `connection.secrets` and read them with
115
+ `ctx.credential`.
114
116
  - Auth flow: call `/auth/start`, then `/auth/continue` with the same `flowId`;
115
117
  carry returned `contextPatch` values into the next request's `context`.
116
118
  - Stealth/browser runtime: keep access-sensitive operations on `ctx.stealth.fetch()` with
package/src/define.ts CHANGED
@@ -600,6 +600,7 @@ export interface ProviderDeclaration {
600
600
  secrets?: ProviderSecretDeclaration[];
601
601
  /** Declares the environment capability binding. A bare object states use without configuration. */
602
602
  env?: Record<string, never> | true;
603
+ /** Declares credential keys; platform-managed auth declares this capability without key filtering. */
603
604
  credential?: CredentialDeclaration;
604
605
  /** Declares provider context metadata; this does not add a `ctx.context` member. */
605
606
  context?: ContextDeclaration;
package/src/engine.ts CHANGED
@@ -199,6 +199,7 @@ function declaresCapability(
199
199
  provider: ProviderDefinition,
200
200
  capability: ProviderCapabilityKey,
201
201
  ): boolean {
202
+ if (capability === "credential" && provider.auth?.mode === "platform-managed") return true;
202
203
  return Object.hasOwn(provider, capability) && provider[capability] !== undefined;
203
204
  }
204
205
 
package/src/lint.ts CHANGED
@@ -398,7 +398,7 @@ function lintAuthModel(provider: {
398
398
  rule: "platform-managed-no-credential-keys",
399
399
  level: "error",
400
400
  field: "credential.keys",
401
- message: `${providerLabel} must not declare credential.keys for platform-managed auth mode.`,
401
+ message: `${providerLabel} must not declare credential.keys because platform-managed auth mode already implies the unfiltered credential capability.`,
402
402
  });
403
403
  }
404
404
 
package/src/types.ts CHANGED
@@ -2266,8 +2266,9 @@ export interface ProviderRuntimeState {
2266
2266
  /**
2267
2267
  * The operation context exposed for one provider declaration. Capability
2268
2268
  * bindings are present only when their corresponding declaration is present;
2269
- * trace and request remain ambient runtime bindings. Omitting the type
2270
- * parameter preserves the legacy full context shape for existing annotations.
2269
+ * platform-managed auth itself declares the credential binding. Trace and
2270
+ * request remain ambient runtime bindings. Omitting the type parameter
2271
+ * preserves the legacy full context shape for existing annotations.
2271
2272
  */
2272
2273
  export type ProviderContext<TConfig = Record<string, unknown>> = {
2273
2274
  request?: ProviderRequestContext;
@@ -2276,7 +2277,9 @@ export type ProviderContext<TConfig = Record<string, unknown>> = {
2276
2277
  & ("env" extends keyof TConfig ? { env: EnvContext } : Record<never, never>)
2277
2278
  & ("credential" extends keyof TConfig
2278
2279
  ? { credential: CredentialContext }
2279
- : Record<never, never>)
2280
+ : TConfig extends { auth: { mode: "platform-managed" } }
2281
+ ? { credential: CredentialContext }
2282
+ : Record<never, never>)
2280
2283
  & ("http" extends keyof TConfig ? { http: HttpClient } : Record<never, never>)
2281
2284
  & ("files" extends keyof TConfig
2282
2285
  ? string extends keyof TConfig