@oglofus/auth 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -29,6 +29,7 @@ Optional for app-level integrations:
29
29
 
30
30
  - `arctic` for OAuth providers in your app code.
31
31
  - `@oslojs/otp` if you need direct OTP utilities in your app (the library already uses it internally for TOTP).
32
+ - `stripe` if you use the Stripe billing plugin.
32
33
 
33
34
  ## Quick Start (Password)
34
35
 
@@ -127,15 +128,9 @@ You can build issues with helpers:
127
128
  ```ts
128
129
  import { createIssue, createIssueFactory } from "@oglofus/auth";
129
130
 
130
- const issue = createIssueFactory<{ email: string; profile: unknown }>([
131
- "email",
132
- "profile",
133
- ] as const);
131
+ const issue = createIssueFactory<{ email: string; profile: unknown }>(["email", "profile"] as const);
134
132
  issue.email("Email is required");
135
- issue.$path(
136
- ["profile", { key: "addresses" }, { index: 0 }, "city"],
137
- "City is required",
138
- );
133
+ issue.$path(["profile", { key: "addresses" }, { index: 0 }, "city"], "City is required");
139
134
  createIssue("Generic failure");
140
135
  ```
141
136
 
@@ -240,6 +235,14 @@ const result = await auth.authenticate({
240
235
  - Multi-tenant orgs, memberships, role inheritance, feature/limit entitlements, invites.
241
236
  - Validates role topology on startup (default role, owner role presence, inheritance cycles).
242
237
 
238
+ ### Stripe (Domain Plugin)
239
+
240
+ - Method: `"stripe"`
241
+ - User and organization subscriptions with typed billing subjects.
242
+ - Checkout session creation, billing portal sessions, webhook verification, local subscription snapshots.
243
+ - Plan-level features and limits, trial tracking, and organization entitlement merge support.
244
+ - Requires the `stripe` package in your application.
245
+
243
246
  ## Account Discovery
244
247
 
245
248
  Use `discover(...)` to support login/register routing logic before full auth:
@@ -257,6 +260,8 @@ See ready-to-copy integrations:
257
260
  - [`examples/sveltekit-email-otp`](./examples/sveltekit-email-otp)
258
261
  - [`examples/oauth2-google-arctic`](./examples/oauth2-google-arctic)
259
262
  - [`examples/two-factor-totp-oslo`](./examples/two-factor-totp-oslo)
263
+ - [`examples/stripe-user-billing`](./examples/stripe-user-billing)
264
+ - [`examples/stripe-organization-billing`](./examples/stripe-organization-billing)
260
265
 
261
266
  ## Scripts
262
267
 
@@ -1,6 +1,6 @@
1
- import { type AuthResult, type OperationResult } from "../types/results.js";
2
- import type { AuthConfig, AuthenticateInputFromPlugins, AuthPublicApi, AnyPlugin, PluginApiMap, PluginMethodsWithApi, RegisterInputFromPlugins } from "../types/plugins.js";
3
1
  import type { AuthRequestContext, CompleteProfileInput, DiscoverAccountDecision, DiscoverAccountInput, TwoFactorVerifyInput, UserBase } from "../types/model.js";
2
+ import type { AnyPlugin, AuthConfig, AuthPublicApi, AuthenticateInputFromPlugins, PluginApiForMethod, PluginMethodsWithApi, RegisterInputFromPlugins } from "../types/plugins.js";
3
+ import { type AuthResult, type OperationResult } from "../types/results.js";
4
4
  export declare class OglofusAuth<U extends UserBase, P extends readonly AnyPlugin<U>[]> implements AuthPublicApi<U, P> {
5
5
  private readonly config;
6
6
  private readonly pluginMap;
@@ -10,7 +10,7 @@ export declare class OglofusAuth<U extends UserBase, P extends readonly AnyPlugi
10
10
  discover(input: DiscoverAccountInput, request?: AuthRequestContext): Promise<OperationResult<DiscoverAccountDecision>>;
11
11
  authenticate(input: AuthenticateInputFromPlugins<P>, request?: AuthRequestContext): Promise<AuthResult<U>>;
12
12
  register(input: RegisterInputFromPlugins<P>, request?: AuthRequestContext): Promise<AuthResult<U>>;
13
- method<M extends PluginMethodsWithApi<P>>(method: M): PluginApiMap<P>[M];
13
+ method<M extends PluginMethodsWithApi<P>>(method: M): PluginApiForMethod<P, M>;
14
14
  verifySecondFactor(input: TwoFactorVerifyInput, request?: AuthRequestContext): Promise<AuthResult<U>>;
15
15
  completeProfile(input: CompleteProfileInput<U>, request?: AuthRequestContext): Promise<AuthResult<U>>;
16
16
  validateSession(sessionId: string, _request?: AuthRequestContext): Promise<{
package/dist/core/auth.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { AuthError } from "../errors/index.js";
2
+ import { createIssue } from "../issues/index.js";
2
3
  import { errorOperation, errorResult, successOperation, successResult, } from "../types/results.js";
3
4
  import { DEFAULT_SESSION_TTL_SECONDS, addSeconds, createId, deterministicTokenHash, normalizeEmailDefault, now, } from "./utils.js";
4
- import { createIssue } from "../issues/index.js";
5
5
  const hasEmail = (value) => typeof value === "object" && value !== null && "email" in value && typeof value.email === "string";
6
6
  const toAuthError = (error) => {
7
7
  if (error instanceof AuthError) {
@@ -26,6 +26,12 @@ export class OglofusAuth {
26
26
  this.config = config;
27
27
  this.normalizeEmail = config.normalize?.email ?? normalizeEmailDefault;
28
28
  this.validatePlugins();
29
+ const getPluginApi = (method) => {
30
+ if (!this.apiMap.has(method)) {
31
+ return null;
32
+ }
33
+ return this.apiMap.get(method);
34
+ };
29
35
  for (const plugin of config.plugins) {
30
36
  this.pluginMap.set(plugin.method, plugin);
31
37
  if (plugin.createApi) {
@@ -33,6 +39,7 @@ export class OglofusAuth {
33
39
  adapters: this.config.adapters,
34
40
  now,
35
41
  security: this.config.security,
42
+ getPluginApi,
36
43
  }));
37
44
  }
38
45
  }
@@ -410,6 +417,12 @@ export class OglofusAuth {
410
417
  now,
411
418
  security: this.config.security,
412
419
  request,
420
+ getPluginApi: (method) => {
421
+ if (!this.apiMap.has(method)) {
422
+ return null;
423
+ }
424
+ return this.apiMap.get(method);
425
+ },
413
426
  };
414
427
  }
415
428
  async createSession(userId) {
@@ -453,9 +466,7 @@ export class OglofusAuth {
453
466
  if (result.allowed) {
454
467
  return null;
455
468
  }
456
- return new AuthError("RATE_LIMITED", "Too many requests.", 429, [], result.retryAfterSeconds === undefined
457
- ? undefined
458
- : { retryAfterSeconds: result.retryAfterSeconds });
469
+ return new AuthError("RATE_LIMITED", "Too many requests.", 429, [], result.retryAfterSeconds === undefined ? undefined : { retryAfterSeconds: result.retryAfterSeconds });
459
470
  }
460
471
  makeRateLimitKey(scope, identity, request) {
461
472
  if (!request?.ip) {
@@ -521,6 +532,7 @@ export class OglofusAuth {
521
532
  const methods = new Set();
522
533
  let twoFactorCount = 0;
523
534
  let organizationsCount = 0;
535
+ let stripeCount = 0;
524
536
  for (const plugin of this.config.plugins) {
525
537
  if (methods.has(plugin.method)) {
526
538
  throw new AuthError("PLUGIN_METHOD_CONFLICT", `Plugin method conflict for '${plugin.method}'.`, 500);
@@ -532,6 +544,9 @@ export class OglofusAuth {
532
544
  if (plugin.method === "organizations") {
533
545
  organizationsCount += 1;
534
546
  }
547
+ if (plugin.method === "stripe") {
548
+ stripeCount += 1;
549
+ }
535
550
  if (plugin.kind === "auth_method") {
536
551
  if (plugin.supports.register && !plugin.register) {
537
552
  throw new AuthError("PLUGIN_MISCONFIGURED", `Plugin '${plugin.method}' declares register support but no register handler.`, 500);
@@ -553,6 +568,9 @@ export class OglofusAuth {
553
568
  if (organizationsCount > 1) {
554
569
  throw new AuthError("PLUGIN_MISCONFIGURED", "At most one organizations plugin is allowed.", 500);
555
570
  }
571
+ if (stripeCount > 1) {
572
+ throw new AuthError("PLUGIN_MISCONFIGURED", "At most one stripe plugin is allowed.", 500);
573
+ }
556
574
  if ((this.config.accountDiscovery?.mode ?? "private") === "explicit" && !this.config.adapters.identity) {
557
575
  throw new AuthError("PLUGIN_MISCONFIGURED", "identity adapter is required when accountDiscovery.mode is explicit.", 500);
558
576
  }
@@ -1,5 +1,5 @@
1
- import { createIssue } from "../issues/index.js";
2
1
  import { AuthError } from "../errors/index.js";
2
+ import { createIssue } from "../issues/index.js";
3
3
  export const ensureFields = (source, fields, basePath = []) => {
4
4
  const issues = fields
5
5
  .filter((field) => {
@@ -1,7 +1,6 @@
1
- import type { UserBase } from "../types/model.js";
2
- import type { ProfileCompletionState, TwoFactorRequiredMeta } from "../types/model.js";
3
1
  import type { Issue } from "../issues/index.js";
4
- export type AuthErrorCode = "INVALID_INPUT" | "METHOD_DISABLED" | "METHOD_NOT_REGISTERABLE" | "ACCOUNT_NOT_FOUND" | "ACCOUNT_EXISTS" | "ACCOUNT_EXISTS_WITH_DIFFERENT_METHOD" | "ORGANIZATION_NOT_FOUND" | "MEMBERSHIP_NOT_FOUND" | "MEMBERSHIP_FORBIDDEN" | "ROLE_INVALID" | "ROLE_NOT_ASSIGNABLE" | "ORGANIZATION_INVITE_INVALID" | "ORGANIZATION_INVITE_EXPIRED" | "SEAT_LIMIT_REACHED" | "FEATURE_DISABLED" | "LIMIT_EXCEEDED" | "LAST_OWNER_GUARD" | "SESSION_NOT_FOUND" | "INVALID_CREDENTIALS" | "USER_NOT_FOUND" | "CONFLICT" | "RATE_LIMITED" | "DELIVERY_FAILED" | "EMAIL_NOT_VERIFIED" | "OTP_EXPIRED" | "OTP_INVALID" | "MAGIC_LINK_EXPIRED" | "MAGIC_LINK_INVALID" | "OAUTH2_PROVIDER_DISABLED" | "OAUTH2_EXCHANGE_FAILED" | "PASSKEY_INVALID_ASSERTION" | "PASSKEY_INVALID_ATTESTATION" | "PASSKEY_CHALLENGE_EXPIRED" | "PROFILE_COMPLETION_REQUIRED" | "PROFILE_COMPLETION_EXPIRED" | "TWO_FACTOR_REQUIRED" | "TWO_FACTOR_INVALID" | "TWO_FACTOR_EXPIRED" | "RECOVERY_CODE_INVALID" | "PLUGIN_METHOD_CONFLICT" | "PLUGIN_MISCONFIGURED" | "INTERNAL_ERROR";
2
+ import type { ProfileCompletionState, TwoFactorRequiredMeta, UserBase } from "../types/model.js";
3
+ export type AuthErrorCode = "INVALID_INPUT" | "METHOD_DISABLED" | "METHOD_NOT_REGISTERABLE" | "ACCOUNT_NOT_FOUND" | "ACCOUNT_EXISTS" | "ACCOUNT_EXISTS_WITH_DIFFERENT_METHOD" | "ORGANIZATION_NOT_FOUND" | "MEMBERSHIP_NOT_FOUND" | "MEMBERSHIP_FORBIDDEN" | "ROLE_INVALID" | "ROLE_NOT_ASSIGNABLE" | "ORGANIZATION_INVITE_INVALID" | "ORGANIZATION_INVITE_EXPIRED" | "SEAT_LIMIT_REACHED" | "FEATURE_DISABLED" | "LIMIT_EXCEEDED" | "LAST_OWNER_GUARD" | "SESSION_NOT_FOUND" | "INVALID_CREDENTIALS" | "USER_NOT_FOUND" | "CONFLICT" | "RATE_LIMITED" | "DELIVERY_FAILED" | "EMAIL_NOT_VERIFIED" | "OTP_EXPIRED" | "OTP_INVALID" | "MAGIC_LINK_EXPIRED" | "MAGIC_LINK_INVALID" | "OAUTH2_PROVIDER_DISABLED" | "OAUTH2_EXCHANGE_FAILED" | "PASSKEY_INVALID_ASSERTION" | "PASSKEY_INVALID_ATTESTATION" | "PASSKEY_CHALLENGE_EXPIRED" | "CUSTOMER_NOT_FOUND" | "SUBSCRIPTION_NOT_FOUND" | "SUBSCRIPTION_ALREADY_EXISTS" | "TRIAL_NOT_AVAILABLE" | "STRIPE_WEBHOOK_INVALID" | "PROFILE_COMPLETION_REQUIRED" | "PROFILE_COMPLETION_EXPIRED" | "TWO_FACTOR_REQUIRED" | "TWO_FACTOR_INVALID" | "TWO_FACTOR_EXPIRED" | "RECOVERY_CODE_INVALID" | "PLUGIN_METHOD_CONFLICT" | "PLUGIN_MISCONFIGURED" | "INTERNAL_ERROR";
5
4
  export declare class AuthError extends Error {
6
5
  readonly code: AuthErrorCode;
7
6
  readonly status: number;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- export * from "./types/index.js";
2
- export * from "./issues/index.js";
3
- export * from "./errors/index.js";
4
- export * from "./core/events.js";
5
1
  export * from "./core/auth.js";
2
+ export * from "./core/events.js";
3
+ export * from "./errors/index.js";
4
+ export * from "./issues/index.js";
6
5
  export * from "./plugins/index.js";
6
+ export * from "./types/index.js";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- export * from "./types/index.js";
2
- export * from "./issues/index.js";
3
- export * from "./errors/index.js";
4
- export * from "./core/events.js";
5
1
  export * from "./core/auth.js";
2
+ export * from "./core/events.js";
3
+ export * from "./errors/index.js";
4
+ export * from "./issues/index.js";
6
5
  export * from "./plugins/index.js";
6
+ export * from "./types/index.js";
@@ -9,4 +9,4 @@ export type EmailOtpPluginConfig<U extends UserBase, K extends keyof U> = {
9
9
  maxAttempts?: number;
10
10
  codeLength?: number;
11
11
  };
12
- export declare const emailOtpPlugin: <U extends UserBase, K extends keyof U>(config: EmailOtpPluginConfig<U, K>) => AuthMethodPlugin<"email_otp", EmailOtpRegisterInput<U, K>, EmailOtpAuthenticateInput, U, EmailOtpPluginApi>;
12
+ export declare const emailOtpPlugin: <U extends UserBase, K extends keyof U>(config: EmailOtpPluginConfig<U, K>) => AuthMethodPlugin<"email_otp", EmailOtpRegisterInput<U, K>, EmailOtpAuthenticateInput, U, EmailOtpPluginApi, true, true>;
@@ -1,9 +1,8 @@
1
+ import { addSeconds, cloneWithout, createId, createNumericCode, secretHash, secretVerify } from "../core/utils.js";
2
+ import { ensureFields } from "../core/validators.js";
1
3
  import { AuthError } from "../errors/index.js";
2
4
  import { createIssue } from "../issues/index.js";
3
5
  import { errorOperation, successOperation } from "../types/results.js";
4
- import { addSeconds, createId, createNumericCode, secretHash, secretVerify, } from "../core/utils.js";
5
- import { cloneWithout } from "../core/utils.js";
6
- import { ensureFields } from "../core/validators.js";
7
6
  const PENDING_PREFIX = "pending:";
8
7
  const EMAIL_OTP_REQUEST_POLICY = { limit: 3, windowSeconds: 300 };
9
8
  const OTP_VERIFY_POLICY = { limit: 10, windowSeconds: 300 };
@@ -38,9 +37,7 @@ export const emailOtpPlugin = (config) => {
38
37
  if (challenge.expiresAt.getTime() <= now.getTime()) {
39
38
  return {
40
39
  ok: false,
41
- error: new AuthError("OTP_EXPIRED", "OTP has expired.", 400, [
42
- createIssue("OTP expired", ["code"]),
43
- ]),
40
+ error: new AuthError("OTP_EXPIRED", "OTP has expired.", 400, [createIssue("OTP expired", ["code"])]),
44
41
  };
45
42
  }
46
43
  if (challenge.attempts >= maxAttempts) {
@@ -60,9 +57,7 @@ export const emailOtpPlugin = (config) => {
60
57
  }
61
58
  return {
62
59
  ok: false,
63
- error: new AuthError("OTP_INVALID", "Invalid OTP code.", 400, [
64
- createIssue("Invalid code", ["code"]),
65
- ]),
60
+ error: new AuthError("OTP_INVALID", "Invalid OTP code.", 400, [createIssue("Invalid code", ["code"])]),
66
61
  };
67
62
  }
68
63
  const consumed = await config.otp.consumeChallenge(challengeId);
@@ -96,9 +91,7 @@ export const emailOtpPlugin = (config) => {
96
91
  const email = input.email.trim().toLowerCase();
97
92
  if (ctx.adapters.rateLimiter) {
98
93
  const policy = ctx.security?.rateLimits?.emailOtpRequest ?? EMAIL_OTP_REQUEST_POLICY;
99
- const limited = await ctx.adapters.rateLimiter.consume(request?.ip
100
- ? `emailOtpRequest:ip:${request.ip}:identity:${email}`
101
- : `emailOtpRequest:identity:${email}`, policy.limit, policy.windowSeconds);
94
+ const limited = await ctx.adapters.rateLimiter.consume(request?.ip ? `emailOtpRequest:ip:${request.ip}:identity:${email}` : `emailOtpRequest:identity:${email}`, policy.limit, policy.windowSeconds);
102
95
  if (!limited.allowed) {
103
96
  return errorOperation(createRateLimitedError(limited.retryAfterSeconds));
104
97
  }
@@ -154,11 +147,7 @@ export const emailOtpPlugin = (config) => {
154
147
  if (requiredError) {
155
148
  return errorOperation(requiredError);
156
149
  }
157
- const payload = cloneWithout(input, [
158
- "method",
159
- "challengeId",
160
- "code",
161
- ]);
150
+ const payload = cloneWithout(input, ["method", "challengeId", "code"]);
162
151
  payload.email = verified.email;
163
152
  payload.emailVerified = true;
164
153
  const user = await ctx.adapters.users.create(payload);
@@ -1,7 +1,8 @@
1
- export * from "./password.js";
2
1
  export * from "./email-otp.js";
3
2
  export * from "./magic-link.js";
4
3
  export * from "./oauth2.js";
4
+ export * from "./organizations.js";
5
5
  export * from "./passkey.js";
6
+ export * from "./password.js";
7
+ export * from "./stripe.js";
6
8
  export * from "./two-factor.js";
7
- export * from "./organizations.js";
@@ -1,7 +1,8 @@
1
- export * from "./password.js";
2
1
  export * from "./email-otp.js";
3
2
  export * from "./magic-link.js";
4
3
  export * from "./oauth2.js";
4
+ export * from "./organizations.js";
5
5
  export * from "./passkey.js";
6
+ export * from "./password.js";
7
+ export * from "./stripe.js";
6
8
  export * from "./two-factor.js";
7
- export * from "./organizations.js";
@@ -8,4 +8,4 @@ export type MagicLinkPluginConfig<U extends UserBase, K extends keyof U> = {
8
8
  tokenTtlSeconds?: number;
9
9
  baseVerifyUrl: string;
10
10
  };
11
- export declare const magicLinkPlugin: <U extends UserBase, K extends keyof U>(config: MagicLinkPluginConfig<U, K>) => AuthMethodPlugin<"magic_link", MagicLinkRegisterInput<U, K>, MagicLinkAuthenticateInput, U, MagicLinkPluginApi>;
11
+ export declare const magicLinkPlugin: <U extends UserBase, K extends keyof U>(config: MagicLinkPluginConfig<U, K>) => AuthMethodPlugin<"magic_link", MagicLinkRegisterInput<U, K>, MagicLinkAuthenticateInput, U, MagicLinkPluginApi, true, true>;
@@ -1,9 +1,8 @@
1
+ import { addSeconds, cloneWithout, createId, createToken, deterministicTokenHash } from "../core/utils.js";
2
+ import { ensureFields } from "../core/validators.js";
1
3
  import { AuthError } from "../errors/index.js";
2
4
  import { createIssue } from "../issues/index.js";
3
5
  import { errorOperation, successOperation } from "../types/results.js";
4
- import { addSeconds, createId, createToken, deterministicTokenHash } from "../core/utils.js";
5
- import { cloneWithout } from "../core/utils.js";
6
- import { ensureFields } from "../core/validators.js";
7
6
  const MAGIC_LINK_REQUEST_POLICY = { limit: 3, windowSeconds: 300 };
8
7
  const createRateLimitedError = (retryAfterSeconds) => new AuthError("RATE_LIMITED", "Too many requests.", 429, [], retryAfterSeconds === undefined ? undefined : { retryAfterSeconds });
9
8
  export const magicLinkPlugin = (config) => {
@@ -56,9 +55,7 @@ export const magicLinkPlugin = (config) => {
56
55
  const email = input.email.trim().toLowerCase();
57
56
  if (ctx.adapters.rateLimiter) {
58
57
  const policy = ctx.security?.rateLimits?.magicLinkRequest ?? MAGIC_LINK_REQUEST_POLICY;
59
- const limited = await ctx.adapters.rateLimiter.consume(request?.ip
60
- ? `magicLinkRequest:ip:${request.ip}:identity:${email}`
61
- : `magicLinkRequest:identity:${email}`, policy.limit, policy.windowSeconds);
58
+ const limited = await ctx.adapters.rateLimiter.consume(request?.ip ? `magicLinkRequest:ip:${request.ip}:identity:${email}` : `magicLinkRequest:identity:${email}`, policy.limit, policy.windowSeconds);
62
59
  if (!limited.allowed) {
63
60
  return errorOperation(createRateLimitedError(limited.retryAfterSeconds));
64
61
  }
@@ -115,10 +112,7 @@ export const magicLinkPlugin = (config) => {
115
112
  if (requiredError) {
116
113
  return errorOperation(requiredError);
117
114
  }
118
- const payload = cloneWithout(input, [
119
- "method",
120
- "token",
121
- ]);
115
+ const payload = cloneWithout(input, ["method", "token"]);
122
116
  payload.email = verified.email;
123
117
  payload.emailVerified = true;
124
118
  const user = await ctx.adapters.users.create(payload);
@@ -38,11 +38,11 @@ export type OAuth2ProviderConfig<U extends UserBase, P extends string> = {
38
38
  };
39
39
  export type OAuth2PluginConfig<U extends UserBase, P extends string, K extends keyof U = never> = {
40
40
  providers: {
41
- [Provider in P]: OAuth2ProviderConfig<U, Provider>;
41
+ [Provider in P]?: OAuth2ProviderConfig<U, Provider>;
42
42
  };
43
43
  accounts: OAuth2AccountAdapter<P>;
44
44
  requiredProfileFields?: readonly K[];
45
45
  pendingProfileTtlSeconds?: number;
46
46
  };
47
47
  export declare const arcticAuthorizationCodeExchange: (client: ArcticAuthorizationCodeClient) => OAuth2AuthorizationCodeExchange;
48
- export declare const oauth2Plugin: <U extends UserBase, P extends string, K extends keyof U = never>(config: OAuth2PluginConfig<U, P, K>) => AuthMethodPlugin<"oauth2", OAuth2AuthenticateInput<P>, OAuth2AuthenticateInput<P>, U>;
48
+ export declare const oauth2Plugin: <U extends UserBase, P extends string, K extends keyof U = never>(config: OAuth2PluginConfig<U, P, K>) => AuthMethodPlugin<"oauth2", OAuth2AuthenticateInput<P>, OAuth2AuthenticateInput<P>, U, never, false, false>;
@@ -1,9 +1,9 @@
1
1
  import { ArcticFetchError, OAuth2RequestError } from "arctic";
2
+ import { addSeconds, createId } from "../core/utils.js";
3
+ import { ensureFields } from "../core/validators.js";
2
4
  import { AuthError } from "../errors/index.js";
3
5
  import { createIssue } from "../issues/index.js";
4
6
  import { errorOperation, successOperation } from "../types/results.js";
5
- import { addSeconds, createId } from "../core/utils.js";
6
- import { ensureFields } from "../core/validators.js";
7
7
  export const arcticAuthorizationCodeExchange = (client) => {
8
8
  return async ({ authorizationCode, codeVerifier }) => {
9
9
  const validateAuthorizationCode = client.validateAuthorizationCode;
@@ -41,7 +41,9 @@ export const oauth2Plugin = (config) => {
41
41
  },
42
42
  completePendingProfile: async (_ctx, { record, user }) => {
43
43
  const continuation = record.continuation;
44
- if (!continuation || typeof continuation.provider !== "string" || typeof continuation.providerUserId !== "string") {
44
+ if (!continuation ||
45
+ typeof continuation.provider !== "string" ||
46
+ typeof continuation.providerUserId !== "string") {
45
47
  return errorOperation(new AuthError("PLUGIN_MISCONFIGURED", "Pending OAuth2 profile is missing provider continuation metadata.", 500));
46
48
  }
47
49
  await config.accounts.linkAccount({
@@ -93,7 +95,9 @@ export const oauth2Plugin = (config) => {
93
95
  ]));
94
96
  }
95
97
  if (error instanceof ArcticFetchError) {
96
- return errorOperation(new AuthError("OAUTH2_EXCHANGE_FAILED", "OAuth2 provider is unreachable right now.", 502, [createIssue("Provider request failed", ["provider"])]));
98
+ return errorOperation(new AuthError("OAUTH2_EXCHANGE_FAILED", "OAuth2 provider is unreachable right now.", 502, [
99
+ createIssue("Provider request failed", ["provider"]),
100
+ ]));
97
101
  }
98
102
  return errorOperation(new AuthError("OAUTH2_EXCHANGE_FAILED", "OAuth2 code exchange failed.", 502));
99
103
  }
@@ -1,5 +1,5 @@
1
1
  import type { MembershipBase, OrganizationBase, UserBase } from "../types/model.js";
2
- import type { OrganizationsPluginConfig, OrganizationsPluginApi, DomainPlugin } from "../types/plugins.js";
2
+ import type { DomainPlugin, OrganizationsPluginApi, OrganizationsPluginConfig } from "../types/plugins.js";
3
3
  export type OrganizationsPluginOptions<O extends OrganizationBase, Role extends string, M extends MembershipBase<Role>, Permission extends string, Feature extends string, LimitKey extends string, RequiredOrgFields extends keyof O = never> = OrganizationsPluginConfig<O, Role, M, Permission, Feature, LimitKey, RequiredOrgFields> & {
4
4
  inviteBaseUrl: string;
5
5
  inviteTtlSeconds?: number;
@@ -9,4 +9,4 @@ export type OrganizationsPluginOptions<O extends OrganizationBase, Role extends
9
9
  nextRole: Role;
10
10
  }) => boolean | Promise<boolean>;
11
11
  };
12
- export declare const organizationsPlugin: <U extends UserBase, O extends OrganizationBase, Role extends string, M extends MembershipBase<Role>, Permission extends string, Feature extends string, LimitKey extends string, RequiredOrgFields extends keyof O = never>(config: OrganizationsPluginOptions<O, Role, M, Permission, Feature, LimitKey, RequiredOrgFields>) => DomainPlugin<"organizations", U, OrganizationsPluginApi<O, Role, M, Permission, Feature, LimitKey, RequiredOrgFields>>;
12
+ export declare const organizationsPlugin: <U extends UserBase, O extends OrganizationBase, Role extends string, M extends MembershipBase<Role>, Permission extends string, Feature extends string, LimitKey extends string, RequiredOrgFields extends keyof O = never>(config: OrganizationsPluginOptions<O, Role, M, Permission, Feature, LimitKey, RequiredOrgFields>) => DomainPlugin<"organizations", U, OrganizationsPluginApi<O, Role, M, Permission, Feature, LimitKey, RequiredOrgFields>, true>;
@@ -1,7 +1,7 @@
1
+ import { addSeconds, createId, createToken, deterministicTokenHash } from "../core/utils.js";
1
2
  import { AuthError } from "../errors/index.js";
2
3
  import { createIssue } from "../issues/index.js";
3
4
  import { errorOperation, successOperation } from "../types/results.js";
4
- import { addSeconds, createId, createToken, deterministicTokenHash } from "../core/utils.js";
5
5
  const normalizeEmail = (value) => value.trim().toLowerCase();
6
6
  const findOwnerRoles = (roles) => Object.entries(roles)
7
7
  .filter(([, definition]) => definition.system?.owner)
@@ -75,15 +75,34 @@ export const organizationsPlugin = (config) => {
75
75
  return membershipRes;
76
76
  }
77
77
  const resolved = resolveRole(membershipRes.data.role, config.handlers.roles);
78
+ const stripeApi = ctx.getPluginApi?.("stripe") ?? null;
79
+ let billingEntitlements = {
80
+ features: {},
81
+ limits: {},
82
+ };
83
+ if (stripeApi) {
84
+ const stripeEntitlements = await stripeApi.getEntitlements({
85
+ subject: {
86
+ kind: "organization",
87
+ organizationId,
88
+ },
89
+ });
90
+ if (!stripeEntitlements.ok) {
91
+ return stripeEntitlements;
92
+ }
93
+ billingEntitlements = stripeEntitlements.data;
94
+ }
78
95
  const featureOverrides = await config.handlers.entitlements.getFeatureOverrides(organizationId);
79
96
  const limitOverrides = await config.handlers.entitlements.getLimitOverrides(organizationId);
80
97
  return successOperation({
81
98
  features: {
82
99
  ...resolved.features,
100
+ ...billingEntitlements.features,
83
101
  ...featureOverrides,
84
102
  },
85
103
  limits: {
86
104
  ...resolved.limits,
105
+ ...billingEntitlements.limits,
87
106
  ...limitOverrides,
88
107
  },
89
108
  });
@@ -118,9 +137,7 @@ export const organizationsPlugin = (config) => {
118
137
  });
119
138
  return { organization, membership };
120
139
  };
121
- const data = ctx.adapters.withTransaction
122
- ? await ctx.adapters.withTransaction(run)
123
- : await run();
140
+ const data = ctx.adapters.withTransaction ? await ctx.adapters.withTransaction(run) : await run();
124
141
  return successOperation(data);
125
142
  },
126
143
  inviteMember: async (input, request) => {
@@ -5,4 +5,4 @@ export type PasskeyPluginConfig<U extends UserBase, K extends keyof U> = {
5
5
  requiredProfileFields: readonly K[];
6
6
  passkeys: PasskeyAdapter;
7
7
  };
8
- export declare const passkeyPlugin: <U extends UserBase, K extends keyof U>(config: PasskeyPluginConfig<U, K>) => AuthMethodPlugin<"passkey", PasskeyRegisterInput<U, K>, PasskeyAuthenticateInput, U>;
8
+ export declare const passkeyPlugin: <U extends UserBase, K extends keyof U>(config: PasskeyPluginConfig<U, K>) => AuthMethodPlugin<"passkey", PasskeyRegisterInput<U, K>, PasskeyAuthenticateInput, U, never, true, false>;
@@ -1,8 +1,8 @@
1
+ import { cloneWithout, createId } from "../core/utils.js";
2
+ import { ensureFields } from "../core/validators.js";
1
3
  import { AuthError } from "../errors/index.js";
2
4
  import { createIssue } from "../issues/index.js";
3
5
  import { errorOperation, successOperation } from "../types/results.js";
4
- import { cloneWithout, createId } from "../core/utils.js";
5
- import { ensureFields } from "../core/validators.js";
6
6
  export const passkeyPlugin = (config) => ({
7
7
  kind: "auth_method",
8
8
  method: "passkey",
@@ -54,10 +54,7 @@ export const passkeyPlugin = (config) => ({
54
54
  }
55
55
  let user = await ctx.adapters.users.findByEmail(input.email);
56
56
  if (!user) {
57
- const payload = cloneWithout(input, [
58
- "method",
59
- "registration",
60
- ]);
57
+ const payload = cloneWithout(input, ["method", "registration"]);
61
58
  payload.emailVerified = true;
62
59
  user = await ctx.adapters.users.create(payload);
63
60
  }
@@ -7,4 +7,4 @@ export type PasswordPluginConfig<U extends UserBase, K extends keyof U> = {
7
7
  hashPassword?: (password: string) => string;
8
8
  verifyPassword?: (password: string, hash: string) => boolean;
9
9
  };
10
- export declare const passwordPlugin: <U extends UserBase, K extends keyof U>(config: PasswordPluginConfig<U, K>) => AuthMethodPlugin<"password", PasswordRegisterInput<U, K>, PasswordAuthenticateInput, U>;
10
+ export declare const passwordPlugin: <U extends UserBase, K extends keyof U>(config: PasswordPluginConfig<U, K>) => AuthMethodPlugin<"password", PasswordRegisterInput<U, K>, PasswordAuthenticateInput, U, never, true, false>;
@@ -1,8 +1,8 @@
1
+ import { cloneWithout, secretHash, secretVerify } from "../core/utils.js";
2
+ import { ensureFields } from "../core/validators.js";
1
3
  import { AuthError } from "../errors/index.js";
2
4
  import { createIssue } from "../issues/index.js";
3
5
  import { errorOperation, successOperation } from "../types/results.js";
4
- import { cloneWithout, secretHash, secretVerify } from "../core/utils.js";
5
- import { ensureFields } from "../core/validators.js";
6
6
  export const passwordPlugin = (config) => ({
7
7
  kind: "auth_method",
8
8
  method: "password",
@@ -25,10 +25,7 @@ export const passwordPlugin = (config) => ({
25
25
  if (requiredError) {
26
26
  return errorOperation(requiredError);
27
27
  }
28
- const payload = cloneWithout(input, [
29
- "method",
30
- "password",
31
- ]);
28
+ const payload = cloneWithout(input, ["method", "password"]);
32
29
  if (payload.emailVerified === undefined) {
33
30
  payload.emailVerified = false;
34
31
  }
@@ -0,0 +1,25 @@
1
+ import type Stripe from "stripe";
2
+ import type { StripeCustomerAdapter, StripeSubscriptionAdapter, StripeTrialUsageAdapter, StripeWebhookEventAdapter } from "../types/adapters.js";
3
+ import type { StripePlan, StripeSubject, UserBase } from "../types/model.js";
4
+ import type { DomainPlugin, StripePlansResolver, StripePluginApi } from "../types/plugins.js";
5
+ type StripePluginHandlers<Feature extends string, LimitKey extends string> = {
6
+ customers: StripeCustomerAdapter;
7
+ subscriptions: StripeSubscriptionAdapter<Feature, LimitKey>;
8
+ events: StripeWebhookEventAdapter;
9
+ trials?: StripeTrialUsageAdapter;
10
+ };
11
+ export type StripePluginConfig<U extends UserBase, Feature extends string, LimitKey extends string> = {
12
+ stripe: Stripe;
13
+ webhookSecret: string;
14
+ plans: StripePlansResolver<Feature, LimitKey>;
15
+ handlers: StripePluginHandlers<Feature, LimitKey>;
16
+ customerMode?: "user" | "organization" | "both";
17
+ };
18
+ type StripePlanCatalogEntry<Feature extends string, LimitKey extends string> = Omit<StripePlan<Feature, LimitKey>, "scope"> & {
19
+ scope: StripeSubject["kind"];
20
+ };
21
+ type StripePlanCatalog<Feature extends string, LimitKey extends string> = readonly StripePlanCatalogEntry<Feature, LimitKey>[];
22
+ export declare const stripePlugin: <U extends UserBase, Feature extends string, LimitKey extends string, const Plans extends StripePlanCatalog<Feature, LimitKey>>(config: Omit<StripePluginConfig<U, Feature, LimitKey>, "plans"> & {
23
+ plans: StripePlansResolver<Feature, LimitKey, Plans>;
24
+ }) => DomainPlugin<"stripe", U, StripePluginApi<Feature, LimitKey>, true>;
25
+ export {};