-
Notifications
You must be signed in to change notification settings - Fork 182
Expand file tree
/
Copy pathoauth-client.ts
More file actions
239 lines (219 loc) · 10.4 KB
/
Copy pathoauth-client.ts
File metadata and controls
239 lines (219 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
import type { Effect } from "effect";
import { Schema } from "effect";
import type { Connection } from "./connection";
import type { StorageFailure } from "./fuma-runtime";
import {
type AuthTemplateSlug,
type ConnectionName,
type IntegrationSlug,
OAuthClientSlug,
OAuthState,
type Owner,
} from "./ids";
/* The v2 OAuth surface contracts. OAuth is a credential mechanism, not an
* integration type. A client is a registered app; running its flow mints a
* Connection. The client is self-contained (carries its own endpoints) and
* integration-independent, so the same app can back connections on whatever
* integrations share that provider.
*
* The OAuth 2.1 *implementation* (PKCE, DCR, token exchange + refresh) lives in
* `oauth-helpers` / `oauth-discovery` / `oauth-service`; these are the public
* input/output shapes the executor's `oauth.*` namespace speaks. */
export type OAuthGrant = "authorization_code" | "client_credentials";
/** Provider OAuth config an integration declares as one of its auth templates —
* what to request. (The flow itself runs off the self-contained OAuthClient.)
* Keyed `kind: "oauth2"` like every auth method across the plugins. */
export interface OAuthAuthentication {
readonly slug: AuthTemplateSlug;
readonly kind: "oauth2";
readonly authorizationUrl: string;
readonly tokenUrl: string;
/** RFC 8707 Resource Indicator to bind the OAuth flow to this protected
* resource, when discovered from protected-resource metadata. */
readonly resource?: string | null;
readonly scopes: readonly string[];
/** True when the authorization server supports OAuth Client ID Metadata
* Document (CIMD). The local OAuth client is then a public PKCE client whose
* `client_id` is this host's metadata-document URL, not a provider-side
* registered app id. */
readonly supportsClientIdMetadataDocument?: boolean;
}
/** A registered OAuth app — pure app identity: clientId/secret + its endpoints.
* Owner-scoped: a shared org app or a user's own BYO app. The app does NOT carry
* scopes — what to request is the INTEGRATION's concern (`OAuthAuthentication.
* scopes`, surfaced via the declared auth method), so the same app can back any
* integration without pinning a scope set. */
export interface OAuthClient {
readonly owner: Owner;
readonly slug: OAuthClientSlug;
readonly authorizationUrl: string;
readonly tokenUrl: string;
readonly grant: OAuthGrant;
readonly clientId: string;
/** The literal client secret. Stored out-of-band in the credential provider
* (vault item id), never inline. Empty string for public / PKCE clients. */
readonly clientSecret: string;
/** RFC 8707 Resource Indicator (MCP). Carried so the refresh request can keep
* the re-minted token bound to the same resource. Null/omitted otherwise. */
readonly resource?: string | null;
}
export type OAuthClientOrigin =
| { readonly kind: "manual" }
| {
readonly kind: "dynamic_client_registration";
readonly integration?: IntegrationSlug | null;
};
export type CreateOAuthClientInput = OAuthClient & {
readonly origin?: OAuthClientOrigin;
};
/** Metadata-only projection of a registered client for listing in the UI.
* Deliberately omits `clientSecret` — the secret is never returned over the
* read surface. `clientId` is included (it is not a secret; it is sent in the
* authorize URL the user's browser visits). */
export interface OAuthClientSummary {
readonly owner: Owner;
readonly slug: OAuthClientSlug;
readonly grant: OAuthGrant;
readonly authorizationUrl: string;
readonly tokenUrl: string;
readonly resource?: string | null;
readonly clientId: string;
readonly origin: OAuthClientOrigin;
}
/** Flow-aware result of `oauth.start` — the status says what's next. */
export type ConnectResult =
| { readonly status: "connected"; readonly connection: Connection }
| {
readonly status: "redirect";
readonly authorizationUrl: string;
readonly state: OAuthState;
};
/** Start a flow through a client to mint a connection for one integration.
* `template` is the integration's oauth template the minted token is applied
* through. */
export interface OAuthStartInput {
readonly client: OAuthClientSlug;
/** The owner that owns `client`. Supplied explicitly (the picker knows it), so
* a Personal connection can be minted through a shared Workspace app without
* any owner-derivation rule. A Workspace connection must use a Workspace app. */
readonly clientOwner: Owner;
/** The owner the minted CONNECTION is saved under (may differ from `clientOwner`). */
readonly owner: Owner;
readonly name: ConnectionName;
readonly integration: IntegrationSlug;
readonly template: AuthTemplateSlug;
readonly identityLabel?: string | null;
/** Browser-facing callback URL for this flow. Defaults to the executor's configured redirectUri. */
readonly redirectUri?: string | null;
}
export interface OAuthCompleteInput {
readonly state: OAuthState;
readonly code: string;
/** Non-standard regional host the authorization server returns on the
* callback (Datadog's `domain`/`site` params) so the code is redeemed at the
* org's actual region rather than the statically advertised one. Used only
* when it is a sibling subdomain of the client's configured token host. */
readonly callbackDomain?: string | null;
}
/** Probe a base/issuer URL for OAuth 2.1 authorization-server metadata so the
* onboarding UI can pre-fill a client's endpoints. */
export interface OAuthProbeInput {
readonly url: string;
}
export interface OAuthProbeResult {
readonly authorizationUrl: string;
readonly tokenUrl: string;
/** RFC 8707 resource indicator discovered from protected-resource metadata.
* Persist this on DCR clients so authorize/token/refresh requests stay bound
* to the protected resource. */
readonly resource?: string | null;
readonly scopesSupported?: readonly string[];
/** Whether the server advertises dynamic client registration (RFC 7591). */
readonly registrationEndpoint?: string | null;
/** RFC 8414 `token_endpoint_auth_methods_supported`. Surfaced so DCR can pick
* a public ("none") client when the server allows it. */
readonly tokenEndpointAuthMethodsSupported?: readonly string[];
/** Draft OAuth Client ID Metadata Document support, advertised by providers
* such as PostHog as `client_id_metadata_document_supported`. */
readonly clientIdMetadataDocumentSupported?: boolean;
}
/** Mint an OAuth client via RFC 7591 Dynamic Client Registration and persist it.
* The user pastes NO client id/secret — the authorization server mints a
* (public, PKCE) client which is stored as an owner-scoped `oauth_client`. */
export interface RegisterDynamicClientInput {
readonly owner: Owner;
readonly slug: OAuthClientSlug;
/** RFC 7591 registration endpoint advertised by the authorization server. */
readonly registrationEndpoint: string;
readonly authorizationUrl: string;
readonly tokenUrl: string;
/** RFC 8707 Resource Indicator (MCP). Persisted on the minted client when known. */
readonly resource?: string | null;
readonly scopes: readonly string[];
/** Auth methods the server advertises. When it allows `none` a public
* (PKCE-only, no secret) client is registered; otherwise `client_secret_post`. */
readonly tokenEndpointAuthMethodsSupported?: readonly string[];
/** Human label for the registered app (RFC 7591 `client_name`). */
readonly clientName?: string;
/** Browser-facing callback URL to register. Defaults to the executor's configured redirectUri. */
readonly redirectUri?: string | null;
/** Integration that requested this dynamic client, when known. */
readonly originIntegration?: IntegrationSlug | null;
}
export class OAuthStartError extends Schema.TaggedErrorClass<OAuthStartError>()("OAuthStartError", {
message: Schema.String,
}) {}
export class OAuthCompleteError extends Schema.TaggedErrorClass<OAuthCompleteError>()(
"OAuthCompleteError",
{
message: Schema.String,
/** True when the auth-code exchange failed in a way the user must restart. */
restartRequired: Schema.optional(Schema.Boolean),
},
) {}
export class OAuthProbeError extends Schema.TaggedErrorClass<OAuthProbeError>()("OAuthProbeError", {
message: Schema.String,
}) {}
export class OAuthRegisterDynamicError extends Schema.TaggedErrorClass<OAuthRegisterDynamicError>()(
"OAuthRegisterDynamicError",
{ message: Schema.String },
) {}
export class OAuthSessionNotFoundError extends Schema.TaggedErrorClass<OAuthSessionNotFoundError>()(
"OAuthSessionNotFoundError",
{ state: OAuthState },
) {}
/** The OAuth surface the executor's `oauth.*` namespace and `ctx.oauth` expose.
* Implemented by `makeOAuthService` (oauth-service.ts), wired by the executor
* with the deps it needs to mint connections. */
export interface OAuthService {
readonly createClient: (
input: CreateOAuthClientInput,
) => Effect.Effect<OAuthClientSlug, StorageFailure>;
/** Mint a client via RFC 7591 Dynamic Client Registration (no pre-shared
* client id/secret) and persist it as an owner-scoped `oauth_client`. */
readonly registerDynamicClient: (
input: RegisterDynamicClientInput,
) => Effect.Effect<OAuthClientSlug, OAuthRegisterDynamicError | StorageFailure>;
/** All registered clients visible to the caller (their org's shared clients +
* their own user clients), as metadata-only summaries — never the secret. */
readonly listClients: () => Effect.Effect<readonly OAuthClientSummary[], StorageFailure>;
/** Permanently remove a registered OAuth app, keyed by (owner, slug). The
* owner policy on `oauth_client` prevents removing another subject's user app.
* Idempotent: removing an already-gone app succeeds. Connections that
* referenced the slug keep their stored value and fail at the next token
* refresh, prompting a reconnect — this op never cascades into connections. */
readonly removeClient: (
owner: Owner,
slug: OAuthClientSlug,
) => Effect.Effect<void, StorageFailure>;
readonly start: (
input: OAuthStartInput,
) => Effect.Effect<ConnectResult, OAuthStartError | StorageFailure>;
readonly complete: (
input: OAuthCompleteInput,
) => Effect.Effect<Connection, OAuthCompleteError | OAuthSessionNotFoundError | StorageFailure>;
readonly cancel: (state: OAuthState) => Effect.Effect<void, StorageFailure>;
readonly probe: (
input: OAuthProbeInput,
) => Effect.Effect<OAuthProbeResult, OAuthProbeError | StorageFailure>;
}