@maxceem/agw 0.5.1 → 0.6.0

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 (2) hide show
  1. package/dist/agw.mjs +463 -352
  2. package/package.json +2 -2
package/dist/agw.mjs CHANGED
@@ -6,7 +6,7 @@ var __export = (target, all) => {
6
6
  };
7
7
 
8
8
  // <define:__AGW_MANIFEST__>
9
- var define_AGW_MANIFEST_default = { version: "0.5.1", wrangler: "4.96.0", upgradeFrom: ["0.5.1", "0.5.0", "0.4.0"], release: { url: "https://github.com/maxceem/app-ai-gateway/releases/download/v0.5.1/gateway-0.5.1.tar.gz", sha256: "f4461618d5882d9b32dc44d5d8db048bbb6d6df5ec9899c77c5877491296d77c", manifest: "f08d792d4b9f8b9537075a99583b39c5a355891a20d20632bb5ed957ad6eacf2" } };
9
+ var define_AGW_MANIFEST_default = { version: "0.6.0", wrangler: "4.96.0", upgradeFrom: ["0.6.0", "0.5.2", "0.5.1", "0.5.0", "0.4.0"], release: { url: "https://github.com/maxceem/app-ai-gateway/releases/download/v0.6.0/gateway-0.6.0.tar.gz", sha256: "792ad8d9cbb13a98c1e99532061128c53d28549de4345adbcc1c5b2c34cd1fec", manifest: "2edb8dd7a9e80bd906ef0ab9354b97f4c431e726eca04524569fe5435d7a6b2c" } };
10
10
 
11
11
  // src/main.ts
12
12
  import { Writable as Writable2 } from "node:stream";
@@ -14642,13 +14642,6 @@ var API_STYLES = [
14642
14642
  /** A provider-native operation with no cross-provider contract of its own. */
14643
14643
  "other"
14644
14644
  ];
14645
- var API_STYLE_PATHS = {
14646
- responses: "v1/responses",
14647
- chat_completions: "v1/chat/completions",
14648
- anthropic_messages: "v1/messages",
14649
- gemini_native: "v1beta/models/{model}:generateContent",
14650
- audio_transcription: "v1/audio/transcriptions"
14651
- };
14652
14645
  var ENDPOINT_API_STYLES = ["responses", "audio_transcription"];
14653
14646
  var OUTPUT_CLAMP_STYLES = [
14654
14647
  "responses",
@@ -15316,6 +15309,18 @@ var AppUpdateSchema = AppWriteSchema.extend({
15316
15309
  description: "Required: the revision the application was read at. An absent one answers 400 app_revision_required, a stale one 409 app_revision_conflict."
15317
15310
  })
15318
15311
  }).meta({ id: "AppUpdate" });
15312
+ var SNIPPET_LANGUAGES = ["swift", "curl"];
15313
+ var AppSnippetQuerySchema = external_exports.object({
15314
+ language: external_exports.enum(SNIPPET_LANGUAGES, { error: "language must be swift or curl" }).optional().meta({
15315
+ description: "swift or curl. Defaults to swift for an App Attest application and curl for a server application; the other one is refused with 400 unsupported_snippet."
15316
+ }),
15317
+ provider: external_exports.string().min(1).optional().meta({
15318
+ description: "The slug of the provider to write the example for. Defaults to the first one the application can reach. Unknown answers 404 provider_not_found; held but outside the application's routing or disabled answers 400 provider_unavailable."
15319
+ }),
15320
+ endpoint: external_exports.string().min(1).optional().meta({
15321
+ description: "The name of one of the application's custom endpoints, to call it instead of a provider path. Unknown answers 404 endpoint_not_found."
15322
+ })
15323
+ });
15319
15324
  var AppAttestKeyIdSchema = external_exports.string().min(1).max(200);
15320
15325
  var ChallengeSchema = external_exports.string().min(1).max(200);
15321
15326
  var AppAttestRegisterRequestSchema = external_exports.object({
@@ -15489,8 +15494,26 @@ var OrganizationSelectRequestSchema = external_exports.object({
15489
15494
  var CredentialNameRequestSchema = external_exports.object({
15490
15495
  name: external_exports.string().trim().min(1).max(100)
15491
15496
  });
15492
- var ManagementKeyCreateRequestSchema = CredentialNameRequestSchema.meta({ id: "ManagementKeyCreateRequest" });
15497
+ var CredentialGrantSchema = external_exports.enum(["read", "manage"]);
15498
+ var ManagementKeyCreateRequestSchema = CredentialNameRequestSchema.extend({
15499
+ grant: CredentialGrantSchema.default("manage").meta({
15500
+ description: "How much of your role the key may use. `read` may list, inspect and validate but cannot change your apps, providers or settings: a change your role would otherwise allow is refused with `403 grant_insufficient`, and the key may still revoke itself. `manage` may do everything your role allows. Defaults to `manage`."
15501
+ })
15502
+ }).meta({ id: "ManagementKeyCreateRequest" });
15493
15503
  var ApiKeyCreateRequestSchema = CredentialNameRequestSchema.meta({ id: "ApiKeyCreateRequest" });
15504
+ var OAuthConsentProofTokenSchema = external_exports.string().regex(/^[A-Za-z0-9_-]{32,256}$/).meta({
15505
+ description: "The proof from the consent page URL's fragment."
15506
+ });
15507
+ var OAuthConsentProofSchema = external_exports.object({ submissionToken: OAuthConsentProofTokenSchema }).strict();
15508
+ var OAuthConsentAllowRequestSchema = external_exports.object({
15509
+ submissionToken: OAuthConsentProofTokenSchema,
15510
+ organizationId: external_exports.string().min(1).max(256).meta({
15511
+ description: "The account the connection acts in: one the signed-in person is a member of."
15512
+ }),
15513
+ grant: CredentialGrantSchema.meta({
15514
+ description: "How much of the person's role the connection may use, whatever the client asked for."
15515
+ })
15516
+ }).strict();
15494
15517
 
15495
15518
  // src/common.ts
15496
15519
  import { randomBytes } from "node:crypto";
@@ -16653,15 +16676,6 @@ var StateStore = class {
16653
16676
  function selectedProviderPolicies(routing) {
16654
16677
  return routing.providers.mode === "selected" ? routing.providers.selected : {};
16655
16678
  }
16656
- function providerPolicyFor(routing, slug) {
16657
- if (routing.providers.mode === "all") return { allowed_paths: [], allowed_models: [] };
16658
- return Object.hasOwn(routing.providers.selected, slug) ? routing.providers.selected[slug] : void 0;
16659
- }
16660
- function reachableProviders(routing, instances) {
16661
- return instances.filter(
16662
- (instance) => instance.status === "active" && providerPolicyFor(routing, instance.slug) !== void 0
16663
- );
16664
- }
16665
16679
 
16666
16680
  // ../src/shared/protocols.ts
16667
16681
  var PROTOCOLS = {
@@ -16711,23 +16725,6 @@ var PROTOCOLS = {
16711
16725
  var CLASSIFIED = Object.values(PROTOCOLS).filter(
16712
16726
  (protocol) => protocol.path !== null
16713
16727
  );
16714
- function classifyPath(providerPath) {
16715
- for (const protocol of CLASSIFIED) {
16716
- const match = protocol.path.exec(providerPath);
16717
- if (!match) continue;
16718
- const span = match.indices?.groups?.model;
16719
- const value = match.groups?.model;
16720
- if (span === void 0 || value === void 0) return { protocol };
16721
- return {
16722
- protocol,
16723
- model: {
16724
- value,
16725
- template: `${providerPath.slice(0, span[0])}{model}${providerPath.slice(span[1])}`
16726
- }
16727
- };
16728
- }
16729
- return { protocol: PROTOCOLS.other };
16730
- }
16731
16728
 
16732
16729
  // ../src/shared/first-request.ts
16733
16730
  var PROVIDER_PLACEHOLDER = "PROVIDER_SLUG";
@@ -16738,126 +16735,9 @@ var EXAMPLE_GAP_NOTES = {
16738
16735
  model: `No priced model was found for this provider, so ${MODEL_PLACEHOLDER} stands in for one. Use a model the provider serves.`,
16739
16736
  body: `This app allows no path whose request shape is known here, so ${BODY_PLACEHOLDER} stands in for the body the provider documents.`
16740
16737
  };
16741
- function examplePath(type, { gatewayRouted = false, model } = {}) {
16742
- const own = providerDescriptor(type).examplePath ?? API_STYLE_PATHS.chat_completions;
16743
- if (own.includes("{model}")) return model ? own.replace("{model}", model) : void 0;
16744
- return gatewayRouted && own.endsWith("chat/completions") ? API_STYLE_PATHS.chat_completions : own;
16745
- }
16746
- var hello = () => [{ role: "user", content: "Say hello." }];
16747
- var EXAMPLE_BODIES = {
16748
- responses: (model) => ({ model, input: "Say hello." }),
16749
- chat_completions: (model) => ({ model, messages: hello() }),
16750
- anthropic_messages: (model) => ({ model, max_tokens: 128, messages: hello() }),
16751
- // The model is in the URL, not the body.
16752
- gemini_native: () => ({ contents: [{ parts: [{ text: "Say hello." }] }] })
16753
- };
16754
- function requestFor(path, model) {
16755
- const { style: style2 } = classifyPath(path).protocol;
16756
- return {
16757
- body: EXAMPLE_BODIES[style2]?.(model) ?? null,
16758
- anthropic: style2 === "anthropic_messages"
16759
- };
16760
- }
16761
- function modelsFor(provider, policy, prices) {
16762
- if (policy?.allowed_models?.length) return policy.allowed_models;
16763
- return [
16764
- ...Object.keys(provider.pricing ?? {}),
16765
- ...Object.entries(prices[provider.type] ?? {}).filter(([, price]) => price.output !== void 0).map(([model]) => model)
16766
- ];
16767
- }
16768
- function firstRequest(routing, providers, prices) {
16769
- let fallback;
16770
- const candidates = routing ? reachableProviders(routing, providers) : providers.filter((provider) => provider.status === "active");
16771
- for (const provider of candidates) {
16772
- const policy = routing?.providers.mode === "selected" ? providerPolicyFor(routing, provider.slug) : void 0;
16773
- const models = modelsFor(provider, policy, prices);
16774
- const model = models[0] ?? MODEL_PLACEHOLDER;
16775
- const gaps = models.length ? [] : ["model"];
16776
- const fallbackPath = examplePath(provider.type, {
16777
- gatewayRouted: Boolean(provider.providerGatewayId),
16778
- model
16779
- });
16780
- const paths = policy?.allowed_paths?.length ? policy.allowed_paths : fallbackPath ? [fallbackPath] : [];
16781
- for (const entry of paths) {
16782
- const path = typeof entry === "string" ? entry : entry.path;
16783
- if (path.includes("*")) continue;
16784
- const fixed = typeof entry !== "string" ? entry.fixed_model : void 0;
16785
- const { body, anthropic } = requestFor(path, fixed ?? model);
16786
- const example = {
16787
- target: { provider: provider.slug, path },
16788
- body,
16789
- anthropic,
16790
- // A fixed model is the app's own, so a catalog that has none of its
16791
- // own to offer is not a gap on a path that never carries one.
16792
- gaps: body && fixed ? [] : gaps
16793
- };
16794
- if (body) return example;
16795
- fallback ??= { ...example, gaps: [...example.gaps, "body"] };
16796
- }
16797
- }
16798
- return fallback ?? {
16799
- target: { provider: PROVIDER_PLACEHOLDER, path: "v1/chat/completions" },
16800
- body: requestFor("v1/chat/completions", MODEL_PLACEHOLDER).body,
16801
- anthropic: false,
16802
- gaps: ["provider", "model"]
16803
- };
16804
- }
16805
- function exampleNotes(example) {
16806
- return example.gaps.map((gap) => EXAMPLE_GAP_NOTES[gap]);
16807
- }
16808
- function exampleUrl(baseUrl, appId, target) {
16809
- const origin2 = baseUrl.replace(/\/+$/, "");
16810
- const app2 = `${origin2}/v1/apps/${encodeURIComponent(appId)}`;
16811
- return "endpoint" in target ? `${app2}/endpoints/${encodeURIComponent(target.endpoint)}` : `${app2}/proxy/${encodeURIComponent(target.provider)}/${target.path}`;
16812
- }
16813
16738
  function shellQuote(value) {
16814
16739
  return `'${value.replaceAll("'", "'\\''")}'`;
16815
16740
  }
16816
- function commented(notes2, marker) {
16817
- return (notes2 ?? []).map((note) => `${marker} ${note}
16818
- `).join("");
16819
- }
16820
- function curlSnippet(options) {
16821
- const { baseUrl, appId, example, notes: notes2, keyExpression = "$APP_AI_GATEWAY_KEY" } = options;
16822
- const body = example.body ? JSON.stringify(example.body) : BODY_PLACEHOLDER;
16823
- return commented(notes2, "#") + `curl --fail-with-body ${shellQuote(exampleUrl(baseUrl, appId, example.target))} \\
16824
- -H "Authorization: Bearer ${keyExpression}" \\
16825
- -H 'Content-Type: application/json' \\
16826
- ` + (example.anthropic ? " -H 'anthropic-version: 2023-06-01' \\\n" : "") + ` -d ${shellQuote(body)}
16827
- `;
16828
- }
16829
- var ISSUER_TOKEN_NOTE = "Replace yourIdentitySDK.currentIDToken(forceRefresh: forceRefresh) with your configured issuer integration.";
16830
- function swiftSignsInUsers(authentication) {
16831
- return authentication?.type === "apple_app_attest" && authentication.end_user.source === "issuer";
16832
- }
16833
- function swiftAuthMode(authentication) {
16834
- return swiftSignsInUsers(authentication) ? ".appAttest(issuerTokenProvider: { forceRefresh in\n // Return a fresh signed token from your configured identity SDK.\n try await yourIdentitySDK.currentIDToken(forceRefresh: forceRefresh)\n })" : ".appAttestInstall";
16835
- }
16836
- function swiftSnippet(options) {
16837
- const { baseUrl, appId, example, notes: notes2, authentication } = options;
16838
- const authMode = swiftAuthMode(authentication);
16839
- const target = "endpoint" in example.target ? `endpointSlug: ${JSON.stringify(example.target.endpoint)}` : `provider: ${JSON.stringify(example.target.provider)},
16840
- providerPath: ${JSON.stringify(example.target.path)}`;
16841
- const body = example.body ? `request.httpBody = Data(${JSON.stringify(JSON.stringify(example.body))}.utf8)` : `// Supply the provider-native JSON body.
16842
- request.httpBody = Data(${JSON.stringify(BODY_PLACEHOLDER)}.utf8)`;
16843
- return commented(notes2, "//") + `import Foundation
16844
- import AppAIGateway
16845
-
16846
- let gateway = AppAIGatewayClient(
16847
- appID: ${JSON.stringify(appId)},
16848
- baseURL: URL(string: ${JSON.stringify(baseUrl.replace(/\/+$/, ""))})!,
16849
- authMode: ${authMode}
16850
- )
16851
-
16852
- var request = try await gateway.authorizedRequest(
16853
- ${target}
16854
- )
16855
- request.setValue("application/json", forHTTPHeaderField: "Content-Type")
16856
- ${example.anthropic ? 'request.setValue("2023-06-01", forHTTPHeaderField: "anthropic-version")\n' : ""}${body}
16857
- let (data, _) = try await URLSession.shared.data(for: request)
16858
- print(String(decoding: data, as: UTF8.self))
16859
- `;
16860
- }
16861
16741
 
16862
16742
  // ../src/shared/app-defaults.ts
16863
16743
  function unlimitedScope() {
@@ -17003,8 +16883,8 @@ async function appCommand(ctx, command, args, flags) {
17003
16883
  const action = command.slice(4);
17004
16884
  if (action === "list") return (await ctx.call("listApps")).data;
17005
16885
  if (action === "validate") {
17006
- const doc2 = localApp(await jsonFile(await required2(flags, "file")));
17007
- return { definition: doc2, validation: await remoteValidation(ctx, doc2) };
16886
+ const doc = localApp(await jsonFile(await required2(flags, "file")));
16887
+ return { definition: doc, validation: await remoteValidation(ctx, doc) };
17008
16888
  }
17009
16889
  if (action === "add" || action === "update") {
17010
16890
  let current;
@@ -17026,32 +16906,32 @@ async function appCommand(ctx, command, args, flags) {
17026
16906
  ))
17027
16907
  fail("invalid_input", "Provide at least one application change.");
17028
16908
  }
17029
- const doc2 = await appDocument(flags, current);
17030
- if (flags["key-output"] && doc2.config.authentication.type !== "api_key")
16909
+ const doc = await appDocument(flags, current);
16910
+ if (flags["key-output"] && doc.config.authentication.type !== "api_key")
17031
16911
  fail("invalid_input", "--key-output only applies to server apps.");
17032
16912
  if (flags["dry-run"])
17033
16913
  return {
17034
16914
  dryRun: true,
17035
- definition: doc2,
17036
- validation: await remoteValidation(ctx, doc2, appId2 || void 0)
16915
+ definition: doc,
16916
+ validation: await remoteValidation(ctx, doc, appId2 || void 0)
17037
16917
  };
17038
16918
  {
17039
16919
  let app2;
17040
16920
  let key;
17041
16921
  if (action === "add") {
17042
16922
  await ctx.bootstrap();
17043
- if (doc2.config.authentication.type === "api_key") {
17044
- const created = await ctx.keyOperation("app.add", doc2, flags["key-output"]);
16923
+ if (doc.config.authentication.type === "api_key") {
16924
+ const created = await ctx.keyOperation("app.add", doc, flags["key-output"]);
17045
16925
  app2 = created.operation.result.app;
17046
16926
  key = created.key;
17047
16927
  } else {
17048
- const created = await ctx.operation("app.add", doc2);
16928
+ const created = await ctx.operation("app.add", doc);
17049
16929
  app2 = created.result.app;
17050
16930
  }
17051
16931
  } else {
17052
16932
  const updated = await ctx.call("updateApp", {
17053
16933
  params: { app: appId2 },
17054
- body: { ...doc2, revision }
16934
+ body: { ...doc, revision }
17055
16935
  });
17056
16936
  ({ app: app2 } = updated.data);
17057
16937
  }
@@ -17069,7 +16949,7 @@ ${snippet}`;
17069
16949
  ...snippet ? { snippet } : {},
17070
16950
  app: app2,
17071
16951
  ...key ? { applicationKey: key } : {},
17072
- guidance: doc2.config.authentication.type === "apple_app_attest" ? `Add https://github.com/maxceem/app-ai-gateway-swift from 1.0.0, enable App Attest, and test on a supported physical device. Run agw app snippet ${app2.id} --provider <slug> for integration code. Production releases should use production-only App Attest.` : `Store the generated key on your server; never embed it in a mobile application. Run agw app snippet ${app2.id} for this request again.`
16952
+ guidance: doc.config.authentication.type === "apple_app_attest" ? `Add https://github.com/maxceem/app-ai-gateway-swift from 1.0.0, enable App Attest, and test on a supported physical device. Run agw app snippet ${app2.id} --provider <slug> for integration code. Production releases should use production-only App Attest.` : `Store the generated key on your server; never embed it in a mobile application. Run agw app snippet ${app2.id} for this request again.`
17073
16953
  };
17074
16954
  }
17075
16955
  }
@@ -17098,9 +16978,10 @@ ${snippet}`;
17098
16978
  return { appId: appId2, applicationKey: created.key };
17099
16979
  }
17100
16980
  const appId = args[0] ?? "";
16981
+ if (action === "check") return (await ctx.call("checkApp", { params: { app: appId } })).data;
16982
+ if (action === "snippet") return appSnippet(ctx, appId, flags);
17101
16983
  const { data } = await ctx.call("getApp", { params: { app: appId } });
17102
16984
  if (action === "show") return data;
17103
- const doc = documentOf(data.app);
17104
16985
  if (action === "remove") {
17105
16986
  await confirm(
17106
16987
  `Delete app ${data.app.name} (${appId})? Its keys, users and authentication state will be removed and clients will lose access.`,
@@ -17108,100 +16989,52 @@ ${snippet}`;
17108
16989
  );
17109
16990
  return (await ctx.call("deleteApp", { params: { app: appId }, query: { confirm: appId } })).data;
17110
16991
  }
17111
- if (action === "check") {
17112
- const validation = await remoteValidation(ctx, doc, appId);
17113
- const { data: providers } = await ctx.call("listProviders");
17114
- const selected = providers.providers.filter(
17115
- (p) => providerPolicyFor(doc.config.routing, p.slug) !== void 0
17116
- );
17117
- return {
17118
- appId,
17119
- validation,
17120
- status: doc.status,
17121
- providers: selected.map((p) => ({
17122
- id: p.id,
17123
- slug: p.slug,
17124
- status: p.status
17125
- })),
17126
- ready: doc.status === "active" && reachableProviders(doc.config.routing, selected).length > 0,
17127
- limitations: [
17128
- "No inference was sent.",
17129
- "Physical device attestation, issuer login, subscription entitlement and upstream credentials were not exercised."
17130
- ]
17131
- };
17132
- }
17133
- if (action === "snippet") {
17134
- const ios = doc.config.authentication.type === "apple_app_attest";
17135
- const language = flags.language ?? (ios ? "swift" : "curl");
17136
- if (!["swift", "curl"].includes(language))
17137
- fail("invalid_input", "--language must be swift or curl.");
17138
- if (language === "swift" !== ios)
17139
- fail(
17140
- "unsupported_snippet",
17141
- ios ? "iOS applications authenticate with App Attest, which curl cannot perform." : "Swift snippets are for iOS applications.",
17142
- `Run agw app snippet ${appId} --language ${ios ? "swift" : "curl"}.`
17143
- );
17144
- const notes2 = [];
17145
- let example;
17146
- if (flags.endpoint) {
17147
- const endpoint = doc.config.endpoints[flags.endpoint];
17148
- if (!endpoint)
17149
- fail("endpoint_not_found", "Choose an existing custom endpoint.");
17150
- const responses = endpoint.api_style === "responses";
17151
- example = {
17152
- target: { endpoint: flags.endpoint },
17153
- body: responses ? { input: "Say hello." } : null,
17154
- anthropic: false,
17155
- gaps: responses ? [] : ["body"]
17156
- };
17157
- } else {
17158
- const routing = doc.config.routing;
17159
- const { data: all } = await ctx.call("listProviders");
17160
- const reachable = reachableProviders(routing, all.providers);
17161
- const requested = typeof flags.provider === "string" ? flags.provider : void 0;
17162
- if (requested && !reachable.some((p) => p.slug === requested))
17163
- fail(
17164
- all.providers.some((p) => p.slug === requested) ? "provider_unavailable" : "provider_not_found",
17165
- all.providers.some((p) => p.slug === requested) ? "That provider is disabled or outside this app's proxy policy." : "No provider has that slug.",
17166
- "Run agw provider list for the slugs, and agw app show <id> for the policy."
17167
- );
17168
- const { data: catalog } = await ctx.call("listModelPrices");
17169
- const providers = requested ? reachable.filter((p) => p.slug === requested) : reachable;
17170
- example = firstRequest(routing, providers, catalog.prices);
17171
- if (!requested && reachable.length > 1)
17172
- notes2.push(`This app can reach ${reachable.length} providers. Add --provider <slug> for a different one.`);
17173
- }
17174
- notes2.push(...exampleNotes(example));
17175
- const clientUrl = ctx.active?.deployment?.apiUrl ?? ctx.url;
17176
- let snippet;
17177
- if (ios) {
17178
- if (swiftSignsInUsers(doc.config.authentication)) notes2.push(ISSUER_TOKEN_NOTE);
17179
- snippet = swiftSnippet({
17180
- baseUrl: clientUrl,
17181
- appId,
17182
- example,
17183
- authentication: doc.config.authentication,
17184
- notes: [
17185
- "Swift package: https://github.com/maxceem/app-ai-gateway-swift (from: 1.0.0)",
17186
- "Enable App Attest and test on a supported physical device.",
17187
- ...notes2
17188
- ]
17189
- });
17190
- } else {
17191
- snippet = curlSnippet({ baseUrl: clientUrl, appId, example, notes: notes2 });
17192
- }
17193
- if (flags.output) {
17194
- const output = await reserveOutput(flags.output);
17195
- try {
17196
- await output.write(snippet);
17197
- return { output: output.path };
17198
- } finally {
17199
- await output.cancel();
16992
+ fail("unknown_command", "Unknown command.");
16993
+ }
16994
+ async function appSnippet(ctx, appId, flags) {
16995
+ const language = flags.language;
16996
+ if (language !== void 0 && !SNIPPET_LANGUAGES.includes(language))
16997
+ fail("invalid_input", "--language must be swift or curl.");
16998
+ const provider = typeof flags.provider === "string" ? flags.provider : void 0;
16999
+ let data;
17000
+ try {
17001
+ ({ data } = await ctx.call("getAppSnippet", {
17002
+ params: { app: appId },
17003
+ query: {
17004
+ ...language === void 0 ? {} : { language },
17005
+ ...provider === void 0 ? {} : { provider },
17006
+ ...flags.endpoint === void 0 ? {} : { endpoint: flags.endpoint }
17200
17007
  }
17008
+ }));
17009
+ } catch (error51) {
17010
+ const remedy = error51 instanceof CliError ? snippetRemedy(error51.code, appId, language) : void 0;
17011
+ if (remedy === void 0 || !(error51 instanceof CliError)) throw error51;
17012
+ throw new CliError(error51.code, error51.message, remedy, error51.exitCode, error51.details);
17013
+ }
17014
+ if (flags.output) {
17015
+ const output = await reserveOutput(flags.output);
17016
+ try {
17017
+ await output.write(data.snippet);
17018
+ return { output: output.path };
17019
+ } finally {
17020
+ await output.cancel();
17201
17021
  }
17202
- return { language: ios ? "swift" : "shell", snippet, notes: notes2 };
17203
17022
  }
17204
- fail("unknown_command", "Unknown command.");
17023
+ return data;
17024
+ }
17025
+ function snippetRemedy(code, appId, language) {
17026
+ switch (code) {
17027
+ // Only a language that was asked for is refused, so the other one is right.
17028
+ case "unsupported_snippet":
17029
+ return `Run agw app snippet ${appId} --language ${language === "swift" ? "curl" : "swift"}.`;
17030
+ case "provider_not_found":
17031
+ case "provider_unavailable":
17032
+ return "Run agw provider list for the slugs, and agw app show <id> for the policy.";
17033
+ case "endpoint_not_found":
17034
+ return `Run agw app show ${appId} for its custom endpoints.`;
17035
+ default:
17036
+ return void 0;
17037
+ }
17205
17038
  }
17206
17039
 
17207
17040
  // src/context.ts
@@ -17606,11 +17439,22 @@ var ManagementKeySummarySchema = external_exports.object({
17606
17439
  createdAt: external_exports.string(),
17607
17440
  revokedAt: external_exports.string().nullable(),
17608
17441
  source: external_exports.string().meta({
17609
- description: "Where the key was issued: `console`, `cli` for a key a CLI login received, or `bootstrap` for the key a CLI account starts with. Display only.",
17442
+ description: "Where the key was issued: `console`, `cli` for a key a CLI login received, `bootstrap` for the key a CLI account starts with, or `oauth` for an OAuth connection an MCP client holds. Display only.",
17610
17443
  example: "cli"
17611
17444
  }),
17445
+ credentialType: external_exports.enum(["apiKey", "oauth"]).meta({
17446
+ description: "What the row authenticates as: `apiKey` for a management key, `oauth` for an MCP client's connection. `source` says where it was issued and is display only.",
17447
+ example: "apiKey"
17448
+ }),
17612
17449
  label: external_exports.string().nullable().meta({
17613
17450
  description: "Who holds it, as the issuing client described itself, e.g. `CLI on mac-studio`. Display only."
17451
+ }),
17452
+ grant: CredentialGrantSchema.meta({
17453
+ description: "How much of its holder's role the key may use: `read` may list, inspect and validate but cannot change anything other than revoking itself; `manage` does everything the role allows. Keys issued before grants existed are `manage`.",
17454
+ example: "manage"
17455
+ }),
17456
+ clientId: external_exports.string().nullable().meta({
17457
+ description: "For an OAuth connection (`credentialType: \"oauth\"`), the client it was issued to: a registered client's id, or the https URL a client identifies itself with. Null for a key. A connection's `name` is the client's own name for itself, display only; its `expiresAt` is when it ends unless the client refreshes it first."
17614
17458
  })
17615
17459
  });
17616
17460
  var ManagementKeyListResponseSchema = external_exports.object({
@@ -17767,7 +17611,7 @@ var IdentitySessionSchema = external_exports.object({
17767
17611
  organization: OrganizationSummarySchema.nullable(),
17768
17612
  role: OrganizationRoleSchema,
17769
17613
  memberships: external_exports.array(OrganizationMembershipSchema),
17770
- credentialType: external_exports.enum(["session", "apiKey"]),
17614
+ credentialType: external_exports.enum(["session", "apiKey", "oauth"]),
17771
17615
  /**
17772
17616
  * How the caller proved who they are: `interactive` for a human who just
17773
17617
  * signed in, `credential` for a key. The CLI's browser handoff refuses to
@@ -17785,6 +17629,58 @@ var IdentitySessionSchema = external_exports.object({
17785
17629
  }).nullable()
17786
17630
  })
17787
17631
  });
17632
+ var OAuthConsentDetailsResponseSchema = external_exports.object({
17633
+ id: external_exports.string(),
17634
+ /** `pending` until a person allows or denies it, or it lapses ten minutes after it opened. */
17635
+ state: external_exports.enum(["pending", "completed", "denied", "expired", "retired"]),
17636
+ client: external_exports.object({
17637
+ /** The `client_id`: a registered client's id, or the https URL of its metadata document. */
17638
+ id: external_exports.string(),
17639
+ /**
17640
+ * The client's own name for itself. Untrusted text: show it as text, as
17641
+ * declared by `domain`, never as markup.
17642
+ */
17643
+ name: external_exports.string(),
17644
+ /** The host that served the client's metadata document; null for a registered client. */
17645
+ domain: external_exports.string().nullable(),
17646
+ source: external_exports.enum(["cimd", "registered"])
17647
+ }),
17648
+ /** Where the browser is sent once this is decided: the host of the client's redirect URI. */
17649
+ redirectHost: external_exports.string(),
17650
+ /** What the client asked for. The person chooses the grant; it is only the default. */
17651
+ requestedGrant: CredentialGrantSchema,
17652
+ expiresAt: external_exports.string(),
17653
+ /** The person signed in to this browser, who would allow it; null when nobody is. */
17654
+ viewer: external_exports.object({ name: external_exports.string().nullable(), email: external_exports.string().nullable() }).nullable(),
17655
+ /** The accounts that person may connect the client to: every live membership. */
17656
+ accounts: external_exports.array(external_exports.object({
17657
+ id: external_exports.string(),
17658
+ name: external_exports.string(),
17659
+ role: OrganizationRoleSchema
17660
+ })),
17661
+ /**
17662
+ * What stands between this browser and Allow: nobody is signed in, or the
17663
+ * person signed in belongs to no account the client could use. Null when
17664
+ * nothing does.
17665
+ */
17666
+ blockedBy: external_exports.enum(["registration_required", "no_eligible_organization"]).nullable(),
17667
+ /**
17668
+ * Whether "continue without an account" would be admitted now: always on
17669
+ * the hosted service, and on a self-hosted deployment only while it is
17670
+ * empty. A guest connection always has the `manage` grant.
17671
+ */
17672
+ guestAvailable: external_exports.boolean(),
17673
+ /** When an account created now without a person would be deleted unless claimed; null where accounts have no deadline. */
17674
+ guestExpiresAt: external_exports.string().nullable(),
17675
+ googleEnabled: external_exports.boolean(),
17676
+ /** Whether the console's own registration would admit a new person now. */
17677
+ registrationOpen: external_exports.boolean()
17678
+ });
17679
+ var OAuthConsentRedirectResponseSchema = external_exports.object({
17680
+ redirect: external_exports.string().meta({
17681
+ description: "The client's redirect URI with `code`, `state` and `iss`, or with `error=access_denied` after a denial. Navigate the browser there."
17682
+ })
17683
+ });
17788
17684
  var OrganizationListResponseSchema = external_exports.object({
17789
17685
  organizations: external_exports.array(OrganizationMembershipSchema)
17790
17686
  });
@@ -17831,6 +17727,35 @@ var AppValidateResponseSchema = external_exports.object({
17831
17727
  var AppDraftValidateResponseSchema = external_exports.object({
17832
17728
  valid: external_exports.literal(true)
17833
17729
  });
17730
+ var AppCheckResponseSchema = external_exports.object({
17731
+ appId: external_exports.string(),
17732
+ validation: AppValidateResponseSchema.meta({
17733
+ description: "The stored configuration judged as an update of it would be. A configuration that would be refused answers the check itself with 400."
17734
+ }),
17735
+ status: external_exports.enum(APP_STATUSES),
17736
+ providers: external_exports.array(external_exports.object({
17737
+ id: external_exports.string(),
17738
+ slug: external_exports.string(),
17739
+ status: external_exports.enum(["active", "disabled"])
17740
+ })).meta({
17741
+ description: "Every provider instance the application's routing names, a disabled one included so that it shows as disabled rather than missing."
17742
+ }),
17743
+ ready: external_exports.boolean().meta({
17744
+ description: "Whether the application is active and at least one provider it names is active."
17745
+ }),
17746
+ limitations: external_exports.array(external_exports.string()).meta({
17747
+ description: "What this check did not exercise, one sentence each."
17748
+ })
17749
+ });
17750
+ var AppSnippetResponseSchema = external_exports.object({
17751
+ language: external_exports.enum(["swift", "shell"]),
17752
+ snippet: external_exports.string().meta({
17753
+ description: "Source to paste. It never contains a credential: a server application's example reads its key from APP_AI_GATEWAY_KEY."
17754
+ }),
17755
+ notes: external_exports.array(external_exports.string()).meta({
17756
+ description: "What the example stands in for or leaves out, one sentence each. They are also written into the snippet as comments."
17757
+ })
17758
+ });
17834
17759
  var CreatedApiKeySchema = external_exports.object({
17835
17760
  id: external_exports.string(),
17836
17761
  name: external_exports.string(),
@@ -17839,6 +17764,7 @@ var CreatedApiKeySchema = external_exports.object({
17839
17764
  key_prefix: external_exports.string(),
17840
17765
  created_at: external_exports.string()
17841
17766
  });
17767
+ var AppKeyMetadataSchema = CreatedApiKeySchema.omit({ key: true });
17842
17768
  var CreatedAppResponseSchema = AppResponseSchema.extend({
17843
17769
  api_key: CreatedApiKeySchema.nullable()
17844
17770
  });
@@ -17949,6 +17875,41 @@ var ModelPriceSchema = external_exports.object({
17949
17875
  var PricesResponseSchema = external_exports.object({
17950
17876
  prices: external_exports.record(external_exports.string(), external_exports.record(external_exports.string(), ModelPriceSchema))
17951
17877
  });
17878
+ var OperationResultSchema = external_exports.object({
17879
+ /** The account a claim settled, a login landed in, or a bootstrap created. */
17880
+ accountId: external_exports.string().optional(),
17881
+ app: AppResponseSchema.shape.app.optional(),
17882
+ api_key: AppKeyMetadataSchema.nullable().optional(),
17883
+ provider: ProviderSummarySchema.optional(),
17884
+ gateway: ProviderGatewaySummarySchema.optional()
17885
+ }).meta({ id: "OperationResult" });
17886
+ var OperationStatusResponseSchema = external_exports.object({
17887
+ id: external_exports.string(),
17888
+ /**
17889
+ * The operation's kind as the gateway runs it: `app.add.reserved` for an app
17890
+ * created under a reservation, `provider.add.browser` for a provider whose
17891
+ * key a person enters in a browser, `claim`, and so on.
17892
+ */
17893
+ kind: external_exports.string(),
17894
+ state: external_exports.enum(["pending", "completed", "expired"]),
17895
+ /** Set beside `state: "expired"` when a person declined the browser step rather than letting it lapse. */
17896
+ denied: external_exports.literal(true).optional(),
17897
+ createdAt: external_exports.string(),
17898
+ /** While pending, when it lapses unless completed. */
17899
+ expiresAt: external_exports.string(),
17900
+ result: OperationResultSchema.optional(),
17901
+ /** While a key the operation created can still be revealed, once, the page a person opens to see it. */
17902
+ reveal_url: external_exports.url().optional()
17903
+ }).meta({ id: "OperationStatus" });
17904
+ var RevealedOperationResponseSchema = external_exports.object({
17905
+ id: external_exports.string(),
17906
+ kind: external_exports.string(),
17907
+ result: external_exports.object({
17908
+ app: AppResponseSchema.shape.app.optional(),
17909
+ /** `key` is the plaintext. It is shown this once; the gateway keeps only a hash. */
17910
+ api_key: CreatedApiKeySchema.nullable().optional()
17911
+ })
17912
+ }).meta({ id: "RevealedOperation" });
17952
17913
 
17953
17914
  // ../src/contracts/cli.ts
17954
17915
  var CliProofSchema = external_exports.string().regex(/^[A-Za-z0-9_-]{32,256}$/);
@@ -18236,6 +18197,11 @@ var CLI_ERRORS = {
18236
18197
  410: "The protected credential exchange has expired; no replacement is minted.",
18237
18198
  429: "The durable initiation or submission rate limit was reached."
18238
18199
  };
18200
+ var CONSENT_ERRORS = {
18201
+ 409: "The authorization was denied, already completed by another choice, or the account the choice needed is gone; nothing was written.",
18202
+ 410: "The authorization lapsed: it is open for ten minutes. Start the connection again from the client."
18203
+ };
18204
+ var CONSENT_DESCRIPTION = "The API of the console's OAuth consent page, which an MCP client's authorization request sends a person to. First-party browser only: both the request URL origin and the exact Origin header must match the console origin, and `submissionToken` is the proof from the consent page URL's fragment, so it never travels in a URL. Allowing, continuing without an account and denying each answer the `redirect` to send the browser to, and answer it again, byte for byte, when repeated with the same proof.";
18239
18205
  var BROWSER_HANDOFF_DESCRIPTION = "First-party browser only: both the request URL origin and exact Origin header must match consoleOrigin; a separate submissionToken is required \u2014 the proof from the approval URL's fragment, or, for a login, the user code the terminal shows. Identity approval (a claim or a login) also requires an interactive human session; registration is limited to a valid pending claim or login. Provider secret values are write-only.";
18240
18206
  var SignUpRequestSchema = external_exports.object({
18241
18207
  name: external_exports.string(),
@@ -18486,7 +18452,7 @@ var CATALOG = {
18486
18452
  security: "management",
18487
18453
  // The whole billing subtree is a person's to act on: a service credential
18488
18454
  // may run an account but may not buy, change or cancel what pays for it.
18489
- policy: { identity: "human" },
18455
+ policy: { session: true },
18490
18456
  response: BillingPlansResponseSchema,
18491
18457
  responseDescription: "Billing service response."
18492
18458
  },
@@ -18497,7 +18463,7 @@ var CATALOG = {
18497
18463
  tags: ["Admin billing"],
18498
18464
  summary: "Get billing access and the current allowance period",
18499
18465
  security: "management",
18500
- policy: { identity: "human" },
18466
+ policy: { session: true },
18501
18467
  response: BillingStatusResponseSchema,
18502
18468
  responseDescription: "Billing access, and the current period against the plan's request allowance when an entitlement resolves."
18503
18469
  },
@@ -18508,7 +18474,7 @@ var CATALOG = {
18508
18474
  tags: ["Admin billing"],
18509
18475
  summary: "Create a hosted checkout",
18510
18476
  security: "management",
18511
- policy: { identity: "human" },
18477
+ policy: { session: true },
18512
18478
  request: BillingCheckoutRequestSchema,
18513
18479
  response: BillingCheckoutResponseSchema,
18514
18480
  responseDescription: "Hosted checkout URL."
@@ -18520,7 +18486,7 @@ var CATALOG = {
18520
18486
  tags: ["Admin billing"],
18521
18487
  summary: "Change the subscription plan",
18522
18488
  security: "management",
18523
- policy: { identity: "human" },
18489
+ policy: { session: true },
18524
18490
  request: BillingPlanSelectionSchema,
18525
18491
  response: BillingChangeResponseSchema,
18526
18492
  responseDescription: "Billing service response."
@@ -18532,7 +18498,7 @@ var CATALOG = {
18532
18498
  tags: ["Admin billing"],
18533
18499
  summary: "Resume a canceled subscription",
18534
18500
  security: "management",
18535
- policy: { identity: "human" },
18501
+ policy: { session: true },
18536
18502
  request: BillingPlanSelectionSchema,
18537
18503
  response: BillingChangeResponseSchema,
18538
18504
  responseDescription: "Billing service response."
@@ -18544,7 +18510,7 @@ var CATALOG = {
18544
18510
  tags: ["Admin billing"],
18545
18511
  summary: "Cancel the subscription at period end",
18546
18512
  security: "management",
18547
- policy: { identity: "human" },
18513
+ policy: { session: true },
18548
18514
  response: BillingCancelResponseSchema,
18549
18515
  responseDescription: "Cancellation accepted."
18550
18516
  },
@@ -18555,7 +18521,7 @@ var CATALOG = {
18555
18521
  tags: ["Admin billing"],
18556
18522
  summary: "Start a no-card trial",
18557
18523
  security: "management",
18558
- policy: { identity: "human" },
18524
+ policy: { session: true },
18559
18525
  request: BillingTrialRequestSchema,
18560
18526
  response: BillingAccessSchema,
18561
18527
  responseDescription: "Trial access state."
@@ -18592,7 +18558,7 @@ var CATALOG = {
18592
18558
  security: "management",
18593
18559
  // A POST that stores nothing: it answers whether a body would be accepted,
18594
18560
  // which is a read of the configuration rules and not a write.
18595
- policy: { role: "member", access: "read" },
18561
+ policy: { role: "member", access: "read", grant: "read" },
18596
18562
  params: APP_PARAM,
18597
18563
  request: AppWriteSchema,
18598
18564
  response: AppValidateResponseSchema,
@@ -18605,11 +18571,34 @@ var CATALOG = {
18605
18571
  summary: "Validate a new application configuration without saving it",
18606
18572
  description: "Judged as a creation would be, against the account's current providers and prices.",
18607
18573
  security: "management",
18608
- policy: { role: "member", access: "read" },
18574
+ policy: { role: "member", access: "read", grant: "read" },
18609
18575
  request: AppWriteSchema,
18610
18576
  response: AppDraftValidateResponseSchema,
18611
18577
  responseDescription: "The configuration would be accepted."
18612
18578
  },
18579
+ checkApp: {
18580
+ method: "GET",
18581
+ path: "/v1/admin/apps/{app}/check",
18582
+ tags: ["Admin applications"],
18583
+ summary: "Check whether an application is ready to serve requests",
18584
+ description: "Validates the stored configuration as an update of it would be judged, lists every provider its routing names with that provider's status \u2014 a disabled one included \u2014 and reports `ready` when the application is active and at least one of those providers is active. It sends no inference: App Attest on a physical device, issuer sign-in, subscription entitlements and the upstream credentials themselves are not exercised, and `limitations` says so in every answer. A stored configuration that would no longer be accepted answers `400 invalid_request` with the reason.",
18585
+ security: "management",
18586
+ params: APP_PARAM,
18587
+ response: AppCheckResponseSchema,
18588
+ responseDescription: "What the check established, and what it did not exercise."
18589
+ },
18590
+ getAppSnippet: {
18591
+ method: "GET",
18592
+ path: "/v1/admin/apps/{app}/snippet",
18593
+ tags: ["Admin applications"],
18594
+ summary: "Get the first request an application can send",
18595
+ description: "Written against what the application has today: a provider it can reach and a priced model where those exist, and named placeholders, explained in `notes`, where they do not. An App Attest application gets Swift for the App AI Gateway Swift package and a server application gets curl; asking for the other answers `400 unsupported_snippet`. The URL is the one application clients call, which is this deployment's separate API host where it publishes one. A snippet never contains a credential: a server application's example reads its key from the `APP_AI_GATEWAY_KEY` environment variable.",
18596
+ security: "management",
18597
+ params: APP_PARAM,
18598
+ query: AppSnippetQuerySchema,
18599
+ response: AppSnippetResponseSchema,
18600
+ responseDescription: "The example, its language and the notes written into it."
18601
+ },
18613
18602
  deleteApp: {
18614
18603
  method: "DELETE",
18615
18604
  path: "/v1/admin/apps/{app}",
@@ -18638,7 +18627,7 @@ var CATALOG = {
18638
18627
  path: "/v1/admin/keys",
18639
18628
  tags: ["Admin management keys"],
18640
18629
  summary: "Create a management key",
18641
- description: "Console session only, and requires the owner or admin role. A management key cannot create another one, so revoking a key you handed out ends that access for good. The plaintext agw_mgmt_ token is returned once, and never expires; the account's own deadline is the only one.",
18630
+ description: "Console session only, and requires the owner or admin role. A management key cannot create another one, so revoking a key you handed out ends that access for good. The plaintext agw_mgmt_ token is returned once, and never expires; the account's own deadline is the only one. A key created with the `read` grant cannot change your apps, providers or settings: a change its owner's role would otherwise allow is refused with `403 grant_insufficient`, and the key may still revoke itself.",
18642
18631
  security: "session",
18643
18632
  request: ManagementKeyCreateRequestSchema,
18644
18633
  status: 201,
@@ -18797,6 +18786,9 @@ var CATALOG = {
18797
18786
  tags: ["Admin organizations"],
18798
18787
  summary: "List the organizations the caller belongs to",
18799
18788
  security: "management",
18789
+ // cf-auth lists them only for an interactive session: a key is scoped to
18790
+ // its one organization and may not read the others its owner belongs to.
18791
+ policy: { session: true },
18800
18792
  response: OrganizationListResponseSchema,
18801
18793
  responseDescription: "Memberships ordered by organization creation time."
18802
18794
  },
@@ -18810,8 +18802,11 @@ var CATALOG = {
18810
18802
  // Switching the active organization re-signs the cookie naming which
18811
18803
  // tenant the caller reads; gating it behind owner/admin would strand a
18812
18804
  // read-only member in one organization, and gating it behind setup access
18813
- // would strand them in an account whose trial has ended.
18814
- policy: { role: "member", access: "read" },
18805
+ // would strand them in an account whose trial has ended. Only an
18806
+ // interactive session has a cookie to re-sign, and cf-auth refuses any
18807
+ // other caller, so the entry says so rather than leaving a key to learn it
18808
+ // from the handler — after the grant check, with a remedy that cannot work.
18809
+ policy: { role: "member", access: "read", session: true },
18815
18810
  request: OrganizationSelectRequestSchema,
18816
18811
  response: IdentitySessionSchema,
18817
18812
  responseDescription: "Session rescoped to the selected organization."
@@ -19021,6 +19016,34 @@ var CATALOG = {
19021
19016
  response: PricesResponseSchema,
19022
19017
  responseDescription: "The priced model catalog this deployment enforces."
19023
19018
  },
19019
+ getOperation: {
19020
+ method: "GET",
19021
+ path: "/v1/admin/operations/{id}",
19022
+ tags: ["Admin change operations"],
19023
+ summary: "Get where an operation stands",
19024
+ description: "Any credential of the account that opened the operation may read it, whatever its role and grant; any other is answered `404 operation_not_found`, so the answer never says whether the id exists elsewhere. A pending operation reports when it lapses, a completed one what it changed, and one declined in a browser `expired` with `denied: true`. It never returns a secret and never collects one: a key the operation created is revealed only on the page `reveal_url` names, to a person signed in to the console as an owner or admin. The approval link of a pending browser step is in the answer that opened it and nowhere else.",
19025
+ security: "management",
19026
+ params: { id: { example: "4b1e\u2026" } },
19027
+ response: OperationStatusResponseSchema,
19028
+ responseDescription: "Where the operation stands, and what it changed once completed."
19029
+ },
19030
+ revealOperation: {
19031
+ method: "POST",
19032
+ path: "/v1/admin/operations/{id}/reveal",
19033
+ tags: ["Admin change operations"],
19034
+ summary: "Reveal the key an operation created, once",
19035
+ description: "Console session only, and requires the owner or admin role in the operation's account; a management key is refused with `403 session_required`. First-party console only: the request URL's origin and the exact `Origin` header must both be the console origin. Answers the key an app or key creation made once, within 15 minutes of its creation: a second reveal is `409 already_revealed`, and one after the window, or for a key revoked since, is `410 operation_expired`. Reveal it only on a page a person asked to see it on, never on load.",
19036
+ security: "session",
19037
+ // Read access: the key already exists, and collecting it is not a change.
19038
+ policy: { role: "admin", access: "read", session: true },
19039
+ params: { id: { example: "4b1e\u2026" } },
19040
+ response: RevealedOperationResponseSchema,
19041
+ responseDescription: "The key the operation created, with its plaintext.",
19042
+ errors: {
19043
+ 409: "The key was already revealed.",
19044
+ 410: "The reveal window passed, or the key was revoked since it was created."
19045
+ }
19046
+ },
19024
19047
  getCliCapabilities: {
19025
19048
  method: "GET",
19026
19049
  path: "/v1/cli/capabilities",
@@ -19091,9 +19114,9 @@ var CATALOG = {
19091
19114
  summary: "Revoke the management key this request is sent with",
19092
19115
  description: "Logging a CLI out. Any management key may end itself, whatever its role and whatever the account's standing; a browser session cannot use this and signs out instead.",
19093
19116
  security: "management",
19094
- // Holding a key is authority enough to end it: a member may, and an
19095
- // account past its free window may.
19096
- policy: { role: "member", access: "read" },
19117
+ // Holding a key is authority enough to end it: a member may, an account
19118
+ // past its free window may, and a key with the `read` grant may.
19119
+ policy: { role: "member", access: "read", grant: "read" },
19097
19120
  response: CliCredentialRevokeResponseSchema,
19098
19121
  responseDescription: "The key is revoked; the next request made with it is refused.",
19099
19122
  errors: CLI_ERRORS
@@ -19205,6 +19228,74 @@ var CATALOG = {
19205
19228
  responseDescription: "A new human identity for a pending claim or login, with its session set as a cookie.",
19206
19229
  errors: CLI_ERRORS
19207
19230
  },
19231
+ /*
19232
+ * The OAuth consent page's API. Like the CLI handoff, there is no page to
19233
+ * fetch here: the console renders `/oauth/consent` from its own bundle, and
19234
+ * these four carry the proof from its fragment in their bodies. The OAuth
19235
+ * endpoints themselves — discovery, `/oauth/authorize`, `/oauth/token` and
19236
+ * `/oauth/revoke` — are HTTP-only and not management operations, so they
19237
+ * have no entries; the MCP guide documents them.
19238
+ */
19239
+ getOauthConsentDetails: {
19240
+ method: "POST",
19241
+ path: "/v1/console/oauth/{id}/details",
19242
+ tags: ["Console OAuth consent"],
19243
+ summary: "OAuth consent: details",
19244
+ description: `${CONSENT_DESCRIPTION} Reading the details changes nothing. The client's name is its own claim, shown as declared by the host that served its metadata document.`,
19245
+ security: "public",
19246
+ params: { id: { description: "The authorization's id, from the consent page URL's `id` query parameter." } },
19247
+ request: OAuthConsentProofSchema,
19248
+ response: OAuthConsentDetailsResponseSchema,
19249
+ responseDescription: "Who is asking and for what, who this browser would allow it as, the accounts that person may choose, and whether continuing without an account is available.",
19250
+ errors: CONSENT_ERRORS
19251
+ },
19252
+ allowOauthConsent: {
19253
+ method: "POST",
19254
+ path: "/v1/console/oauth/{id}/allow",
19255
+ tags: ["Console OAuth consent"],
19256
+ summary: "OAuth consent: allow",
19257
+ description: `${CONSENT_DESCRIPTION} Console session only: the person signed in to this browser connects the client to one of their accounts, with the grant they choose. A management key or an OAuth access token is refused with \`403 session_required\`.`,
19258
+ security: "session",
19259
+ // Any member may connect a client with their own role, as any member may
19260
+ // log a CLI in; the grant is theirs to narrow. Read access, because
19261
+ // connecting changes nothing in the account.
19262
+ policy: { role: "member", access: "read", session: true },
19263
+ params: { id: { description: "The authorization's id." } },
19264
+ request: OAuthConsentAllowRequestSchema,
19265
+ response: OAuthConsentRedirectResponseSchema,
19266
+ responseDescription: "The client's redirect URI with the authorization code.",
19267
+ errors: CONSENT_ERRORS
19268
+ },
19269
+ continueOauthWithoutAccount: {
19270
+ method: "POST",
19271
+ path: "/v1/console/oauth/{id}/guest",
19272
+ tags: ["Console OAuth consent"],
19273
+ summary: "OAuth consent: continue without an account",
19274
+ description: `${CONSENT_DESCRIPTION} Creates an account nobody has claimed yet, owned by the connection's own service identity, and connects the client to it with the \`manage\` grant whatever it asked for. Admitted where the deployment allows a new unclaimed account \u2014 always on the hosted service, where it is deleted unless a person claims it, and on a self-hosted deployment only while it is empty \u2014 and counted per network address with the CLI's account creation. Repeating it for the same authorization answers the same account.`,
19275
+ security: "public",
19276
+ params: { id: { description: "The authorization's id." } },
19277
+ request: OAuthConsentProofSchema,
19278
+ response: OAuthConsentRedirectResponseSchema,
19279
+ responseDescription: "The client's redirect URI with the authorization code.",
19280
+ errors: {
19281
+ ...CONSENT_ERRORS,
19282
+ 409: "This deployment no longer admits an account without a person (a self-hosted one that has been initialized), or another choice completed the authorization first.",
19283
+ 429: "Too many accounts were created from this network address. Carries Retry-After."
19284
+ }
19285
+ },
19286
+ denyOauthConsent: {
19287
+ method: "POST",
19288
+ path: "/v1/console/oauth/{id}/deny",
19289
+ tags: ["Console OAuth consent"],
19290
+ summary: "OAuth consent: deny",
19291
+ description: `${CONSENT_DESCRIPTION} Anyone holding the link may decline, signed in or not.`,
19292
+ security: "public",
19293
+ params: { id: { description: "The authorization's id." } },
19294
+ request: OAuthConsentProofSchema,
19295
+ response: OAuthConsentRedirectResponseSchema,
19296
+ responseDescription: "The client's redirect URI with `error=access_denied`.",
19297
+ errors: CONSENT_ERRORS
19298
+ },
19208
19299
  cliBrowserGoogle: {
19209
19300
  method: "POST",
19210
19301
  path: "/v1/cli/browser/{id}/google",
@@ -19237,6 +19328,93 @@ function operationPath(name, ...rest) {
19237
19328
  return query === void 0 ? filled : filled + searchSuffix(query);
19238
19329
  }
19239
19330
 
19331
+ // src/client.ts
19332
+ var CLI_OPERATIONS = /* @__PURE__ */ new Set([
19333
+ "getCliCapabilities",
19334
+ "bootstrapCliAccount",
19335
+ "createCliOperation",
19336
+ "pollCliOperation",
19337
+ "getCliAccount",
19338
+ "getCliUsage",
19339
+ "openCliLogin",
19340
+ "redeemCliLogin",
19341
+ "revokeCliCredential"
19342
+ ]);
19343
+ var pathFor = operationPath;
19344
+ var GatewayClient = class {
19345
+ transport;
19346
+ source;
19347
+ constructor(transport, source) {
19348
+ this.transport = transport;
19349
+ this.source = source;
19350
+ }
19351
+ /**
19352
+ * Sends one documented operation and parses its answer with that operation's
19353
+ * own schema.
19354
+ *
19355
+ * The parse is not a formality. zod drops what a schema does not declare, so
19356
+ * this is what keeps an undocumented server field — or anything an unexpected
19357
+ * response carries — out of the values the commands go on to print.
19358
+ */
19359
+ async publicCall(name, { params, query, body, key, url: url2, headers } = {}) {
19360
+ const wire = await this.transport.request(
19361
+ url2 ?? this.source.url(),
19362
+ pathFor(name, params, query),
19363
+ {
19364
+ method: CATALOG[name].method,
19365
+ ...body === void 0 ? {} : { body },
19366
+ ...key === void 0 ? {} : { key },
19367
+ ...headers === void 0 ? {} : { headers }
19368
+ }
19369
+ );
19370
+ return { data: parse4(name, wire.data) };
19371
+ }
19372
+ /** The same call, with the connection's management credential attached. */
19373
+ async call(name, options = {}) {
19374
+ const key = options.key ?? this.credential(name);
19375
+ try {
19376
+ return await this.publicCall(name, { ...options, key });
19377
+ } catch (error51) {
19378
+ if (error51 instanceof CliError && error51.details?.status === 401 && options.key === void 0 && this.source.environmentKey())
19379
+ throw new CliError(
19380
+ error51.code,
19381
+ error51.message,
19382
+ `Check ${MANAGEMENT_KEY_ENV}: the deployment refused the key it holds.`,
19383
+ error51.exitCode,
19384
+ error51.details
19385
+ );
19386
+ throw error51;
19387
+ }
19388
+ }
19389
+ /**
19390
+ * The current management credential, or the refusal that names the way back.
19391
+ * An admin operation is also reachable from a plain deployment connection,
19392
+ * which is why those get the wider instruction.
19393
+ */
19394
+ credential(name) {
19395
+ const token = this.source.managementKey();
19396
+ if (!token)
19397
+ fail(
19398
+ "login_required",
19399
+ "The selected connection is not authenticated.",
19400
+ CLI_OPERATIONS.has(name) ? `Run agw account login, or set ${MANAGEMENT_KEY_ENV}.` : `Run agw account login or agw deployment connect, or set ${MANAGEMENT_KEY_ENV}.`,
19401
+ 4
19402
+ );
19403
+ return token;
19404
+ }
19405
+ };
19406
+ function parse4(name, data) {
19407
+ const parsed = CATALOG[name].response.safeParse(data);
19408
+ if (!parsed.success)
19409
+ fail(
19410
+ "invalid_response",
19411
+ `The deployment answered ${String(name)} with an unusable body.`,
19412
+ "Check that the CLI and the deployment are the same release.",
19413
+ 3
19414
+ );
19415
+ return parsed.data;
19416
+ }
19417
+
19240
19418
  // src/login.ts
19241
19419
  import { createServer } from "node:http";
19242
19420
  import { hostname as hostname4, platform as platform2, release } from "node:os";
@@ -19399,17 +19577,6 @@ function jsonLine(line, paint) {
19399
19577
  }
19400
19578
 
19401
19579
  // src/context.ts
19402
- var CLI_OPERATIONS = /* @__PURE__ */ new Set([
19403
- "getCliCapabilities",
19404
- "bootstrapCliAccount",
19405
- "createCliOperation",
19406
- "pollCliOperation",
19407
- "getCliAccount",
19408
- "getCliUsage",
19409
- "openCliLogin",
19410
- "redeemCliLogin",
19411
- "revokeCliCredential"
19412
- ]);
19413
19580
  var LOGIN_TIMEOUT_SECONDS = 300;
19414
19581
  var LOGIN_RECORD_MS = 36e5;
19415
19582
  var LOOPBACK_GRACE_MS = 1e4;
@@ -19422,7 +19589,6 @@ var defaultIo = () => ({
19422
19589
  });
19423
19590
  var KEY_OPERATION_KINDS = /* @__PURE__ */ new Set(["app.add", "app.key.add"]);
19424
19591
  var OPERATION_RETENTION_MS = 90 * 864e5;
19425
- var pathFor = operationPath;
19426
19592
  var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 425, 429]);
19427
19593
  var OPERATION_BOUND_STATUS = /* @__PURE__ */ new Set([409, 410]);
19428
19594
  function definitiveRefusal(error51) {
@@ -19447,7 +19613,8 @@ function bootstrapCredential(operation2) {
19447
19613
  var Context = class {
19448
19614
  store;
19449
19615
  state;
19450
- transport;
19616
+ /** Every request this context sends goes through it. */
19617
+ client;
19451
19618
  flags;
19452
19619
  io;
19453
19620
  /** Operations whose result this run has printed, released once stdout has taken it. */
@@ -19456,9 +19623,13 @@ var Context = class {
19456
19623
  constructor(store, state, transport, flags, io = {}) {
19457
19624
  this.store = store;
19458
19625
  this.state = state;
19459
- this.transport = transport;
19460
19626
  this.flags = flags;
19461
19627
  this.io = { ...defaultIo(), ...io };
19628
+ this.client = new GatewayClient(transport, {
19629
+ url: () => this.url,
19630
+ managementKey: () => this.managementKey,
19631
+ environmentKey: () => this.environmentKey
19632
+ });
19462
19633
  }
19463
19634
  get active() {
19464
19635
  return this.state.active;
@@ -19491,70 +19662,13 @@ var Context = class {
19491
19662
  async save() {
19492
19663
  await this.store.write(this.state);
19493
19664
  }
19494
- /**
19495
- * Sends one documented operation and parses its answer with that operation's
19496
- * own schema.
19497
- *
19498
- * The parse is not a formality. zod drops what a schema does not declare, so
19499
- * this is what keeps an undocumented server field — or anything an unexpected
19500
- * response carries — out of the values the commands go on to print.
19501
- */
19502
- async publicCall(name, { params, query, body, key, url: url2, headers } = {}) {
19503
- const wire = await this.transport.request(
19504
- url2 ?? this.url,
19505
- pathFor(name, params, query),
19506
- {
19507
- method: CATALOG[name].method,
19508
- ...body === void 0 ? {} : { body },
19509
- ...key === void 0 ? {} : { key },
19510
- ...headers === void 0 ? {} : { headers }
19511
- }
19512
- );
19513
- return { data: this.parse(name, wire.data) };
19514
- }
19515
- parse(name, data) {
19516
- const parsed = CATALOG[name].response.safeParse(data);
19517
- if (!parsed.success)
19518
- fail(
19519
- "invalid_response",
19520
- `The deployment answered ${String(name)} with an unusable body.`,
19521
- "Check that the CLI and the deployment are the same release.",
19522
- 3
19523
- );
19524
- return parsed.data;
19665
+ /** One documented operation, sent with only the credential `options` names; see {@link GatewayClient}. */
19666
+ publicCall(name, options = {}) {
19667
+ return this.client.publicCall(name, options);
19525
19668
  }
19526
19669
  /** The same call, with the connection's management credential attached. */
19527
- async call(name, options = {}) {
19528
- const key = options.key ?? this.credential(name);
19529
- try {
19530
- return await this.publicCall(name, { ...options, key });
19531
- } catch (error51) {
19532
- if (error51 instanceof CliError && error51.details?.status === 401 && options.key === void 0 && this.environmentKey)
19533
- throw new CliError(
19534
- error51.code,
19535
- error51.message,
19536
- `Check ${MANAGEMENT_KEY_ENV}: the deployment refused the key it holds.`,
19537
- error51.exitCode,
19538
- error51.details
19539
- );
19540
- throw error51;
19541
- }
19542
- }
19543
- /**
19544
- * The current management credential, or the refusal that names the way back.
19545
- * An admin operation is also reachable from a plain deployment connection,
19546
- * which is why those get the wider instruction.
19547
- */
19548
- credential(name) {
19549
- const token = this.managementKey;
19550
- if (!token)
19551
- fail(
19552
- "login_required",
19553
- "The selected connection is not authenticated.",
19554
- CLI_OPERATIONS.has(name) ? `Run agw account login, or set ${MANAGEMENT_KEY_ENV}.` : `Run agw account login or agw deployment connect, or set ${MANAGEMENT_KEY_ENV}.`,
19555
- 4
19556
- );
19557
- return token;
19670
+ call(name, options = {}) {
19671
+ return this.client.call(name, options);
19558
19672
  }
19559
19673
  /**
19560
19674
  * The record for one operation: the one an identical earlier command left,
@@ -21622,10 +21736,7 @@ function app(command, result, context, style2) {
21622
21736
  ],
21623
21737
  style2
21624
21738
  ),
21625
- ...notes([
21626
- ...result.validation.remote ? [] : result.validation.skipped,
21627
- ...result.limitations
21628
- ])
21739
+ ...notes(result.limitations)
21629
21740
  ];
21630
21741
  if ("definition" in result) {
21631
21742
  const dryRun = "dryRun" in result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maxceem/agw",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "Manage App AI Gateway from a terminal or coding agent: providers, iOS and server apps, usage, and self-hosted deployment on Cloudflare",
6
6
  "keywords": [
@@ -25,7 +25,7 @@
25
25
  "directory": "cli"
26
26
  },
27
27
  "bugs": "https://github.com/maxceem/app-ai-gateway/issues",
28
- "upgradeFrom": ["0.5.0", "0.4.0"],
28
+ "upgradeFrom": ["0.5.2", "0.5.1", "0.5.0", "0.4.0"],
29
29
  "license": "Apache-2.0",
30
30
  "bin": {
31
31
  "agw": "dist/agw.mjs"