@atlasauth/backend 0.2.0 → 0.5.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.
@@ -1,30 +1,235 @@
1
1
  import { type RequestFn } from './request';
2
2
  /**
3
- * §BYOK messaging provider config — wire the tenant's own email/SMS provider
4
- * (Resend/SES/SMTP/Brevo/Mailgun, Twilio) over the secret-key BAPI.
3
+ * Messaging providers — wire your own email or SMS provider (Resend, SES, SMTP,
4
+ * Brevo, Mailgun; Twilio, Vonage, MessageBird, Plivo, Telnyx, Sinch, Infobip,
5
+ * ClickSend, Bandwidth, AWS SNS) over the secret-key Backend API.
6
+ *
7
+ * Each transport accepts an exact set of `config` keys; the API refuses an
8
+ * unknown key with a 422 naming `config.<key>`. These types mirror that schema,
9
+ * so a typo or another provider's key name fails to compile.
5
10
  *
6
11
  * A provider SECRET is WRITE-ONLY: it goes in through `config` and is never
7
12
  * read back — a response reports only WHICH secret keys are set (`secrets_set`).
8
- * Omit a secret on an edit to keep the stored one (so a from-address change need
9
- * not re-enter the API key).
13
+ *
14
+ * Required keys: every key marked "Required on first setup" must be sent the
15
+ * first time you configure a transport (or when you switch the channel to a
16
+ * different transport). When you update the SAME transport, the stored config is
17
+ * kept: omit a key — or send it as `''` — and its stored value (including a
18
+ * stored secret) stays. So a from-address change need not re-enter the API key.
10
19
  */
11
- export interface MessagingProvider {
12
- object: 'messaging_provider';
13
- channel: 'email' | 'sms';
14
- transport: string;
15
- from_address: string | null;
16
- status: string;
17
- /** Non-secret config only — secret values are stripped before serialising. */
18
- config: Record<string, unknown>;
19
- /** Which secret keys are set for this transport, never their values. */
20
- secrets_set: Record<string, boolean>;
21
- updated_at: number;
20
+ /** Email transports accepted by `PUT /v1/messaging_providers/email`. */
21
+ export type EmailTransport = 'resend' | 'ses' | 'smtp' | 'brevo' | 'mailgun';
22
+ /** SMS transports accepted by `PUT /v1/messaging_providers/sms`. */
23
+ export type SmsTransport = 'twilio' | 'vonage' | 'messagebird' | 'plivo' | 'telnyx' | 'sinch' | 'infobip' | 'clicksend' | 'bandwidth' | 'aws_sns';
24
+ export type MessagingTransport = EmailTransport | SmsTransport;
25
+ /** Sender number or ID for an SMS provider. `from_address` on the request takes precedence. */
26
+ interface SmsFrom {
27
+ /** Sender number or ID for this provider. Optional; `from_address` on the request takes precedence. */
28
+ from?: string;
22
29
  }
23
- export interface MessagingProviders {
24
- object: 'messaging_providers';
25
- email: MessagingProvider | null;
26
- sms: MessagingProvider | null;
30
+ /** Resend. */
31
+ export interface ResendConfig {
32
+ /** Resend API key (`re_…`). Required on first setup. Write-only: never returned. */
33
+ api_key: string;
34
+ }
35
+ /** Brevo. */
36
+ export interface BrevoConfig {
37
+ /** Brevo API key (`xkeysib-…`). Required on first setup. Write-only: never returned. */
38
+ api_key: string;
39
+ }
40
+ /** Mailgun. */
41
+ export interface MailgunConfig {
42
+ /** Mailgun private API key. Required on first setup. Write-only: never returned. */
43
+ api_key: string;
44
+ /** Your Mailgun sending domain, e.g. `mg.example.com`. Required on first setup. */
45
+ domain: string;
46
+ /** Mailgun region. Default `us`. */
47
+ region?: 'us' | 'eu';
48
+ }
49
+ /** Amazon SES. */
50
+ export interface SesConfig {
51
+ /** AWS region, e.g. `us-east-1`. Required on first setup. */
52
+ region: string;
53
+ /** IAM access key ID. Required on first setup. Write-only: never returned. */
54
+ access_key_id: string;
55
+ /** IAM secret access key. Required on first setup. Write-only: never returned. */
56
+ secret_access_key: string;
57
+ /** STS session token, only for temporary credentials. Write-only: never returned. */
58
+ session_token?: string;
59
+ }
60
+ /** Any SMTP server. */
61
+ export interface SmtpConfig {
62
+ /** SMTP server hostname, e.g. `smtp.example.com`. Required on first setup. */
63
+ host: string;
64
+ /** SMTP port (1-65535). Default 587. */
65
+ port?: number;
66
+ /** Use implicit TLS. Default: `true` on port 465, otherwise STARTTLS. */
67
+ secure?: boolean;
68
+ /** SMTP username, if the server requires authentication. */
69
+ username?: string;
70
+ /** SMTP password or app password. Write-only: never returned. */
71
+ password?: string;
72
+ }
73
+ /** Twilio. */
74
+ export interface TwilioConfig extends SmsFrom {
75
+ /** Twilio Account SID (`AC…`). Required on first setup. */
76
+ account_sid: string;
77
+ /** Twilio auth token. Required on first setup. Write-only: never returned. */
78
+ auth_token: string;
79
+ /** WhatsApp-enabled sender number, for the WhatsApp channel. */
80
+ whatsapp_from?: string;
81
+ /** Voice-capable number, for the voice channel. */
82
+ voice_from?: string;
83
+ }
84
+ /** Vonage. */
85
+ export interface VonageConfig extends SmsFrom {
86
+ /** Vonage API key. Required on first setup. */
87
+ api_key: string;
88
+ /** Vonage API secret. Required on first setup. Write-only: never returned. */
89
+ api_secret: string;
90
+ }
91
+ /** MessageBird (Bird). */
92
+ export interface MessagebirdConfig extends SmsFrom {
93
+ /** MessageBird (Bird) access key. Required on first setup. Write-only: never returned. */
94
+ access_key: string;
95
+ }
96
+ /** Plivo. */
97
+ export interface PlivoConfig extends SmsFrom {
98
+ /** Plivo Auth ID. Required on first setup. */
99
+ auth_id: string;
100
+ /** Plivo auth token. Required on first setup. Write-only: never returned. */
101
+ auth_token: string;
102
+ }
103
+ /** Telnyx. */
104
+ export interface TelnyxConfig extends SmsFrom {
105
+ /** Telnyx API key. Required on first setup. Write-only: never returned. */
106
+ api_key: string;
107
+ }
108
+ /** Sinch. */
109
+ export interface SinchConfig extends SmsFrom {
110
+ /** Sinch service plan ID. Required on first setup. */
111
+ service_plan_id: string;
112
+ /** Sinch API token. Required on first setup. Write-only: never returned. */
113
+ api_token: string;
114
+ /** Sinch API region, a short lowercase code such as `us` or `eu`. Default `us`. */
115
+ region?: string;
116
+ }
117
+ /** Infobip. */
118
+ export interface InfobipConfig extends SmsFrom {
119
+ /** Your Infobip base URL, e.g. `https://xxxxx.api.infobip.com`. Must be https. Required on first setup. */
120
+ base_url: string;
121
+ /** Infobip API key. Required on first setup. Write-only: never returned. */
122
+ api_key: string;
123
+ }
124
+ /** ClickSend. */
125
+ export interface ClicksendConfig extends SmsFrom {
126
+ /** ClickSend username (your login email). Required on first setup. */
127
+ username: string;
128
+ /** ClickSend API key. Required on first setup. Write-only: never returned. */
129
+ api_key: string;
130
+ }
131
+ /** Bandwidth. */
132
+ export interface BandwidthConfig extends SmsFrom {
133
+ /** Bandwidth account ID. Required on first setup. */
134
+ account_id: string;
135
+ /** Bandwidth messaging application ID. Required on first setup. */
136
+ application_id: string;
137
+ /** Bandwidth API username. Required on first setup. */
138
+ username: string;
139
+ /** Bandwidth API password. Required on first setup. Write-only: never returned. */
140
+ password: string;
141
+ }
142
+ /** Amazon SNS. */
143
+ export interface AwsSnsConfig extends SmsFrom {
144
+ /** AWS region, e.g. `us-east-1`. Required on first setup. */
145
+ region: string;
146
+ /** IAM access key ID. Required on first setup. */
147
+ access_key_id: string;
148
+ /** IAM secret access key. Required on first setup. Write-only: never returned. */
149
+ secret_access_key: string;
150
+ /** STS session token, only for temporary credentials. Write-only: never returned. */
151
+ session_token?: string;
152
+ }
153
+ /** The `config` shape for each transport, keyed by transport. */
154
+ export interface MessagingTransportConfigs {
155
+ resend: ResendConfig;
156
+ ses: SesConfig;
157
+ smtp: SmtpConfig;
158
+ brevo: BrevoConfig;
159
+ mailgun: MailgunConfig;
160
+ twilio: TwilioConfig;
161
+ vonage: VonageConfig;
162
+ messagebird: MessagebirdConfig;
163
+ plivo: PlivoConfig;
164
+ telnyx: TelnyxConfig;
165
+ sinch: SinchConfig;
166
+ infobip: InfobipConfig;
167
+ clicksend: ClicksendConfig;
168
+ bandwidth: BandwidthConfig;
169
+ aws_sns: AwsSnsConfig;
27
170
  }
171
+ /** The write-only (secret) keys of each transport — never present in a response. */
172
+ export interface MessagingSecretKeys {
173
+ resend: 'api_key';
174
+ ses: 'access_key_id' | 'secret_access_key' | 'session_token';
175
+ smtp: 'password';
176
+ brevo: 'api_key';
177
+ mailgun: 'api_key';
178
+ twilio: 'auth_token';
179
+ vonage: 'api_secret';
180
+ messagebird: 'access_key';
181
+ plivo: 'auth_token';
182
+ telnyx: 'api_key';
183
+ sinch: 'api_token';
184
+ infobip: 'api_key';
185
+ clicksend: 'api_key';
186
+ bandwidth: 'password';
187
+ aws_sns: 'secret_access_key' | 'session_token';
188
+ }
189
+ interface ProviderBodyBase<T extends MessagingTransport> {
190
+ transport: T;
191
+ /**
192
+ * Email: the From address, e.g. `"Acme <no-reply@acme.com>"` — a sender your
193
+ * provider accepts. SMS: the sender number in E.164 format, e.g. `+15551234567`.
194
+ */
195
+ from_address?: string;
196
+ }
197
+ /**
198
+ * First setup of transport `T` (or a switch to it): `config` carries every
199
+ * required key.
200
+ */
201
+ export interface MessagingProviderSetup<T extends MessagingTransport> extends ProviderBodyBase<T> {
202
+ config: MessagingTransportConfigs[T];
203
+ }
204
+ /**
205
+ * An update of the transport already configured on the channel: every `config`
206
+ * key is optional. An omitted (or `''`) key keeps its stored value — including a
207
+ * stored secret.
208
+ */
209
+ export interface MessagingProviderUpdate<T extends MessagingTransport> extends ProviderBodyBase<T> {
210
+ config?: Partial<MessagingTransportConfigs[T]>;
211
+ }
212
+ /** Body for `messaging.setEmail` — discriminated by `transport`. */
213
+ export type EmailProviderInput = {
214
+ [T in EmailTransport]: MessagingProviderSetup<T> | MessagingProviderUpdate<T>;
215
+ }[EmailTransport];
216
+ /** Body for `messaging.setSms` — discriminated by `transport`. */
217
+ export type SmsProviderInput = {
218
+ [T in SmsTransport]: MessagingProviderSetup<T> | MessagingProviderUpdate<T>;
219
+ }[SmsTransport];
220
+ /** First-setup body for the email channel. Use with `satisfies` to require every required key. */
221
+ export type EmailProviderSetup = {
222
+ [T in EmailTransport]: MessagingProviderSetup<T>;
223
+ }[EmailTransport];
224
+ /** First-setup body for the SMS channel. Use with `satisfies` to require every required key. */
225
+ export type SmsProviderSetup = {
226
+ [T in SmsTransport]: MessagingProviderSetup<T>;
227
+ }[SmsTransport];
228
+ /**
229
+ * @deprecated The untyped body accepted before per-transport typing. Use
230
+ * {@link EmailProviderInput} / {@link SmsProviderInput} instead; the API refuses
231
+ * any `config` key the transport does not use.
232
+ */
28
233
  export interface MessagingProviderInput {
29
234
  /** The transport, e.g. `resend`, `ses`, `smtp` (email) or `twilio` (sms). */
30
235
  transport: string;
@@ -32,6 +237,42 @@ export interface MessagingProviderInput {
32
237
  /** Transport config; carries the write-only secret keys on write. */
33
238
  config?: Record<string, unknown>;
34
239
  }
240
+ /** The non-secret config keys of transport `T`. */
241
+ type PublicConfigKey<T extends MessagingTransport> = Exclude<keyof MessagingTransportConfigs[T], MessagingSecretKeys[T]>;
242
+ /**
243
+ * A configured provider as returned by the API. `config` holds the non-secret
244
+ * settings only (values come back as strings, e.g. `port: "587"`); secrets are
245
+ * never returned — `secrets_set` says which ones are stored.
246
+ */
247
+ export interface MessagingProviderOf<C extends 'email' | 'sms', T extends MessagingTransport> {
248
+ object: 'messaging_provider';
249
+ channel: C;
250
+ transport: T;
251
+ from_address: string | null;
252
+ status: string;
253
+ /** Non-secret config only — secret values are never returned. */
254
+ config: {
255
+ [K in PublicConfigKey<T>]?: string;
256
+ };
257
+ /** For each secret key of this transport: whether a value is stored. Never the value. */
258
+ secrets_set: {
259
+ [K in MessagingSecretKeys[T]]: boolean;
260
+ };
261
+ updated_at: number;
262
+ }
263
+ export type EmailProvider = {
264
+ [T in EmailTransport]: MessagingProviderOf<'email', T>;
265
+ }[EmailTransport];
266
+ export type SmsProvider = {
267
+ [T in SmsTransport]: MessagingProviderOf<'sms', T>;
268
+ }[SmsTransport];
269
+ /** Either channel's provider. Narrow on `transport` (or `channel`) to read its config. */
270
+ export type MessagingProvider = EmailProvider | SmsProvider;
271
+ export interface MessagingProviders {
272
+ object: 'messaging_providers';
273
+ email: EmailProvider | null;
274
+ sms: SmsProvider | null;
275
+ }
35
276
  export interface MessagingTestResult {
36
277
  object: 'messaging_test';
37
278
  channel: string;
@@ -43,14 +284,39 @@ export interface DeletedMessagingProvider {
43
284
  channel: string;
44
285
  deleted: true;
45
286
  }
287
+ /**
288
+ * The deprecated untyped body, accepted only when `transport` is a plain
289
+ * `string` (not a known literal) — so a typed literal with a wrong key still
290
+ * fails to compile instead of falling through to the legacy overload.
291
+ */
292
+ type LegacyInput<T extends string> = Omit<MessagingProviderInput, 'transport'> & {
293
+ transport: T;
294
+ } & (string extends T ? unknown : never);
46
295
  export declare function messagingResource(request: RequestFn): {
47
296
  /** The instance's configured email and SMS providers, secrets omitted. */
48
297
  get(): Promise<MessagingProviders>;
49
- setEmail(body: MessagingProviderInput): Promise<MessagingProvider>;
50
- setSms(body: MessagingProviderInput): Promise<MessagingProvider>;
298
+ /**
299
+ * Configure (or update) the email provider. `config` keys are checked per
300
+ * `transport`; on an update of the same transport, omitted keys — including
301
+ * secrets — keep their stored values.
302
+ */
303
+ setEmail: {
304
+ (body: EmailProviderInput): Promise<EmailProvider>;
305
+ <T extends string>(body: LegacyInput<T>): Promise<MessagingProvider>;
306
+ };
307
+ /**
308
+ * Configure (or update) the SMS provider. `config` keys are checked per
309
+ * `transport`; on an update of the same transport, omitted keys — including
310
+ * secrets — keep their stored values.
311
+ */
312
+ setSms: {
313
+ (body: SmsProviderInput): Promise<SmsProvider>;
314
+ <T extends string>(body: LegacyInput<T>): Promise<MessagingProvider>;
315
+ };
51
316
  deleteChannel(channel: "email" | "sms"): Promise<DeletedMessagingProvider>;
52
317
  /** Send a fixed, clearly-marked test message through the channel's provider. */
53
318
  test(channel: "email" | "sms", body: {
54
319
  to: string;
55
320
  }): Promise<MessagingTestResult>;
56
321
  };
322
+ export {};
@@ -3,17 +3,29 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.messagingResource = messagingResource;
4
4
  const request_1 = require("./request");
5
5
  function messagingResource(request) {
6
+ function setEmail(body) {
7
+ return request({ method: 'PUT', path: '/v1/messaging_providers/email', body });
8
+ }
9
+ function setSms(body) {
10
+ return request({ method: 'PUT', path: '/v1/messaging_providers/sms', body });
11
+ }
6
12
  return {
7
13
  /** The instance's configured email and SMS providers, secrets omitted. */
8
14
  get() {
9
15
  return request({ method: 'GET', path: '/v1/messaging_providers' });
10
16
  },
11
- setEmail(body) {
12
- return request({ method: 'PUT', path: '/v1/messaging_providers/email', body });
13
- },
14
- setSms(body) {
15
- return request({ method: 'PUT', path: '/v1/messaging_providers/sms', body });
16
- },
17
+ /**
18
+ * Configure (or update) the email provider. `config` keys are checked per
19
+ * `transport`; on an update of the same transport, omitted keys — including
20
+ * secrets — keep their stored values.
21
+ */
22
+ setEmail,
23
+ /**
24
+ * Configure (or update) the SMS provider. `config` keys are checked per
25
+ * `transport`; on an update of the same transport, omitted keys — including
26
+ * secrets — keep their stored values.
27
+ */
28
+ setSms,
17
29
  deleteChannel(channel) {
18
30
  return request({ method: 'DELETE', path: `/v1/messaging_providers/${(0, request_1.enc)(channel)}` });
19
31
  },
@@ -1 +1 @@
1
- {"version":3,"file":"messaging.js","sourceRoot":"","sources":["../../src/client/messaging.ts"],"names":[],"mappings":";;AAmDA,8CAwBC;AA3ED,uCAAgD;AAmDhD,SAAgB,iBAAiB,CAAC,OAAkB;IAClD,OAAO;QACL,0EAA0E;QAC1E,GAAG;YACD,OAAO,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,yBAAyB,EAAE,CAAC,CAAC;QACrE,CAAC;QACD,QAAQ,CAAC,IAA4B;YACnC,OAAO,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,+BAA+B,EAAE,IAAI,EAAE,CAAC,CAAC;QACjF,CAAC;QACD,MAAM,CAAC,IAA4B;YACjC,OAAO,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,6BAA6B,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/E,CAAC;QACD,aAAa,CAAC,OAAwB;YACpC,OAAO,OAAO,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,2BAA2B,IAAA,aAAG,EAAC,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC;QACxF,CAAC;QACD,gFAAgF;QAChF,IAAI,CAAC,OAAwB,EAAE,IAAoB;YACjD,OAAO,OAAO,CAAC;gBACb,MAAM,EAAE,MAAM;gBACd,IAAI,EAAE,2BAA2B,IAAA,aAAG,EAAC,OAAO,CAAC,OAAO;gBACpD,IAAI;aACL,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"messaging.js","sourceRoot":"","sources":["../../src/client/messaging.ts"],"names":[],"mappings":";;AAsVA,8CA4CC;AAlYD,uCAAgD;AAsVhD,SAAgB,iBAAiB,CAAC,OAAkB;IAIlD,SAAS,QAAQ,CAAC,IAAa;QAC7B,OAAO,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,+BAA+B,EAAE,IAAI,EAAE,CAAC,CAAC;IACjF,CAAC;IAKD,SAAS,MAAM,CAAC,IAAa;QAC3B,OAAO,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,6BAA6B,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/E,CAAC;IAED,OAAO;QACL,0EAA0E;QAC1E,GAAG;YACD,OAAO,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,yBAAyB,EAAE,CAAC,CAAC;QACrE,CAAC;QACD;;;;WAIG;QACH,QAAQ;QACR;;;;WAIG;QACH,MAAM;QACN,aAAa,CAAC,OAAwB;YACpC,OAAO,OAAO,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,2BAA2B,IAAA,aAAG,EAAC,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC;QACxF,CAAC;QACD,gFAAgF;QAChF,IAAI,CAAC,OAAwB,EAAE,IAAoB;YACjD,OAAO,OAAO,CAAC;gBACb,MAAM,EAAE,MAAM;gBACd,IAAI,EAAE,2BAA2B,IAAA,aAAG,EAAC,OAAO,CAAC,OAAO;gBACpD,IAAI;aACL,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * OIDC discovery of an instance's JWKS URL, so `new AtlasBackend({ issuer })`
3
+ * works without a `jwksUrl`.
4
+ *
5
+ * Where the document lives:
6
+ * - an instance with its own frontend API host (every instance created from
7
+ * the dashboard): `${issuer}/.well-known/openid-configuration`;
8
+ * - an instance on the SHARED host: its tokens carry the shared issuer, and the
9
+ * shared host's root discovery document describes a different instance's key
10
+ * set. Such an instance must name itself (`instanceId`, or a `publishableKey`
11
+ * it is derived from) so discovery uses the per-instance document at
12
+ * `${issuer}/instances/${instanceId}/.well-known/openid-configuration`.
13
+ *
14
+ * Caching mirrors the JWKS cache: the discovered URL is reused for the JWKS TTL
15
+ * (an hour), a failed refresh keeps serving the last good URL, and a failing
16
+ * endpoint is retried at most once a minute — so a broken or hostile discovery
17
+ * endpoint cannot turn verification traffic into outbound request floods.
18
+ */
19
+ export declare class DiscoveryError extends Error {
20
+ constructor(message: string);
21
+ }
22
+ export interface JwksDiscoveryDeps {
23
+ /** The `iss` tokens carry. The discovery document's `issuer` must equal it. */
24
+ issuer: string;
25
+ /** Per-instance discovery for shared-host instances. */
26
+ instanceId?: string;
27
+ fetchImpl?: typeof fetch;
28
+ now?: () => number;
29
+ ttlMs?: number;
30
+ refetchIntervalMs?: number;
31
+ }
32
+ /** The instance id inside a publishable key (`pk_live_ins_…` / `pk_test_ins_…`), if it has one. */
33
+ export declare function instanceIdFromPublishableKey(publishableKey: string): string | undefined;
34
+ /** The discovery document URL for an issuer (and optional instance id). */
35
+ export declare function discoveryUrlFor(issuer: string, instanceId?: string): string;
36
+ export declare class JwksDiscovery {
37
+ private readonly deps;
38
+ readonly url: string;
39
+ private cachedUri;
40
+ private fetchedAt;
41
+ private lastAttemptAt;
42
+ private lastError;
43
+ private inflight;
44
+ constructor(deps: JwksDiscoveryDeps);
45
+ private get now();
46
+ /** The JWKS URL, from cache when fresh. Throws {@link DiscoveryError} when none can be had. */
47
+ jwksUri(): Promise<string>;
48
+ private fetchUri;
49
+ }
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.JwksDiscovery = exports.DiscoveryError = void 0;
4
+ exports.instanceIdFromPublishableKey = instanceIdFromPublishableKey;
5
+ exports.discoveryUrlFor = discoveryUrlFor;
6
+ const jwks_cache_1 = require("./jwks-cache");
7
+ /**
8
+ * OIDC discovery of an instance's JWKS URL, so `new AtlasBackend({ issuer })`
9
+ * works without a `jwksUrl`.
10
+ *
11
+ * Where the document lives:
12
+ * - an instance with its own frontend API host (every instance created from
13
+ * the dashboard): `${issuer}/.well-known/openid-configuration`;
14
+ * - an instance on the SHARED host: its tokens carry the shared issuer, and the
15
+ * shared host's root discovery document describes a different instance's key
16
+ * set. Such an instance must name itself (`instanceId`, or a `publishableKey`
17
+ * it is derived from) so discovery uses the per-instance document at
18
+ * `${issuer}/instances/${instanceId}/.well-known/openid-configuration`.
19
+ *
20
+ * Caching mirrors the JWKS cache: the discovered URL is reused for the JWKS TTL
21
+ * (an hour), a failed refresh keeps serving the last good URL, and a failing
22
+ * endpoint is retried at most once a minute — so a broken or hostile discovery
23
+ * endpoint cannot turn verification traffic into outbound request floods.
24
+ */
25
+ class DiscoveryError extends Error {
26
+ constructor(message) {
27
+ super(message);
28
+ this.name = 'DiscoveryError';
29
+ }
30
+ }
31
+ exports.DiscoveryError = DiscoveryError;
32
+ const stripSlash = (value) => value.replace(/\/+$/, '');
33
+ /** The instance id inside a publishable key (`pk_live_ins_…` / `pk_test_ins_…`), if it has one. */
34
+ function instanceIdFromPublishableKey(publishableKey) {
35
+ const match = /^pk_(?:live|test)_(ins_[A-Za-z0-9]+)$/.exec(publishableKey.trim());
36
+ return match?.[1];
37
+ }
38
+ /** The discovery document URL for an issuer (and optional instance id). */
39
+ function discoveryUrlFor(issuer, instanceId) {
40
+ const base = stripSlash(issuer);
41
+ return instanceId
42
+ ? `${base}/instances/${encodeURIComponent(instanceId)}/.well-known/openid-configuration`
43
+ : `${base}/.well-known/openid-configuration`;
44
+ }
45
+ function isLoopback(hostname) {
46
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';
47
+ }
48
+ class JwksDiscovery {
49
+ deps;
50
+ url;
51
+ cachedUri = null;
52
+ fetchedAt = 0;
53
+ lastAttemptAt = 0;
54
+ lastError = null;
55
+ inflight = null;
56
+ constructor(deps) {
57
+ this.deps = deps;
58
+ this.url = discoveryUrlFor(deps.issuer, deps.instanceId);
59
+ }
60
+ get now() {
61
+ return (this.deps.now ?? Date.now)();
62
+ }
63
+ /** The JWKS URL, from cache when fresh. Throws {@link DiscoveryError} when none can be had. */
64
+ async jwksUri() {
65
+ const now = this.now;
66
+ const ttl = this.deps.ttlMs ?? jwks_cache_1.DEFAULT_TTL_MS;
67
+ const interval = this.deps.refetchIntervalMs ?? jwks_cache_1.REFETCH_INTERVAL_MS;
68
+ if (this.cachedUri && now - this.fetchedAt < ttl)
69
+ return this.cachedUri;
70
+ // Throttle: inside the retry window, answer from what we have (or the last error).
71
+ if (this.lastAttemptAt && now - this.lastAttemptAt < interval) {
72
+ if (this.cachedUri)
73
+ return this.cachedUri;
74
+ if (this.lastError)
75
+ throw this.lastError;
76
+ }
77
+ if (this.inflight)
78
+ return this.inflight;
79
+ this.lastAttemptAt = now;
80
+ this.inflight = this.fetchUri()
81
+ .then((uri) => {
82
+ this.cachedUri = uri;
83
+ this.fetchedAt = now;
84
+ this.lastError = null;
85
+ return uri;
86
+ })
87
+ .catch((error) => {
88
+ this.lastError =
89
+ error instanceof DiscoveryError
90
+ ? error
91
+ : new DiscoveryError(`OIDC discovery at ${this.url} failed: ${error?.message ?? String(error)}`);
92
+ // Keep serving the last good URL through a discovery outage.
93
+ if (this.cachedUri)
94
+ return this.cachedUri;
95
+ throw this.lastError;
96
+ })
97
+ .finally(() => {
98
+ this.inflight = null;
99
+ });
100
+ return this.inflight;
101
+ }
102
+ async fetchUri() {
103
+ const fail = (why) => new DiscoveryError(`OIDC discovery at ${this.url} failed: ${why}. Pass \`jwksUrl\` explicitly` +
104
+ (this.deps.instanceId
105
+ ? '.'
106
+ : ', or — for an instance without its own frontend API domain — pass `instanceId` (or `publishableKey`).'));
107
+ const response = await (this.deps.fetchImpl ?? fetch)(this.url, {
108
+ headers: { accept: 'application/json' },
109
+ });
110
+ if (!response.ok)
111
+ throw fail(`HTTP ${response.status}`);
112
+ let body;
113
+ try {
114
+ body = await response.json();
115
+ }
116
+ catch {
117
+ throw fail('the response is not JSON');
118
+ }
119
+ const doc = body;
120
+ if (!doc || typeof doc !== 'object')
121
+ throw fail('the response is not a discovery document');
122
+ // OIDC Discovery §4.3: the document's issuer MUST match the issuer it was
123
+ // fetched for — otherwise its keys belong to someone else.
124
+ if (typeof doc.issuer !== 'string' || stripSlash(doc.issuer) !== stripSlash(this.deps.issuer)) {
125
+ throw fail(`its issuer (${String(doc.issuer)}) does not match the configured issuer (${this.deps.issuer})`);
126
+ }
127
+ if (typeof doc.jwks_uri !== 'string')
128
+ throw fail('it has no jwks_uri');
129
+ let jwks;
130
+ try {
131
+ jwks = new URL(doc.jwks_uri);
132
+ }
133
+ catch {
134
+ throw fail('its jwks_uri is not an absolute URL');
135
+ }
136
+ if (jwks.protocol !== 'https:' && !(jwks.protocol === 'http:' && isLoopback(jwks.hostname))) {
137
+ throw fail('its jwks_uri is not https');
138
+ }
139
+ return jwks.toString();
140
+ }
141
+ }
142
+ exports.JwksDiscovery = JwksDiscovery;
143
+ //# sourceMappingURL=discovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './jwks-cache';
2
+ export { DiscoveryError, JwksDiscovery, discoveryUrlFor, instanceIdFromPublishableKey } from './discovery';
2
3
  export * from './verify';
3
4
  export * from './handshake';
4
5
  export * from './webhooks';
@@ -7,3 +8,7 @@ export type { AtlasClient } from './client';
7
8
  export * from './client';
8
9
  export { createTestIssuer } from './testing';
9
10
  export type { TestIssuer, CreateTestIssuerOptions } from './testing';
11
+ export { verifyPCActn } from './pca/verify';
12
+ export type { PcaVerdict, PcaContext, VerifyPCActnOptions } from './pca/verify';
13
+ export { requirePCA, defaultExtract, memoryPcaStore } from './pca/require-pca';
14
+ export type { RequirePcaOptions, PcaGuardResult, PcaRequestLike, PcaStateStore } from './pca/require-pca';
package/dist/index.js CHANGED
@@ -14,9 +14,15 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.createTestIssuer = exports.createAtlasClient = void 0;
17
+ exports.memoryPcaStore = exports.defaultExtract = exports.requirePCA = exports.verifyPCActn = exports.createTestIssuer = exports.createAtlasClient = exports.instanceIdFromPublishableKey = exports.discoveryUrlFor = exports.JwksDiscovery = exports.DiscoveryError = void 0;
18
18
  // §7.3 token verification — the original surface, unchanged.
19
19
  __exportStar(require("./jwks-cache"), exports);
20
+ // OIDC discovery of the JWKS URL when only `issuer` is configured.
21
+ var discovery_1 = require("./discovery");
22
+ Object.defineProperty(exports, "DiscoveryError", { enumerable: true, get: function () { return discovery_1.DiscoveryError; } });
23
+ Object.defineProperty(exports, "JwksDiscovery", { enumerable: true, get: function () { return discovery_1.JwksDiscovery; } });
24
+ Object.defineProperty(exports, "discoveryUrlFor", { enumerable: true, get: function () { return discovery_1.discoveryUrlFor; } });
25
+ Object.defineProperty(exports, "instanceIdFromPublishableKey", { enumerable: true, get: function () { return discovery_1.instanceIdFromPublishableKey; } });
20
26
  __exportStar(require("./verify"), exports);
21
27
  __exportStar(require("./handshake"), exports);
22
28
  // §12.2 inbound webhook verification for consumers.
@@ -30,4 +36,12 @@ __exportStar(require("./client"), exports);
30
36
  // Its own module, so it never sits on the hot verification path.
31
37
  var testing_1 = require("./testing");
32
38
  Object.defineProperty(exports, "createTestIssuer", { enumerable: true, get: function () { return testing_1.createTestIssuer; } });
39
+ // Proof-Carrying Authority (agentic auth): offline verifier + framework-agnostic guard,
40
+ // beside the session-token verifier above.
41
+ var verify_1 = require("./pca/verify");
42
+ Object.defineProperty(exports, "verifyPCActn", { enumerable: true, get: function () { return verify_1.verifyPCActn; } });
43
+ var require_pca_1 = require("./pca/require-pca");
44
+ Object.defineProperty(exports, "requirePCA", { enumerable: true, get: function () { return require_pca_1.requirePCA; } });
45
+ Object.defineProperty(exports, "defaultExtract", { enumerable: true, get: function () { return require_pca_1.defaultExtract; } });
46
+ Object.defineProperty(exports, "memoryPcaStore", { enumerable: true, get: function () { return require_pca_1.memoryPcaStore; } });
33
47
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,6DAA6D;AAC7D,+CAA6B;AAC7B,2CAAyB;AACzB,8CAA4B;AAE5B,oDAAoD;AACpD,6CAA2B;AAE3B,8DAA8D;AAC9D,mCAA6C;AAApC,2GAAA,iBAAiB,OAAA;AAE1B,2CAAyB;AAEzB,yEAAyE;AACzE,2EAA2E;AAC3E,iEAAiE;AACjE,qCAA6C;AAApC,2GAAA,gBAAgB,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,6DAA6D;AAC7D,+CAA6B;AAC7B,mEAAmE;AACnE,yCAA2G;AAAlG,2GAAA,cAAc,OAAA;AAAE,0GAAA,aAAa,OAAA;AAAE,4GAAA,eAAe,OAAA;AAAE,yHAAA,4BAA4B,OAAA;AACrF,2CAAyB;AACzB,8CAA4B;AAE5B,oDAAoD;AACpD,6CAA2B;AAE3B,8DAA8D;AAC9D,mCAA6C;AAApC,2GAAA,iBAAiB,OAAA;AAE1B,2CAAyB;AAEzB,yEAAyE;AACzE,2EAA2E;AAC3E,iEAAiE;AACjE,qCAA6C;AAApC,2GAAA,gBAAgB,OAAA;AAGzB,wFAAwF;AACxF,2CAA2C;AAC3C,uCAA4C;AAAnC,sGAAA,YAAY,OAAA;AAErB,iDAA+E;AAAtE,yGAAA,UAAU,OAAA;AAAE,6GAAA,cAAc,OAAA;AAAE,6GAAA,cAAc,OAAA"}
@@ -34,6 +34,12 @@ export interface JwksCacheDeps {
34
34
  * when both are given the static set wins and no network call is ever made.
35
35
  */
36
36
  staticJwks?: Jwks;
37
+ /**
38
+ * Resolve the JWKS URL lazily (OIDC discovery) when no fixed `url` is given.
39
+ * Called on each fetch attempt; it does its own caching. A rejection is
40
+ * recorded as {@link JwksCache.lastError} and treated like a failed fetch.
41
+ */
42
+ resolveUrl?: () => Promise<string>;
37
43
  fetchImpl?: typeof fetch;
38
44
  now?: () => number;
39
45
  ttlMs?: number;
@@ -47,6 +53,8 @@ export declare class JwksCache {
47
53
  private lastAttemptAt;
48
54
  /** Exposed so a caller can assert the rate limit actually bit. */
49
55
  lastOutcome: FetchOutcome;
56
+ /** Why the last attempt produced no keys (e.g. a discovery failure), for diagnostics. */
57
+ lastError: string | null;
50
58
  constructor(deps: JwksCacheDeps);
51
59
  private get now();
52
60
  private has;