@cleocode/contracts 2026.6.13 → 2026.6.14

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 (128) hide show
  1. package/dist/__tests__/operation-def.test.js +4 -1
  2. package/dist/__tests__/operation-def.test.js.map +1 -1
  3. package/dist/__tests__/provider-def.test.d.ts +12 -0
  4. package/dist/__tests__/provider-def.test.d.ts.map +1 -0
  5. package/dist/__tests__/provider-def.test.js +84 -0
  6. package/dist/__tests__/provider-def.test.js.map +1 -0
  7. package/dist/__tests__/service-provider.test.d.ts +17 -0
  8. package/dist/__tests__/service-provider.test.d.ts.map +1 -0
  9. package/dist/__tests__/service-provider.test.js +108 -0
  10. package/dist/__tests__/service-provider.test.js.map +1 -0
  11. package/dist/boundary.d.ts +52 -0
  12. package/dist/boundary.d.ts.map +1 -1
  13. package/dist/boundary.js +23 -0
  14. package/dist/boundary.js.map +1 -1
  15. package/dist/conduit.d.ts +171 -0
  16. package/dist/conduit.d.ts.map +1 -1
  17. package/dist/config/index.d.ts +23 -0
  18. package/dist/config/index.d.ts.map +1 -0
  19. package/dist/config/index.js +22 -0
  20. package/dist/config/index.js.map +1 -0
  21. package/dist/config.d.ts +2 -1
  22. package/dist/config.d.ts.map +1 -1
  23. package/dist/dispatch/__tests__/tasks-gateway-crud-ops.test.d.ts +21 -0
  24. package/dist/dispatch/__tests__/tasks-gateway-crud-ops.test.d.ts.map +1 -0
  25. package/dist/dispatch/__tests__/tasks-gateway-crud-ops.test.js +84 -0
  26. package/dist/dispatch/__tests__/tasks-gateway-crud-ops.test.js.map +1 -0
  27. package/dist/dispatch/identity.d.ts +1 -1
  28. package/dist/dispatch/identity.d.ts.map +1 -1
  29. package/dist/dispatch/identity.js +20 -0
  30. package/dist/dispatch/identity.js.map +1 -1
  31. package/dist/dispatch/operation-def.d.ts +20 -0
  32. package/dist/dispatch/operation-def.d.ts.map +1 -1
  33. package/dist/dispatch/operations-registry.d.ts.map +1 -1
  34. package/dist/dispatch/operations-registry.js +1030 -46
  35. package/dist/dispatch/operations-registry.js.map +1 -1
  36. package/dist/index.d.ts +15 -6
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +15 -2
  39. package/dist/index.js.map +1 -1
  40. package/dist/lease-ipc/__tests__/freeze.test.js +92 -0
  41. package/dist/lease-ipc/__tests__/freeze.test.js.map +1 -1
  42. package/dist/lease-ipc/index.d.ts +2 -2
  43. package/dist/lease-ipc/index.d.ts.map +1 -1
  44. package/dist/lease-ipc/index.js +1 -1
  45. package/dist/lease-ipc/index.js.map +1 -1
  46. package/dist/lease-ipc/messages.d.ts +167 -3
  47. package/dist/lease-ipc/messages.d.ts.map +1 -1
  48. package/dist/lease-ipc/messages.js +93 -0
  49. package/dist/lease-ipc/messages.js.map +1 -1
  50. package/dist/llm/catalog-schema.d.ts +242 -0
  51. package/dist/llm/catalog-schema.d.ts.map +1 -0
  52. package/dist/llm/catalog-schema.js +153 -0
  53. package/dist/llm/catalog-schema.js.map +1 -0
  54. package/dist/llm/catalog-schema.json +139 -0
  55. package/dist/llm/index.d.ts +15 -0
  56. package/dist/llm/index.d.ts.map +1 -0
  57. package/dist/llm/index.js +14 -0
  58. package/dist/llm/index.js.map +1 -0
  59. package/dist/llm/oauth.d.ts +16 -2
  60. package/dist/llm/oauth.d.ts.map +1 -1
  61. package/dist/llm/onboarding.d.ts +126 -0
  62. package/dist/llm/onboarding.d.ts.map +1 -0
  63. package/dist/llm/onboarding.js +34 -0
  64. package/dist/llm/onboarding.js.map +1 -0
  65. package/dist/llm/provider-def.d.ts +254 -0
  66. package/dist/llm/provider-def.d.ts.map +1 -0
  67. package/dist/llm/provider-def.js +93 -0
  68. package/dist/llm/provider-def.js.map +1 -0
  69. package/dist/llm/system-resolver.d.ts +42 -0
  70. package/dist/llm/system-resolver.d.ts.map +1 -1
  71. package/dist/operations/admin.d.ts +124 -1
  72. package/dist/operations/admin.d.ts.map +1 -1
  73. package/dist/operations/entities.d.ts +110 -0
  74. package/dist/operations/entities.d.ts.map +1 -0
  75. package/dist/operations/entities.js +695 -0
  76. package/dist/operations/entities.js.map +1 -0
  77. package/dist/operations/llm.d.ts +15 -0
  78. package/dist/operations/llm.d.ts.map +1 -1
  79. package/dist/operations/output-contracts-data.d.ts.map +1 -1
  80. package/dist/operations/output-contracts-data.js +230 -0
  81. package/dist/operations/output-contracts-data.js.map +1 -1
  82. package/dist/operations/service.d.ts +68 -0
  83. package/dist/operations/service.d.ts.map +1 -0
  84. package/dist/operations/service.js +336 -0
  85. package/dist/operations/service.js.map +1 -0
  86. package/dist/operations/tasks.d.ts +109 -0
  87. package/dist/operations/tasks.d.ts.map +1 -1
  88. package/dist/operations/tasks.js.map +1 -1
  89. package/dist/playbook.d.ts +56 -0
  90. package/dist/playbook.d.ts.map +1 -1
  91. package/dist/release/plan.d.ts +5 -5
  92. package/dist/vault/service-provider.d.ts +343 -0
  93. package/dist/vault/service-provider.d.ts.map +1 -0
  94. package/dist/vault/service-provider.js +857 -0
  95. package/dist/vault/service-provider.js.map +1 -0
  96. package/dist/workgraph.d.ts +12 -12
  97. package/package.json +10 -2
  98. package/src/__tests__/__snapshots__/operations-registry.test.ts.snap +1 -1
  99. package/src/__tests__/operation-def.test.ts +6 -1
  100. package/src/__tests__/provider-def.test.ts +93 -0
  101. package/src/__tests__/service-provider.test.ts +129 -0
  102. package/src/boundary.ts +69 -0
  103. package/src/conduit.ts +200 -0
  104. package/src/config/index.ts +35 -0
  105. package/src/config.ts +2 -1
  106. package/src/dispatch/__tests__/tasks-gateway-crud-ops.test.ts +99 -0
  107. package/src/dispatch/identity.ts +20 -0
  108. package/src/dispatch/operation-def.ts +21 -0
  109. package/src/dispatch/operations-registry.ts +1062 -36
  110. package/src/index.ts +137 -1
  111. package/src/lease-ipc/__tests__/freeze.test.ts +96 -0
  112. package/src/lease-ipc/index.ts +12 -0
  113. package/src/lease-ipc/messages.ts +117 -0
  114. package/src/llm/catalog-schema.json +139 -0
  115. package/src/llm/catalog-schema.ts +184 -0
  116. package/src/llm/index.ts +29 -0
  117. package/src/llm/oauth.ts +16 -2
  118. package/src/llm/onboarding.ts +131 -0
  119. package/src/llm/provider-def.ts +291 -0
  120. package/src/llm/system-resolver.ts +45 -0
  121. package/src/operations/admin.ts +144 -0
  122. package/src/operations/entities.ts +758 -0
  123. package/src/operations/llm.ts +15 -0
  124. package/src/operations/output-contracts-data.ts +264 -0
  125. package/src/operations/service.ts +360 -0
  126. package/src/operations/tasks.ts +119 -0
  127. package/src/playbook.ts +56 -0
  128. package/src/vault/service-provider.ts +1137 -0
@@ -0,0 +1,1137 @@
1
+ /**
2
+ * Universal service-vault — declarative service-provider registry (types + DATA).
3
+ *
4
+ * EP-UNIVERSAL-SERVICE-VAULT (epic T11765 · saga SG-VAULT-CORE T10409 · M2 W1a ·
5
+ * task T11937). The **service** half of the universal vault: machine-wide OAuth /
6
+ * API credentials for third-party SERVICES (github, google, notion, figma, …) —
7
+ * distinct from the LLM-provider credential pool (`accounts`, T11709) which holds
8
+ * model-API keys. A {@link ServiceProviderDef} is a purely declarative description
9
+ * of one connectable service: its stable key, display label, auth shape, and the
10
+ * default OAuth endpoints/scopes the (later) OAuth dance (T11939) will drive.
11
+ *
12
+ * ## Why this lives in `@cleocode/contracts` (Gate 10 purity)
13
+ *
14
+ * This module is **types + frozen DATA only**. {@link SERVICE_PROVIDERS} is a
15
+ * `const` record literal (no bodied function, no zod schema, no type guard) — it
16
+ * is configuration data, exactly like a routing table, so it satisfies the
17
+ * contracts-purity gate (`lint-no-runtime-in-contracts`). The runtime that
18
+ * CONSUMES the registry (the store, the trust gate, the OAuth flow) lives in
19
+ * `@cleocode/core`; this file never imports from core and carries no side effects.
20
+ *
21
+ * ## Pattern, not census (T11937 vs T11938)
22
+ *
23
+ * Only **github** and **google** are seeded here, deliberately — they prove the
24
+ * declarative pattern (one PKCE provider with a fixed loopback, one classic
25
+ * web-OAuth provider) before the fan-out to ~40 services in the follow-up
26
+ * (T11938). Adding a service is a single DATA entry; no code change.
27
+ *
28
+ * @module contracts/vault/service-provider
29
+ * @task T11937
30
+ * @epic T11765
31
+ * @saga T10409
32
+ * @see ../llm/oauth.js — `ProviderOAuthConfig` / `OAuthMode` (reused here)
33
+ * @see ../llm/sealed-credential.js — `SealedCredential` (the egress handle a resolved service credential is wrapped in)
34
+ */
35
+
36
+ import type { ProviderOAuthConfig } from '../llm/oauth.js';
37
+
38
+ /**
39
+ * Auth shape a service uses to obtain credentials.
40
+ *
41
+ * - `oauth2` — classic OAuth 2.0 Authorization Code grant (e.g. github web flow).
42
+ * - `oauth2-pkce` — OAuth 2.0 Authorization Code + PKCE (RFC 7636), for CLI /
43
+ * headless flows (e.g. google installed-app flow).
44
+ * - `api-key` — a long-lived API token pasted/imported directly (no OAuth dance).
45
+ *
46
+ * The value drives which fields of {@link ServiceProviderDef.oauth} are required
47
+ * and which OAuth code path (T11939) runs for the service.
48
+ *
49
+ * @task T11937
50
+ */
51
+ export const SERVICE_AUTH_KINDS = ['oauth2', 'oauth2-pkce', 'api-key'] as const;
52
+
53
+ /** TypeScript union derived from {@link SERVICE_AUTH_KINDS}. */
54
+ export type ServiceAuthKind = (typeof SERVICE_AUTH_KINDS)[number];
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Refresh configuration (T11938 BREADTH · T11939 W2 — driven by this DATA)
58
+ // ---------------------------------------------------------------------------
59
+
60
+ /**
61
+ * Discriminant naming the refresh GRANT a provider uses (T11938 AC3).
62
+ *
63
+ * The OAuth flow (T11939) branches on this value to pick the refresh code path:
64
+ *
65
+ * - `refresh-token` — the standard OAuth 2.0 `grant_type=refresh_token` exchange
66
+ * (RFC 6749 §6), shaped by {@link RefreshConfig.bodyFormat} /
67
+ * {@link RefreshConfig.clientAuth}. The common case (google, atlassian, …).
68
+ * - `github-app` — a GitHub App installation token: sign an RS256 JWT with the
69
+ * app private key, then POST `/app/installations/{id}/access_tokens`.
70
+ * - `service-account-jwt` — a Google service-account: sign an RS256 JWT and
71
+ * exchange it via `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer`.
72
+ * - `client-credentials` — the OAuth 2.0 `grant_type=client_credentials` grant
73
+ * (RFC 6749 §4.4), Basic-auth client id/secret (e.g. MongoDB Atlas SAs).
74
+ *
75
+ * @task T11938
76
+ */
77
+ export const REFRESH_KINDS = [
78
+ 'refresh-token',
79
+ 'github-app',
80
+ 'service-account-jwt',
81
+ 'client-credentials',
82
+ ] as const;
83
+
84
+ /** TypeScript union derived from {@link REFRESH_KINDS}. */
85
+ export type RefreshKind = (typeof REFRESH_KINDS)[number];
86
+
87
+ /**
88
+ * Body encoding for a `refresh-token` token-endpoint POST.
89
+ *
90
+ * - `form` — `application/x-www-form-urlencoded` (OAuth 2.0 default; google).
91
+ * - `json` — `application/json` (required by Atlassian / Notion).
92
+ *
93
+ * @task T11938
94
+ */
95
+ export type RefreshBodyFormat = 'form' | 'json';
96
+
97
+ /**
98
+ * How the OAuth client credentials are presented on a `refresh-token` POST.
99
+ *
100
+ * - `body` — `client_id` + `client_secret` carried IN the request body (default).
101
+ * - `basic-auth` — `Authorization: Basic base64(client_id:client_secret)` header
102
+ * (Notion, Supabase).
103
+ *
104
+ * @task T11938
105
+ */
106
+ export type RefreshClientAuth = 'body' | 'basic-auth';
107
+
108
+ /**
109
+ * Declarative refresh configuration for a `refresh-token` provider (T11938 AC1).
110
+ *
111
+ * Pure data: the token endpoint, the body encoding, and how the client
112
+ * credentials are presented. The OAuth flow (T11939) reads this to drive the
113
+ * `grant_type=refresh_token` exchange WITHOUT any per-provider code. The
114
+ * `kind` discriminant selects the broader grant (the three special variants
115
+ * — github-app / service-account-jwt / client-credentials — set `kind` and
116
+ * leave `bodyFormat`/`clientAuth` at their `form`/`body` defaults; their grant
117
+ * mechanics are not refresh-token-shaped).
118
+ *
119
+ * Cannibalized from onecli `apps.rs::RefreshConfig` (DATA only — the Rust
120
+ * `reqwest`/env-var resolution is re-implemented in TS in T11939).
121
+ *
122
+ * @task T11938
123
+ */
124
+ export interface RefreshConfig {
125
+ /** Which refresh GRANT this provider uses ({@link RefreshKind}). */
126
+ readonly kind: RefreshKind;
127
+ /**
128
+ * Token endpoint the refresh exchange POSTs to. For `refresh-token` /
129
+ * `client-credentials`; the JWT variants use a fixed provider endpoint and may
130
+ * leave this as the canonical value for documentation.
131
+ */
132
+ readonly tokenUrl: string;
133
+ /** Body encoding for a `refresh-token` exchange. Defaults to `form`. */
134
+ readonly bodyFormat?: RefreshBodyFormat;
135
+ /** Client-credential presentation for a `refresh-token` exchange. Defaults to `body`. */
136
+ readonly clientAuth?: RefreshClientAuth;
137
+ }
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Host rules + header injection (T11938 BREADTH — declarative reach metadata)
141
+ // ---------------------------------------------------------------------------
142
+
143
+ /**
144
+ * Auth strategy a host rule applies when a resolved credential is injected into
145
+ * an outbound request to a matched host.
146
+ *
147
+ * - `bearer` — `Authorization: Bearer <token>` (the OAuth default).
148
+ * - `basic-x-access-token` — `Authorization: Basic base64("x-access-token:<token>")`
149
+ * (GitHub git-over-HTTPS).
150
+ * - `header` — the token is injected via a named header rather than `Authorization`
151
+ * (paired with {@link ServiceProviderDef.credentialHeaders}).
152
+ *
153
+ * Declarative only in this milestone — the injecting proxy is a later epic.
154
+ *
155
+ * @task T11938
156
+ */
157
+ export type HostAuthStrategy = 'bearer' | 'basic-x-access-token' | 'header';
158
+
159
+ /**
160
+ * One host-match rule declaring which upstream hosts a provider's credential is
161
+ * valid for and how it is injected (T11938 AC1/AC4 — every provider declares a
162
+ * non-empty {@link ServiceProviderDef.hostRules}).
163
+ *
164
+ * `host` is matched EXACTLY (no wildcards in this milestone). `pathPrefix`, when
165
+ * set, narrows the rule to requests under that path (e.g. the legacy
166
+ * `www.googleapis.com/gmail/` endpoint). Cannibalized from onecli
167
+ * `apps.rs::HostRule` (DATA only).
168
+ *
169
+ * @task T11938
170
+ */
171
+ export interface ServiceHostRule {
172
+ /** Exact upstream host this rule matches (e.g. `'api.github.com'`). */
173
+ readonly host: string;
174
+ /** Optional path prefix narrowing the rule (e.g. `'/gmail/'`). */
175
+ readonly pathPrefix?: string;
176
+ /** How the credential is injected for a matched request. */
177
+ readonly strategy: HostAuthStrategy;
178
+ }
179
+
180
+ /**
181
+ * One declarative injection action applied to an outbound request when a resolved
182
+ * service credential is injected at the harness tool boundary (T11940 · M2-W3).
183
+ *
184
+ * A discriminated union over the action `kind`. The injector
185
+ * ({@link import('@cleocode/core').injectServiceCredentials}) materializes the
186
+ * sealed credential's plaintext ONLY at the wire (inside the action's value
187
+ * resolver) and applies these mutations to a request descriptor — there is NO MITM
188
+ * proxy (the seam is the in-process tool boundary, T11940 AC4):
189
+ *
190
+ * - `set-header` — set `name` to the materialized value (overwriting any existing).
191
+ * - `replace-header` — set `name` to the value ONLY if the header is already
192
+ * present (a no-op when absent — used to swap a placeholder the tool emitted).
193
+ * - `remove-header` — delete `name` (e.g. strip a stale `Authorization` before
194
+ * re-injecting the vault credential).
195
+ * - `set-param` — set the query-string parameter `name` to the materialized value.
196
+ *
197
+ * The `valueSource` discriminates WHERE the injected value comes from:
198
+ * `'token'` (the decrypted access token), `'credential-field'` (a named field of
199
+ * the credential blob, e.g. an AWS access-key id), or `'metadata'` (a non-secret
200
+ * connection-metadata key, e.g. a Google project id). `remove-header` carries no
201
+ * `valueSource` (it injects nothing).
202
+ *
203
+ * @task T11940
204
+ */
205
+ export type InjectionRule =
206
+ | {
207
+ /** Set a header to the injected value (overwrites any existing). */
208
+ readonly kind: 'set-header';
209
+ /** The header name to set. */
210
+ readonly name: string;
211
+ /** Where the injected value comes from. */
212
+ readonly valueSource: InjectionValueSource;
213
+ /** For `bearer`/`basic-x-access-token` token framing — how to wrap the value. */
214
+ readonly framing?: HostAuthStrategy;
215
+ }
216
+ | {
217
+ /** Set a header ONLY when it is already present (swap a placeholder). */
218
+ readonly kind: 'replace-header';
219
+ /** The header name to replace. */
220
+ readonly name: string;
221
+ /** Where the injected value comes from. */
222
+ readonly valueSource: InjectionValueSource;
223
+ /** For `bearer`/`basic-x-access-token` token framing — how to wrap the value. */
224
+ readonly framing?: HostAuthStrategy;
225
+ }
226
+ | {
227
+ /** Remove a header (injects no value). */
228
+ readonly kind: 'remove-header';
229
+ /** The header name to remove. */
230
+ readonly name: string;
231
+ }
232
+ | {
233
+ /** Set a query-string parameter to the injected value. */
234
+ readonly kind: 'set-param';
235
+ /** The query-parameter name to set. */
236
+ readonly name: string;
237
+ /** Where the injected value comes from. */
238
+ readonly valueSource: InjectionValueSource;
239
+ };
240
+
241
+ /**
242
+ * Where an {@link InjectionRule}'s injected value is sourced from.
243
+ *
244
+ * - `token` — the decrypted access token (the secret materialized at the wire).
245
+ * - `credential-field` — a named field of the credential blob (paired with the
246
+ * rule's `name` resolution via {@link ServiceProviderDef.credentialHeaders}).
247
+ * - `metadata` — a non-secret connection-metadata key.
248
+ *
249
+ * @task T11940
250
+ */
251
+ export type InjectionValueSource = 'token' | 'credential-field' | 'metadata';
252
+
253
+ /**
254
+ * Inject an HTTP header from a credential-blob field (e.g. a regional API key →
255
+ * `DD-API-KEY`). Declarative metadata; the injecting proxy is a later epic.
256
+ *
257
+ * @task T11938
258
+ */
259
+ export interface CredentialHeaderRule {
260
+ /** The credential-blob field whose value is injected. */
261
+ readonly credentialField: string;
262
+ /** The HTTP header the value is injected as. */
263
+ readonly headerName: string;
264
+ }
265
+
266
+ /**
267
+ * Inject an HTTP header from a connection-metadata key (e.g. a project id →
268
+ * `x-goog-user-project`). Declarative metadata; the injecting proxy is a later
269
+ * epic.
270
+ *
271
+ * @task T11938
272
+ */
273
+ export interface MetadataHeaderRule {
274
+ /** The non-secret metadata key whose value is injected. */
275
+ readonly metadataKey: string;
276
+ /** The HTTP header the value is injected as. */
277
+ readonly headerName: string;
278
+ }
279
+
280
+ /**
281
+ * Declarative description of one connectable third-party service.
282
+ *
283
+ * A `ServiceProviderDef` is pure configuration: it names a service, describes how
284
+ * it authenticates, and (for OAuth services) carries the default endpoints and
285
+ * scopes the OAuth flow will drive. It holds NO secret material — a user's actual
286
+ * tokens live encrypted in the `service_connections` table; BYOC client secrets
287
+ * (when the user brings their own OAuth app) live encrypted in `service_configs`.
288
+ *
289
+ * @task T11937
290
+ */
291
+ export interface ServiceProviderDef {
292
+ /**
293
+ * Stable provider key (lowercase, kebab) — the join key against
294
+ * `service_connections.provider` / `service_configs.provider`. NEVER renamed
295
+ * once shipped (it is persisted in user rows). e.g. `'github'`, `'google'`.
296
+ */
297
+ readonly provider: string;
298
+ /** Human-readable display name for UI / CLI output. e.g. `'GitHub'`. */
299
+ readonly displayName: string;
300
+ /** Auth shape this service uses ({@link ServiceAuthKind}). */
301
+ readonly authKind: ServiceAuthKind;
302
+ /**
303
+ * Default OAuth configuration (endpoints + scopes + the public client id of
304
+ * CLEO's first-party OAuth app). `undefined` for an `api-key`-only service.
305
+ *
306
+ * Reuses the LLM-layer {@link ProviderOAuthConfig} shape verbatim — the OAuth
307
+ * mechanics (PKCE vs classic, endpoints, redirect, extra params) are identical
308
+ * whether the credential is for a model API or a service API. When the user
309
+ * supplies their own client id/secret (BYOC, stored in `service_configs`),
310
+ * those OVERRIDE the `clientId`/`scope` declared here at flow time (T11939).
311
+ */
312
+ readonly oauth?: ProviderOAuthConfig;
313
+ /**
314
+ * Default space-separated scopes requested when CLEO's first-party app is used
315
+ * and `oauth.scope` is not overridden by a BYOC `service_configs.settings`.
316
+ * Mirrors `oauth.scope`; kept as a top-level field so an `api-key` service can
317
+ * still document the access level a token is expected to carry.
318
+ */
319
+ readonly defaultScopes?: string;
320
+ /**
321
+ * One-line description of what connecting this service enables. Surfaced in the
322
+ * connect UI / `cleo service list`. Non-secret.
323
+ */
324
+ readonly description: string;
325
+ /**
326
+ * Non-empty list of host-match rules (T11938 AC1/AC4) declaring which upstream
327
+ * hosts this provider's resolved credential is valid for and how it is injected
328
+ * ({@link ServiceHostRule}). The registry-completeness test asserts EVERY
329
+ * provider carries at least one rule. Cannibalized from onecli
330
+ * `apps.rs::AppProvider.host_rules` (DATA only).
331
+ */
332
+ readonly hostRules: readonly ServiceHostRule[];
333
+ /**
334
+ * Refresh configuration (T11938 AC1/AC3). Present for every `oauth2` /
335
+ * `oauth2-pkce` provider that can refresh; `undefined` for an `api-key` service
336
+ * (no token to refresh) or an OAuth service that only re-authorizes. The OAuth
337
+ * flow (T11939) reads {@link RefreshConfig.kind} to pick the refresh grant.
338
+ */
339
+ readonly refresh?: RefreshConfig;
340
+ /**
341
+ * Optional header-injection rules sourced from the credential blob (T11938 AC1).
342
+ * Declarative reach metadata; the injecting proxy is a later epic.
343
+ */
344
+ readonly credentialHeaders?: readonly CredentialHeaderRule[];
345
+ /**
346
+ * Optional header-injection rules sourced from connection metadata (T11938 AC1).
347
+ * Declarative reach metadata; the injecting proxy is a later epic.
348
+ */
349
+ readonly metadataHeaders?: readonly MetadataHeaderRule[];
350
+ }
351
+
352
+ // ---------------------------------------------------------------------------
353
+ // Shared refresh configs (T11938) — frozen DATA reused across provider families.
354
+ // Mirror onecli `apps.rs` statics (GOOGLE_REFRESH, ATLASSIAN_REFRESH, …). These
355
+ // are `const` object literals (no runtime helper) so contracts-purity (Gate 10)
356
+ // stays green.
357
+ // ---------------------------------------------------------------------------
358
+
359
+ /** Shared refresh config for every Google OAuth API (form body, client-id/secret in body). */
360
+ const GOOGLE_REFRESH: RefreshConfig = {
361
+ kind: 'refresh-token',
362
+ tokenUrl: 'https://oauth2.googleapis.com/token',
363
+ bodyFormat: 'form',
364
+ clientAuth: 'body',
365
+ };
366
+
367
+ /** Shared refresh config for the Atlassian OAuth APIs (Jira, Confluence) — JSON body. */
368
+ const ATLASSIAN_REFRESH: RefreshConfig = {
369
+ kind: 'refresh-token',
370
+ tokenUrl: 'https://auth.atlassian.com/oauth/token',
371
+ bodyFormat: 'json',
372
+ clientAuth: 'body',
373
+ };
374
+
375
+ /**
376
+ * The full service-provider census — **~40 declarative entries** (T11938 BREADTH).
377
+ *
378
+ * Spread into {@link SERVICE_PROVIDERS} after the github/google seeds. Each entry
379
+ * is PURE DATA cannibalized from onecli `apps.rs::APP_PROVIDERS` (host rules,
380
+ * refresh shape, display name) — no code, no runtime helper. Three special-case
381
+ * refresh GRANTS are declared as {@link RefreshKind} discriminants (T11938 AC3):
382
+ * `github-app` (GitHub App), `client-credentials` (MongoDB Atlas SAs). The OAuth
383
+ * flow (T11939) branches on `refresh.kind` to drive the right grant.
384
+ *
385
+ * Default OAuth `clientId`s are PLACEHOLDER public client ids (BYOC-overridden at
386
+ * flow time from `service_configs`); they are not secrets.
387
+ *
388
+ * @task T11938
389
+ */
390
+ const SERVICE_PROVIDERS_BREADTH: Readonly<Record<string, ServiceProviderDef>> = {
391
+ // ── GitHub App (installation token via signed RS256 JWT — special refresh) ──
392
+ 'github-app': {
393
+ provider: 'github-app',
394
+ displayName: 'GitHub App',
395
+ authKind: 'oauth2',
396
+ description: 'GitHub App installation access — short-lived installation tokens.',
397
+ defaultScopes: '',
398
+ refresh: {
399
+ kind: 'github-app',
400
+ tokenUrl: 'https://api.github.com/app/installations',
401
+ },
402
+ hostRules: [
403
+ { host: 'api.github.com', strategy: 'bearer' },
404
+ { host: 'github.com', strategy: 'basic-x-access-token' },
405
+ { host: 'raw.githubusercontent.com', strategy: 'bearer' },
406
+ ],
407
+ },
408
+ // ── Google API family (all share GOOGLE_REFRESH + the google PKCE oauth) ──
409
+ gmail: {
410
+ provider: 'gmail',
411
+ displayName: 'Gmail',
412
+ authKind: 'oauth2-pkce',
413
+ description: 'Read, search, and send mail via the Gmail API.',
414
+ defaultScopes: 'https://www.googleapis.com/auth/gmail.modify',
415
+ oauth: {
416
+ mode: 'pkce',
417
+ clientId: 'cleo-google-client-id.apps.googleusercontent.com',
418
+ authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
419
+ tokenEndpoint: 'https://oauth2.googleapis.com/token',
420
+ scope: 'https://www.googleapis.com/auth/gmail.modify',
421
+ redirectUri: 'http://127.0.0.1:7878/service/gmail/callback',
422
+ extraAuthParams: { access_type: 'offline', prompt: 'consent' },
423
+ },
424
+ refresh: GOOGLE_REFRESH,
425
+ hostRules: [
426
+ { host: 'gmail.googleapis.com', strategy: 'bearer' },
427
+ { host: 'www.googleapis.com', pathPrefix: '/gmail/', strategy: 'bearer' },
428
+ ],
429
+ },
430
+ 'google-calendar': {
431
+ provider: 'google-calendar',
432
+ displayName: 'Google Calendar',
433
+ authKind: 'oauth2-pkce',
434
+ description: 'Manage calendars and events via the Google Calendar API.',
435
+ defaultScopes: 'https://www.googleapis.com/auth/calendar',
436
+ oauth: {
437
+ mode: 'pkce',
438
+ clientId: 'cleo-google-client-id.apps.googleusercontent.com',
439
+ authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
440
+ tokenEndpoint: 'https://oauth2.googleapis.com/token',
441
+ scope: 'https://www.googleapis.com/auth/calendar',
442
+ redirectUri: 'http://127.0.0.1:7878/service/google-calendar/callback',
443
+ extraAuthParams: { access_type: 'offline', prompt: 'consent' },
444
+ },
445
+ refresh: GOOGLE_REFRESH,
446
+ hostRules: [{ host: 'www.googleapis.com', pathPrefix: '/calendar/', strategy: 'bearer' }],
447
+ },
448
+ 'google-drive': {
449
+ provider: 'google-drive',
450
+ displayName: 'Google Drive',
451
+ authKind: 'oauth2-pkce',
452
+ description: 'List, read, and write files via the Google Drive API.',
453
+ defaultScopes: 'https://www.googleapis.com/auth/drive',
454
+ oauth: {
455
+ mode: 'pkce',
456
+ clientId: 'cleo-google-client-id.apps.googleusercontent.com',
457
+ authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
458
+ tokenEndpoint: 'https://oauth2.googleapis.com/token',
459
+ scope: 'https://www.googleapis.com/auth/drive',
460
+ redirectUri: 'http://127.0.0.1:7878/service/google-drive/callback',
461
+ extraAuthParams: { access_type: 'offline', prompt: 'consent' },
462
+ },
463
+ refresh: GOOGLE_REFRESH,
464
+ hostRules: [
465
+ { host: 'www.googleapis.com', pathPrefix: '/drive/', strategy: 'bearer' },
466
+ { host: 'www.googleapis.com', pathPrefix: '/upload/drive/', strategy: 'bearer' },
467
+ ],
468
+ },
469
+ 'google-docs': {
470
+ provider: 'google-docs',
471
+ displayName: 'Google Docs',
472
+ authKind: 'oauth2-pkce',
473
+ description: 'Read and edit documents via the Google Docs API.',
474
+ defaultScopes: 'https://www.googleapis.com/auth/documents',
475
+ oauth: {
476
+ mode: 'pkce',
477
+ clientId: 'cleo-google-client-id.apps.googleusercontent.com',
478
+ authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
479
+ tokenEndpoint: 'https://oauth2.googleapis.com/token',
480
+ scope: 'https://www.googleapis.com/auth/documents',
481
+ redirectUri: 'http://127.0.0.1:7878/service/google-docs/callback',
482
+ extraAuthParams: { access_type: 'offline', prompt: 'consent' },
483
+ },
484
+ refresh: GOOGLE_REFRESH,
485
+ hostRules: [{ host: 'docs.googleapis.com', strategy: 'bearer' }],
486
+ },
487
+ 'google-sheets': {
488
+ provider: 'google-sheets',
489
+ displayName: 'Google Sheets',
490
+ authKind: 'oauth2-pkce',
491
+ description: 'Read and write spreadsheets via the Google Sheets API.',
492
+ defaultScopes: 'https://www.googleapis.com/auth/spreadsheets',
493
+ oauth: {
494
+ mode: 'pkce',
495
+ clientId: 'cleo-google-client-id.apps.googleusercontent.com',
496
+ authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
497
+ tokenEndpoint: 'https://oauth2.googleapis.com/token',
498
+ scope: 'https://www.googleapis.com/auth/spreadsheets',
499
+ redirectUri: 'http://127.0.0.1:7878/service/google-sheets/callback',
500
+ extraAuthParams: { access_type: 'offline', prompt: 'consent' },
501
+ },
502
+ refresh: GOOGLE_REFRESH,
503
+ hostRules: [{ host: 'sheets.googleapis.com', strategy: 'bearer' }],
504
+ },
505
+ 'google-slides': {
506
+ provider: 'google-slides',
507
+ displayName: 'Google Slides',
508
+ authKind: 'oauth2-pkce',
509
+ description: 'Read and edit presentations via the Google Slides API.',
510
+ defaultScopes: 'https://www.googleapis.com/auth/presentations',
511
+ oauth: {
512
+ mode: 'pkce',
513
+ clientId: 'cleo-google-client-id.apps.googleusercontent.com',
514
+ authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
515
+ tokenEndpoint: 'https://oauth2.googleapis.com/token',
516
+ scope: 'https://www.googleapis.com/auth/presentations',
517
+ redirectUri: 'http://127.0.0.1:7878/service/google-slides/callback',
518
+ extraAuthParams: { access_type: 'offline', prompt: 'consent' },
519
+ },
520
+ refresh: GOOGLE_REFRESH,
521
+ hostRules: [{ host: 'slides.googleapis.com', strategy: 'bearer' }],
522
+ },
523
+ 'google-tasks': {
524
+ provider: 'google-tasks',
525
+ displayName: 'Google Tasks',
526
+ authKind: 'oauth2-pkce',
527
+ description: 'Manage task lists via the Google Tasks API.',
528
+ defaultScopes: 'https://www.googleapis.com/auth/tasks',
529
+ oauth: {
530
+ mode: 'pkce',
531
+ clientId: 'cleo-google-client-id.apps.googleusercontent.com',
532
+ authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
533
+ tokenEndpoint: 'https://oauth2.googleapis.com/token',
534
+ scope: 'https://www.googleapis.com/auth/tasks',
535
+ redirectUri: 'http://127.0.0.1:7878/service/google-tasks/callback',
536
+ extraAuthParams: { access_type: 'offline', prompt: 'consent' },
537
+ },
538
+ refresh: GOOGLE_REFRESH,
539
+ hostRules: [{ host: 'tasks.googleapis.com', strategy: 'bearer' }],
540
+ },
541
+ 'google-forms': {
542
+ provider: 'google-forms',
543
+ displayName: 'Google Forms',
544
+ authKind: 'oauth2-pkce',
545
+ description: 'Read forms and responses via the Google Forms API.',
546
+ defaultScopes: 'https://www.googleapis.com/auth/forms.body',
547
+ oauth: {
548
+ mode: 'pkce',
549
+ clientId: 'cleo-google-client-id.apps.googleusercontent.com',
550
+ authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
551
+ tokenEndpoint: 'https://oauth2.googleapis.com/token',
552
+ scope: 'https://www.googleapis.com/auth/forms.body',
553
+ redirectUri: 'http://127.0.0.1:7878/service/google-forms/callback',
554
+ extraAuthParams: { access_type: 'offline', prompt: 'consent' },
555
+ },
556
+ refresh: GOOGLE_REFRESH,
557
+ hostRules: [{ host: 'forms.googleapis.com', strategy: 'bearer' }],
558
+ },
559
+ 'google-classroom': {
560
+ provider: 'google-classroom',
561
+ displayName: 'Google Classroom',
562
+ authKind: 'oauth2-pkce',
563
+ description: 'Manage courses and coursework via the Google Classroom API.',
564
+ defaultScopes: 'https://www.googleapis.com/auth/classroom.courses',
565
+ oauth: {
566
+ mode: 'pkce',
567
+ clientId: 'cleo-google-client-id.apps.googleusercontent.com',
568
+ authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
569
+ tokenEndpoint: 'https://oauth2.googleapis.com/token',
570
+ scope: 'https://www.googleapis.com/auth/classroom.courses',
571
+ redirectUri: 'http://127.0.0.1:7878/service/google-classroom/callback',
572
+ extraAuthParams: { access_type: 'offline', prompt: 'consent' },
573
+ },
574
+ refresh: GOOGLE_REFRESH,
575
+ hostRules: [{ host: 'classroom.googleapis.com', strategy: 'bearer' }],
576
+ },
577
+ 'google-admin': {
578
+ provider: 'google-admin',
579
+ displayName: 'Google Admin',
580
+ authKind: 'oauth2-pkce',
581
+ description: 'Directory and admin operations via the Google Admin SDK.',
582
+ defaultScopes: 'https://www.googleapis.com/auth/admin.directory.user',
583
+ oauth: {
584
+ mode: 'pkce',
585
+ clientId: 'cleo-google-client-id.apps.googleusercontent.com',
586
+ authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
587
+ tokenEndpoint: 'https://oauth2.googleapis.com/token',
588
+ scope: 'https://www.googleapis.com/auth/admin.directory.user',
589
+ redirectUri: 'http://127.0.0.1:7878/service/google-admin/callback',
590
+ extraAuthParams: { access_type: 'offline', prompt: 'consent' },
591
+ },
592
+ refresh: GOOGLE_REFRESH,
593
+ hostRules: [{ host: 'admin.googleapis.com', strategy: 'bearer' }],
594
+ },
595
+ 'google-analytics': {
596
+ provider: 'google-analytics',
597
+ displayName: 'Google Analytics',
598
+ authKind: 'oauth2-pkce',
599
+ description: 'Query reports via the Google Analytics Data API.',
600
+ defaultScopes: 'https://www.googleapis.com/auth/analytics.readonly',
601
+ oauth: {
602
+ mode: 'pkce',
603
+ clientId: 'cleo-google-client-id.apps.googleusercontent.com',
604
+ authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
605
+ tokenEndpoint: 'https://oauth2.googleapis.com/token',
606
+ scope: 'https://www.googleapis.com/auth/analytics.readonly',
607
+ redirectUri: 'http://127.0.0.1:7878/service/google-analytics/callback',
608
+ extraAuthParams: { access_type: 'offline', prompt: 'consent' },
609
+ },
610
+ refresh: GOOGLE_REFRESH,
611
+ hostRules: [{ host: 'analyticsdata.googleapis.com', strategy: 'bearer' }],
612
+ },
613
+ 'google-search-console': {
614
+ provider: 'google-search-console',
615
+ displayName: 'Google Search Console',
616
+ authKind: 'oauth2-pkce',
617
+ description: 'Query search analytics via the Google Search Console API.',
618
+ defaultScopes: 'https://www.googleapis.com/auth/webmasters.readonly',
619
+ oauth: {
620
+ mode: 'pkce',
621
+ clientId: 'cleo-google-client-id.apps.googleusercontent.com',
622
+ authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
623
+ tokenEndpoint: 'https://oauth2.googleapis.com/token',
624
+ scope: 'https://www.googleapis.com/auth/webmasters.readonly',
625
+ redirectUri: 'http://127.0.0.1:7878/service/google-search-console/callback',
626
+ extraAuthParams: { access_type: 'offline', prompt: 'consent' },
627
+ },
628
+ refresh: GOOGLE_REFRESH,
629
+ hostRules: [
630
+ { host: 'searchconsole.googleapis.com', strategy: 'bearer' },
631
+ { host: 'www.googleapis.com', pathPrefix: '/webmasters/', strategy: 'bearer' },
632
+ ],
633
+ },
634
+ 'google-meet': {
635
+ provider: 'google-meet',
636
+ displayName: 'Google Meet',
637
+ authKind: 'oauth2-pkce',
638
+ description: 'Manage meeting spaces and conference records via the Google Meet API.',
639
+ defaultScopes: 'https://www.googleapis.com/auth/meetings.space.created',
640
+ oauth: {
641
+ mode: 'pkce',
642
+ clientId: 'cleo-google-client-id.apps.googleusercontent.com',
643
+ authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
644
+ tokenEndpoint: 'https://oauth2.googleapis.com/token',
645
+ scope: 'https://www.googleapis.com/auth/meetings.space.created',
646
+ redirectUri: 'http://127.0.0.1:7878/service/google-meet/callback',
647
+ extraAuthParams: { access_type: 'offline', prompt: 'consent' },
648
+ },
649
+ refresh: GOOGLE_REFRESH,
650
+ hostRules: [{ host: 'meet.googleapis.com', strategy: 'bearer' }],
651
+ },
652
+ 'google-photos': {
653
+ provider: 'google-photos',
654
+ displayName: 'Google Photos',
655
+ authKind: 'oauth2-pkce',
656
+ description: 'Read media items and albums via the Google Photos Library API.',
657
+ defaultScopes: 'https://www.googleapis.com/auth/photoslibrary.readonly',
658
+ oauth: {
659
+ mode: 'pkce',
660
+ clientId: 'cleo-google-client-id.apps.googleusercontent.com',
661
+ authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
662
+ tokenEndpoint: 'https://oauth2.googleapis.com/token',
663
+ scope: 'https://www.googleapis.com/auth/photoslibrary.readonly',
664
+ redirectUri: 'http://127.0.0.1:7878/service/google-photos/callback',
665
+ extraAuthParams: { access_type: 'offline', prompt: 'consent' },
666
+ },
667
+ refresh: GOOGLE_REFRESH,
668
+ hostRules: [{ host: 'photoslibrary.googleapis.com', strategy: 'bearer' }],
669
+ },
670
+ youtube: {
671
+ provider: 'youtube',
672
+ displayName: 'YouTube',
673
+ authKind: 'oauth2-pkce',
674
+ description: 'Manage channels, videos, and playlists via the YouTube Data API.',
675
+ defaultScopes: 'https://www.googleapis.com/auth/youtube',
676
+ oauth: {
677
+ mode: 'pkce',
678
+ clientId: 'cleo-google-client-id.apps.googleusercontent.com',
679
+ authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
680
+ tokenEndpoint: 'https://oauth2.googleapis.com/token',
681
+ scope: 'https://www.googleapis.com/auth/youtube',
682
+ redirectUri: 'http://127.0.0.1:7878/service/youtube/callback',
683
+ extraAuthParams: { access_type: 'offline', prompt: 'consent' },
684
+ },
685
+ refresh: GOOGLE_REFRESH,
686
+ hostRules: [{ host: 'www.googleapis.com', pathPrefix: '/youtube/', strategy: 'bearer' }],
687
+ },
688
+ 'vertex-ai': {
689
+ provider: 'vertex-ai',
690
+ displayName: 'Vertex AI',
691
+ authKind: 'oauth2-pkce',
692
+ description: 'Call Google Cloud Vertex AI model endpoints.',
693
+ defaultScopes: 'https://www.googleapis.com/auth/cloud-platform',
694
+ oauth: {
695
+ mode: 'pkce',
696
+ clientId: 'cleo-google-client-id.apps.googleusercontent.com',
697
+ authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
698
+ tokenEndpoint: 'https://oauth2.googleapis.com/token',
699
+ scope: 'https://www.googleapis.com/auth/cloud-platform',
700
+ redirectUri: 'http://127.0.0.1:7878/service/vertex-ai/callback',
701
+ extraAuthParams: { access_type: 'offline', prompt: 'consent' },
702
+ },
703
+ refresh: GOOGLE_REFRESH,
704
+ hostRules: [
705
+ { host: 'aiplatform.googleapis.com', strategy: 'bearer' },
706
+ { host: 'oauth2.googleapis.com', strategy: 'bearer' },
707
+ ],
708
+ metadataHeaders: [{ metadataKey: 'projectId', headerName: 'x-goog-user-project' }],
709
+ },
710
+ // ── Atlassian family (JSON refresh body) ──
711
+ jira: {
712
+ provider: 'jira',
713
+ displayName: 'Jira',
714
+ authKind: 'oauth2',
715
+ description: 'Issues, projects, and workflows via the Jira Cloud REST API.',
716
+ defaultScopes: 'read:jira-work write:jira-work offline_access',
717
+ oauth: {
718
+ mode: 'pkce',
719
+ clientId: 'cleo-atlassian-client-id',
720
+ authorizationEndpoint: 'https://auth.atlassian.com/authorize',
721
+ tokenEndpoint: 'https://auth.atlassian.com/oauth/token',
722
+ scope: 'read:jira-work write:jira-work offline_access',
723
+ redirectUri: 'http://127.0.0.1:7878/service/jira/callback',
724
+ extraAuthParams: { audience: 'api.atlassian.com', prompt: 'consent' },
725
+ },
726
+ refresh: ATLASSIAN_REFRESH,
727
+ hostRules: [{ host: 'api.atlassian.com', strategy: 'bearer' }],
728
+ },
729
+ confluence: {
730
+ provider: 'confluence',
731
+ displayName: 'Confluence',
732
+ authKind: 'oauth2',
733
+ description: 'Spaces, pages, and content via the Confluence Cloud REST API.',
734
+ defaultScopes: 'read:confluence-content.all write:confluence-content offline_access',
735
+ oauth: {
736
+ mode: 'pkce',
737
+ clientId: 'cleo-atlassian-client-id',
738
+ authorizationEndpoint: 'https://auth.atlassian.com/authorize',
739
+ tokenEndpoint: 'https://auth.atlassian.com/oauth/token',
740
+ scope: 'read:confluence-content.all write:confluence-content offline_access',
741
+ redirectUri: 'http://127.0.0.1:7878/service/confluence/callback',
742
+ extraAuthParams: { audience: 'api.atlassian.com', prompt: 'consent' },
743
+ },
744
+ refresh: ATLASSIAN_REFRESH,
745
+ hostRules: [{ host: 'api.atlassian.com', strategy: 'bearer' }],
746
+ },
747
+ // ── Notion (JSON body + Basic-auth client credentials) ──
748
+ notion: {
749
+ provider: 'notion',
750
+ displayName: 'Notion',
751
+ authKind: 'oauth2',
752
+ description: 'Databases, pages, and blocks via the Notion API.',
753
+ defaultScopes: '',
754
+ oauth: {
755
+ mode: 'pkce',
756
+ clientId: 'cleo-notion-client-id',
757
+ authorizationEndpoint: 'https://api.notion.com/v1/oauth/authorize',
758
+ tokenEndpoint: 'https://api.notion.com/v1/oauth/token',
759
+ scope: '',
760
+ redirectUri: 'http://127.0.0.1:7878/service/notion/callback',
761
+ extraAuthParams: { owner: 'user' },
762
+ },
763
+ refresh: {
764
+ kind: 'refresh-token',
765
+ tokenUrl: 'https://api.notion.com/v1/oauth/token',
766
+ bodyFormat: 'json',
767
+ clientAuth: 'basic-auth',
768
+ },
769
+ hostRules: [{ host: 'api.notion.com', strategy: 'bearer' }],
770
+ },
771
+ // ── Dropbox (form body) ──
772
+ dropbox: {
773
+ provider: 'dropbox',
774
+ displayName: 'Dropbox',
775
+ authKind: 'oauth2',
776
+ description: 'Files, folders, and sharing via the Dropbox API.',
777
+ defaultScopes: 'files.content.read files.content.write account_info.read',
778
+ oauth: {
779
+ mode: 'pkce',
780
+ clientId: 'cleo-dropbox-client-id',
781
+ authorizationEndpoint: 'https://www.dropbox.com/oauth2/authorize',
782
+ tokenEndpoint: 'https://api.dropboxapi.com/oauth2/token',
783
+ scope: 'files.content.read files.content.write account_info.read',
784
+ redirectUri: 'http://127.0.0.1:7878/service/dropbox/callback',
785
+ extraAuthParams: { token_access_type: 'offline' },
786
+ },
787
+ refresh: {
788
+ kind: 'refresh-token',
789
+ tokenUrl: 'https://api.dropboxapi.com/oauth2/token',
790
+ bodyFormat: 'form',
791
+ clientAuth: 'body',
792
+ },
793
+ hostRules: [
794
+ { host: 'api.dropboxapi.com', strategy: 'bearer' },
795
+ { host: 'content.dropboxapi.com', strategy: 'bearer' },
796
+ ],
797
+ },
798
+ // ── LinkedIn (form body) ──
799
+ linkedin: {
800
+ provider: 'linkedin',
801
+ displayName: 'LinkedIn',
802
+ authKind: 'oauth2',
803
+ description: 'Profile, posts, and shares via the LinkedIn API.',
804
+ defaultScopes: 'r_liteprofile r_emailaddress w_member_social',
805
+ oauth: {
806
+ mode: 'pkce',
807
+ clientId: 'cleo-linkedin-client-id',
808
+ authorizationEndpoint: 'https://www.linkedin.com/oauth/v2/authorization',
809
+ tokenEndpoint: 'https://www.linkedin.com/oauth/v2/accessToken',
810
+ scope: 'r_liteprofile r_emailaddress w_member_social',
811
+ redirectUri: 'http://127.0.0.1:7878/service/linkedin/callback',
812
+ },
813
+ refresh: {
814
+ kind: 'refresh-token',
815
+ tokenUrl: 'https://www.linkedin.com/oauth/v2/accessToken',
816
+ bodyFormat: 'form',
817
+ clientAuth: 'body',
818
+ },
819
+ hostRules: [{ host: 'api.linkedin.com', strategy: 'bearer' }],
820
+ },
821
+ // ── Todoist (form body) ──
822
+ todoist: {
823
+ provider: 'todoist',
824
+ displayName: 'Todoist',
825
+ authKind: 'oauth2',
826
+ description: 'Tasks, projects, and labels via the Todoist REST API.',
827
+ defaultScopes: 'data:read_write',
828
+ oauth: {
829
+ mode: 'pkce',
830
+ clientId: 'cleo-todoist-client-id',
831
+ authorizationEndpoint: 'https://todoist.com/oauth/authorize',
832
+ tokenEndpoint: 'https://api.todoist.com/oauth/access_token',
833
+ scope: 'data:read_write',
834
+ redirectUri: 'http://127.0.0.1:7878/service/todoist/callback',
835
+ },
836
+ refresh: {
837
+ kind: 'refresh-token',
838
+ tokenUrl: 'https://api.todoist.com/oauth/access_token',
839
+ bodyFormat: 'form',
840
+ clientAuth: 'body',
841
+ },
842
+ hostRules: [{ host: 'api.todoist.com', strategy: 'bearer' }],
843
+ },
844
+ // ── Supabase Management API (form body + Basic-auth client credentials) ──
845
+ supabase: {
846
+ provider: 'supabase',
847
+ displayName: 'Supabase',
848
+ authKind: 'oauth2',
849
+ description: 'Projects and configuration via the Supabase Management API.',
850
+ defaultScopes: 'all',
851
+ oauth: {
852
+ mode: 'pkce',
853
+ clientId: 'cleo-supabase-client-id',
854
+ authorizationEndpoint: 'https://api.supabase.com/v1/oauth/authorize',
855
+ tokenEndpoint: 'https://api.supabase.com/v1/oauth/token',
856
+ scope: 'all',
857
+ redirectUri: 'http://127.0.0.1:7878/service/supabase/callback',
858
+ },
859
+ refresh: {
860
+ kind: 'refresh-token',
861
+ tokenUrl: 'https://api.supabase.com/v1/oauth/token',
862
+ bodyFormat: 'form',
863
+ clientAuth: 'basic-auth',
864
+ },
865
+ hostRules: [{ host: 'api.supabase.com', strategy: 'bearer' }],
866
+ },
867
+ // ── MongoDB Atlas (client_credentials grant — special refresh) ──
868
+ 'mongodb-atlas': {
869
+ provider: 'mongodb-atlas',
870
+ displayName: 'MongoDB Atlas',
871
+ authKind: 'oauth2',
872
+ description: 'Clusters and projects via the MongoDB Atlas Administration API.',
873
+ defaultScopes: '',
874
+ refresh: {
875
+ kind: 'client-credentials',
876
+ tokenUrl: 'https://cloud.mongodb.com/api/oauth/token',
877
+ },
878
+ hostRules: [{ host: 'cloud.mongodb.com', strategy: 'bearer' }],
879
+ },
880
+ // ── monday.com (api-key — no OAuth refresh) ──
881
+ monday: {
882
+ provider: 'monday',
883
+ displayName: 'monday.com',
884
+ authKind: 'oauth2',
885
+ description: 'Boards, items, and updates via the monday.com GraphQL API.',
886
+ defaultScopes: 'me:read boards:read boards:write',
887
+ oauth: {
888
+ mode: 'pkce',
889
+ clientId: 'cleo-monday-client-id',
890
+ authorizationEndpoint: 'https://auth.monday.com/oauth2/authorize',
891
+ tokenEndpoint: 'https://auth.monday.com/oauth2/token',
892
+ scope: 'me:read boards:read boards:write',
893
+ redirectUri: 'http://127.0.0.1:7878/service/monday/callback',
894
+ },
895
+ hostRules: [{ host: 'api.monday.com', strategy: 'bearer' }],
896
+ },
897
+ // ── Resend (api-key) ──
898
+ resend: {
899
+ provider: 'resend',
900
+ displayName: 'Resend',
901
+ authKind: 'api-key',
902
+ description: 'Send transactional email via the Resend API (API-key auth).',
903
+ defaultScopes: 'full_access',
904
+ hostRules: [{ host: 'api.resend.com', strategy: 'bearer' }],
905
+ },
906
+ // ── Cloudflare (api-key) ──
907
+ cloudflare: {
908
+ provider: 'cloudflare',
909
+ displayName: 'Cloudflare',
910
+ authKind: 'api-key',
911
+ description: 'Zones, DNS, and Workers via the Cloudflare API (API-token auth).',
912
+ defaultScopes: '',
913
+ hostRules: [{ host: 'api.cloudflare.com', strategy: 'bearer' }],
914
+ },
915
+ // ── Fly.io (api-key) ──
916
+ flyio: {
917
+ provider: 'flyio',
918
+ displayName: 'Fly.io',
919
+ authKind: 'api-key',
920
+ description: 'Machines and apps via the Fly.io Machines API (API-token auth).',
921
+ defaultScopes: '',
922
+ hostRules: [
923
+ { host: 'api.machines.dev', strategy: 'bearer' },
924
+ { host: 'api.fly.io', strategy: 'bearer' },
925
+ ],
926
+ },
927
+ // ── AWS (api-key/SigV4 — credential headers; no token refresh) ──
928
+ aws: {
929
+ provider: 'aws',
930
+ displayName: 'AWS',
931
+ authKind: 'api-key',
932
+ description: 'AWS service APIs signed with SigV4 (access-key credentials).',
933
+ defaultScopes: '',
934
+ hostRules: [{ host: 'amazonaws.com', strategy: 'header' }],
935
+ credentialHeaders: [{ credentialField: 'accessKeyId', headerName: 'x-amz-access-key-id' }],
936
+ },
937
+ // ── Figma (form body refresh) ──
938
+ figma: {
939
+ provider: 'figma',
940
+ displayName: 'Figma',
941
+ authKind: 'oauth2',
942
+ description: 'Files, projects, and comments via the Figma REST API.',
943
+ defaultScopes: 'file_read',
944
+ oauth: {
945
+ mode: 'pkce',
946
+ clientId: 'cleo-figma-client-id',
947
+ authorizationEndpoint: 'https://www.figma.com/oauth',
948
+ tokenEndpoint: 'https://api.figma.com/v1/oauth/token',
949
+ scope: 'file_read',
950
+ redirectUri: 'http://127.0.0.1:7878/service/figma/callback',
951
+ },
952
+ refresh: {
953
+ kind: 'refresh-token',
954
+ tokenUrl: 'https://api.figma.com/v1/oauth/refresh',
955
+ bodyFormat: 'form',
956
+ clientAuth: 'body',
957
+ },
958
+ hostRules: [{ host: 'api.figma.com', strategy: 'bearer' }],
959
+ },
960
+ // ── Linear (form body refresh) ──
961
+ linear: {
962
+ provider: 'linear',
963
+ displayName: 'Linear',
964
+ authKind: 'oauth2',
965
+ description: 'Issues, projects, and cycles via the Linear GraphQL API.',
966
+ defaultScopes: 'read write',
967
+ oauth: {
968
+ mode: 'pkce',
969
+ clientId: 'cleo-linear-client-id',
970
+ authorizationEndpoint: 'https://linear.app/oauth/authorize',
971
+ tokenEndpoint: 'https://api.linear.app/oauth/token',
972
+ scope: 'read write',
973
+ redirectUri: 'http://127.0.0.1:7878/service/linear/callback',
974
+ },
975
+ refresh: {
976
+ kind: 'refresh-token',
977
+ tokenUrl: 'https://api.linear.app/oauth/token',
978
+ bodyFormat: 'form',
979
+ clientAuth: 'body',
980
+ },
981
+ hostRules: [{ host: 'api.linear.app', strategy: 'bearer' }],
982
+ },
983
+ // ── Slack (form body refresh; token rotation) ──
984
+ slack: {
985
+ provider: 'slack',
986
+ displayName: 'Slack',
987
+ authKind: 'oauth2',
988
+ description: 'Channels, messages, and users via the Slack Web API.',
989
+ defaultScopes: 'channels:read chat:write users:read',
990
+ oauth: {
991
+ mode: 'pkce',
992
+ clientId: 'cleo-slack-client-id',
993
+ authorizationEndpoint: 'https://slack.com/oauth/v2/authorize',
994
+ tokenEndpoint: 'https://slack.com/api/oauth.v2.access',
995
+ scope: 'channels:read chat:write users:read',
996
+ redirectUri: 'http://127.0.0.1:7878/service/slack/callback',
997
+ },
998
+ refresh: {
999
+ kind: 'refresh-token',
1000
+ tokenUrl: 'https://slack.com/api/oauth.v2.access',
1001
+ bodyFormat: 'form',
1002
+ clientAuth: 'body',
1003
+ },
1004
+ hostRules: [{ host: 'slack.com', strategy: 'bearer' }],
1005
+ },
1006
+ // ── Discord (form body refresh) ──
1007
+ discord: {
1008
+ provider: 'discord',
1009
+ displayName: 'Discord',
1010
+ authKind: 'oauth2',
1011
+ description: 'Guilds, channels, and messages via the Discord API.',
1012
+ defaultScopes: 'identify guilds',
1013
+ oauth: {
1014
+ mode: 'pkce',
1015
+ clientId: 'cleo-discord-client-id',
1016
+ authorizationEndpoint: 'https://discord.com/oauth2/authorize',
1017
+ tokenEndpoint: 'https://discord.com/api/oauth2/token',
1018
+ scope: 'identify guilds',
1019
+ redirectUri: 'http://127.0.0.1:7878/service/discord/callback',
1020
+ },
1021
+ refresh: {
1022
+ kind: 'refresh-token',
1023
+ tokenUrl: 'https://discord.com/api/oauth2/token',
1024
+ bodyFormat: 'form',
1025
+ clientAuth: 'body',
1026
+ },
1027
+ hostRules: [{ host: 'discord.com', strategy: 'bearer' }],
1028
+ },
1029
+ // ── Sentry (form body refresh) ──
1030
+ sentry: {
1031
+ provider: 'sentry',
1032
+ displayName: 'Sentry',
1033
+ authKind: 'oauth2',
1034
+ description: 'Issues, events, and releases via the Sentry API.',
1035
+ defaultScopes: 'org:read project:read event:read',
1036
+ oauth: {
1037
+ mode: 'pkce',
1038
+ clientId: 'cleo-sentry-client-id',
1039
+ authorizationEndpoint: 'https://sentry.io/oauth/authorize/',
1040
+ tokenEndpoint: 'https://sentry.io/oauth/token/',
1041
+ scope: 'org:read project:read event:read',
1042
+ redirectUri: 'http://127.0.0.1:7878/service/sentry/callback',
1043
+ },
1044
+ refresh: {
1045
+ kind: 'refresh-token',
1046
+ tokenUrl: 'https://sentry.io/oauth/token/',
1047
+ bodyFormat: 'form',
1048
+ clientAuth: 'body',
1049
+ },
1050
+ hostRules: [{ host: 'sentry.io', strategy: 'bearer' }],
1051
+ },
1052
+ // ── Vercel (api-key — no OAuth refresh) ──
1053
+ vercel: {
1054
+ provider: 'vercel',
1055
+ displayName: 'Vercel',
1056
+ authKind: 'api-key',
1057
+ description: 'Deployments, projects, and domains via the Vercel REST API.',
1058
+ defaultScopes: '',
1059
+ hostRules: [{ host: 'api.vercel.com', strategy: 'bearer' }],
1060
+ },
1061
+ };
1062
+
1063
+ /**
1064
+ * The seeded service-provider registry — **github + google + the ~40-service
1065
+ * census** (T11937 seeds + T11938 breadth).
1066
+ *
1067
+ * Keyed by {@link ServiceProviderDef.provider}. This is the SSoT the store reads
1068
+ * when validating a `connect` and the OAuth flow (T11939) reads for default
1069
+ * endpoints/scopes. The two seeds prove the two OAuth shapes:
1070
+ *
1071
+ * - **github** — classic OAuth 2.0 web flow (`authKind: 'oauth2'`), fixed
1072
+ * loopback redirect; user grants `repo`/`read:user` scopes.
1073
+ * - **google** — installed-app OAuth 2.0 + PKCE (`authKind: 'oauth2-pkce'`),
1074
+ * loopback redirect; user grants offline-access + the requested API scopes.
1075
+ *
1076
+ * The `clientId` values are PLACEHOLDER public client ids for CLEO's first-party
1077
+ * OAuth apps — the real ids are wired (or BYOC-overridden) when the OAuth dance
1078
+ * lands in T11939. They are NOT secrets (a public OAuth client id is, by design,
1079
+ * embeddable in a distributed CLI).
1080
+ *
1081
+ * Fan-out to the full ~40-service census is the follow-up (T11938): add a DATA
1082
+ * entry here, nothing else.
1083
+ *
1084
+ * @task T11937
1085
+ */
1086
+ export const SERVICE_PROVIDERS: Readonly<Record<string, ServiceProviderDef>> = {
1087
+ github: {
1088
+ provider: 'github',
1089
+ displayName: 'GitHub',
1090
+ authKind: 'oauth2',
1091
+ description: 'Repositories, issues, pull requests, and Actions on GitHub.',
1092
+ defaultScopes: 'repo read:user read:org',
1093
+ oauth: {
1094
+ mode: 'pkce',
1095
+ // PLACEHOLDER public client id for CLEO's first-party GitHub OAuth app —
1096
+ // real id (or a BYOC override from service_configs) wired in T11939.
1097
+ clientId: 'cleo-github-client-id',
1098
+ authorizationEndpoint: 'https://github.com/login/oauth/authorize',
1099
+ tokenEndpoint: 'https://github.com/login/oauth/access_token',
1100
+ scope: 'repo read:user read:org',
1101
+ redirectUri: 'http://127.0.0.1:7878/service/github/callback',
1102
+ },
1103
+ hostRules: [
1104
+ { host: 'api.github.com', strategy: 'bearer' },
1105
+ { host: 'github.com', strategy: 'basic-x-access-token' },
1106
+ { host: 'raw.githubusercontent.com', strategy: 'bearer' },
1107
+ ],
1108
+ },
1109
+ google: {
1110
+ provider: 'google',
1111
+ displayName: 'Google',
1112
+ authKind: 'oauth2-pkce',
1113
+ description: 'Gmail, Drive, Calendar, and other Google APIs.',
1114
+ defaultScopes: 'openid email profile https://www.googleapis.com/auth/drive.readonly',
1115
+ oauth: {
1116
+ mode: 'pkce',
1117
+ // PLACEHOLDER public client id for CLEO's first-party Google installed-app
1118
+ // OAuth client — real id (or a BYOC override) wired in T11939.
1119
+ clientId: 'cleo-google-client-id.apps.googleusercontent.com',
1120
+ authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
1121
+ tokenEndpoint: 'https://oauth2.googleapis.com/token',
1122
+ scope: 'openid email profile https://www.googleapis.com/auth/drive.readonly',
1123
+ redirectUri: 'http://127.0.0.1:7878/service/google/callback',
1124
+ extraAuthParams: {
1125
+ // Google requires these to return a refresh token on the first consent.
1126
+ access_type: 'offline',
1127
+ prompt: 'consent',
1128
+ },
1129
+ },
1130
+ refresh: GOOGLE_REFRESH,
1131
+ hostRules: [
1132
+ { host: 'www.googleapis.com', strategy: 'bearer' },
1133
+ { host: 'accounts.google.com', strategy: 'bearer' },
1134
+ ],
1135
+ },
1136
+ ...SERVICE_PROVIDERS_BREADTH,
1137
+ };