@user-center/next-better-auth 0.0.0-stage → 0.1.8
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/LICENSE +21 -0
- package/README.md +38 -2
- package/dist/integration-contracts/src/compatibility.d.ts +9 -0
- package/dist/integration-contracts/src/compatibility.js +3 -0
- package/dist/integration-contracts/src/config.d.ts +15 -0
- package/dist/integration-contracts/src/config.js +17 -0
- package/dist/integration-contracts/src/environment.d.ts +25 -0
- package/dist/integration-contracts/src/environment.js +108 -0
- package/dist/integration-contracts/src/metadata.d.ts +4 -0
- package/dist/integration-contracts/src/metadata.js +71 -0
- package/dist/integration-contracts/src/public.d.ts +51 -0
- package/dist/integration-contracts/src/public.js +134 -0
- package/dist/next-better-auth/src/client.d.ts +10 -0
- package/dist/next-better-auth/src/client.js +36 -0
- package/dist/next-better-auth/src/config.d.ts +2 -0
- package/dist/next-better-auth/src/config.js +1 -0
- package/dist/next-better-auth/src/public.d.ts +1 -0
- package/dist/next-better-auth/src/public.js +1 -0
- package/dist/next-better-auth/src/server.d.ts +142 -0
- package/dist/next-better-auth/src/server.js +298 -0
- package/package.json +64 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 User Center contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,39 @@
|
|
|
1
|
-
#
|
|
1
|
+
# User Center Next Better Auth SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Candidate 0.1.8 for Next App Router on Node and Better Auth 1.7.12. This package creates a complete controlled authentication instance. You supply the database adapter, independent session secret and business mapping. It never edits your project or runs migrations.
|
|
4
|
+
|
|
5
|
+
This is a public npm trial candidate, prepared with public access and the `next` tag. This package is licensed under MIT, with the complete notice in the included LICENSE. This preparation only performs a dry run and does not upload the package. Scope ownership, publisher authentication and registry version availability still require verification before a separately authorized publication.
|
|
6
|
+
|
|
7
|
+
Install the verified local archive together with the exact Better Auth peer. If your host imports `server-only`, declare it directly too:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add better-auth@1.7.12 server-only@0.0.1 /path/to/user-center-next-better-auth-0.1.8.tgz
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
After the operator supplies a verified official publication record for this version, you can instead install `@user-center/next-better-auth@0.1.8` with the same Better Auth peer. Until then, use the supplied archive and do not guess-install a similarly named registry package. The `next` tag does not certify production readiness.
|
|
14
|
+
|
|
15
|
+
Use the public entries below. Do not import private dist or workspace source paths.
|
|
16
|
+
|
|
17
|
+
| Entry | Host use |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| root or `/public` | Public configuration validation and types |
|
|
20
|
+
| `/config` | `describeRegistration`, `describeIntegration`, environment profile selection and PostgreSQL binding checks |
|
|
21
|
+
| `/server` | `createUserCenterAuth`, with explicit adapter, secrets, user policy and business resolver |
|
|
22
|
+
| `/client` | `createUserCenterClient`, for controlled login, safe session reads and local logout |
|
|
23
|
+
|
|
24
|
+
You can calculate a registration before creating a Client:
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
import { describeRegistration } from '@user-center/next-better-auth/config';
|
|
28
|
+
const registration = describeRegistration({ issuer: 'https://auth.example.com/api/auth', platformOrigin: 'https://platform.example.test' });
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Register the exact returned callback and origin in your center console. Create a Manifest v1, then import `createUserCenterAuth` from `/server` in your own server module. Mount only `instance.handlers.GET/POST`, never the raw Better Auth handler. Create a browser facade with `createUserCenterClient(instance.publicConfig)` from `/client`.
|
|
32
|
+
|
|
33
|
+
The root and `/public` exports are safe configuration utilities. `/config` uses Node crypto without network or database access. `/server` is protected by `server-only`. Keep client and session secrets on the server. An authentication session does not authorize a business user: protect pages and APIs with `getPrincipal`, requiring `authenticated` and your own permissions.
|
|
34
|
+
|
|
35
|
+
The complete guide, Manifest example, schema requirements, failure recovery and independent consumer are in `docs/integration/sdk/` and `examples/sdk-next-client/` in the corresponding source snapshot. The source repository is [ZXFTech/user-center](https://github.com/ZXFTech/user-center), with the [SDK guide](https://github.com/ZXFTech/user-center/blob/main/docs/integration/sdk/README.md). These are repository destinations; this dry run does not verify that uncommitted documentation has been pushed or that a reader has repository access. Keep the guide and sample with an offline delivery.
|
|
36
|
+
|
|
37
|
+
No real production integration or runtime combination is certified by this candidate. Verify the release manifest and SHA256 before installation. Host storage migrations, global Account identity uniqueness, business user mapping and permissions need host implementation and actual acceptance. Platform logout does not promise global center logout. Blog integration is completed according to the user's confirmation, without being reverified here or implying that blog uses 0.1.8. Production mail, the independent real authentication chain and formal runtime combinations remain unverified.
|
|
38
|
+
|
|
39
|
+
The `build` and `typecheck` scripts are for a source checkout with its workspace tools. The installed package is precompiled and does not execute these scripts during installation or authentication. In a source checkout, `pnpm toolkit:npm:dry-run` checks the sealed archives and invokes npm with fixed dry run, offline and ignore scripts options. It never uploads a package.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const MATRIX_VERSION = "2026-11-04.1";
|
|
2
|
+
export interface RuntimeCombination {
|
|
3
|
+
node: string;
|
|
4
|
+
next: string;
|
|
5
|
+
react: string;
|
|
6
|
+
betterAuth: string;
|
|
7
|
+
evidence: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const VERIFIED_COMBINATIONS: readonly RuntimeCombination[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type PublicConfig } from "./public.js";
|
|
2
|
+
export { ENVIRONMENTS, resolveEnvironmentConfig, validateEnvironmentSelection, assertPostgresBinding } from "./environment.js";
|
|
3
|
+
export type { EnvironmentName, EnvironmentSelection, PostgresBinding } from "./environment.js";
|
|
4
|
+
export declare function describeRegistration(input: {
|
|
5
|
+
issuer: string;
|
|
6
|
+
platformOrigin: string;
|
|
7
|
+
authBasePath?: string;
|
|
8
|
+
}): {
|
|
9
|
+
canonicalIssuer: string;
|
|
10
|
+
platformOrigin: string;
|
|
11
|
+
authBasePath: string;
|
|
12
|
+
providerId: string;
|
|
13
|
+
callbackUrl: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function describeIntegration(input: unknown): PublicConfig;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { canonicalizeIssuer, parseManifest, validateOrigin, validateAuthBasePath, record, keys } from "./public.js";
|
|
3
|
+
export { ENVIRONMENTS, resolveEnvironmentConfig, validateEnvironmentSelection, assertPostgresBinding } from "./environment.js";
|
|
4
|
+
const digest = (s) => createHash("sha256").update(s, "utf8").digest("hex");
|
|
5
|
+
export function describeRegistration(input) {
|
|
6
|
+
keys(record(input), ["issuer", "platformOrigin", "authBasePath"]);
|
|
7
|
+
const canonicalIssuer = canonicalizeIssuer(input.issuer);
|
|
8
|
+
const platformOrigin = validateOrigin(input.platformOrigin);
|
|
9
|
+
const authBasePath = validateAuthBasePath(input.authBasePath);
|
|
10
|
+
const providerId = "user-center-" + digest(canonicalIssuer);
|
|
11
|
+
return { canonicalIssuer, platformOrigin, authBasePath, providerId, callbackUrl: platformOrigin + authBasePath + "/callback/" + providerId };
|
|
12
|
+
}
|
|
13
|
+
export function describeIntegration(input) {
|
|
14
|
+
const manifest = parseManifest(input);
|
|
15
|
+
const registration = describeRegistration({ issuer: manifest.issuer, platformOrigin: manifest.platformOrigin, authBasePath: manifest.authBasePath });
|
|
16
|
+
return { ...manifest, ...registration, centerOrigin: new URL(registration.canonicalIssuer).origin, discoveryUrl: registration.canonicalIssuer + "/.well-known/openid-configuration", cookiePrefix: "uc_" + digest(manifest.platformOrigin + "\n" + registration.canonicalIssuer).slice(0, 24) };
|
|
17
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type Manifest } from "./public.js";
|
|
2
|
+
export declare const ENVIRONMENTS: readonly ["development", "staging", "production"];
|
|
3
|
+
export type EnvironmentName = typeof ENVIRONMENTS[number];
|
|
4
|
+
export interface EnvironmentSelection {
|
|
5
|
+
runtimeEnvironment: EnvironmentName;
|
|
6
|
+
targetEnvironment: EnvironmentName;
|
|
7
|
+
}
|
|
8
|
+
export interface PostgresBinding {
|
|
9
|
+
urlEnv: string;
|
|
10
|
+
hostname: string;
|
|
11
|
+
port: number;
|
|
12
|
+
database: string;
|
|
13
|
+
username: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function validateEnvironmentSelection(input: unknown): Readonly<EnvironmentSelection>;
|
|
16
|
+
export declare function assertPostgresBinding(url: string, input: unknown): void;
|
|
17
|
+
/** Pure selection only. The host reads the selected secret variables on its server. */
|
|
18
|
+
export declare function resolveEnvironmentConfig(input: unknown, selection: {
|
|
19
|
+
runtimeEnvironment: string;
|
|
20
|
+
targetEnvironment?: string;
|
|
21
|
+
}): {
|
|
22
|
+
manifest: Manifest;
|
|
23
|
+
environment: Readonly<EnvironmentSelection>;
|
|
24
|
+
database?: PostgresBinding;
|
|
25
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { IntegrationError, canonicalizeIssuer, keys, parseManifest, record, validateOrigin } from "./public.js";
|
|
2
|
+
export const ENVIRONMENTS = ["development", "staging", "production"];
|
|
3
|
+
const invalid = () => { throw new IntegrationError("ENVIRONMENT_MISMATCH"); };
|
|
4
|
+
function environment(value) {
|
|
5
|
+
if (typeof value !== "string" || !ENVIRONMENTS.includes(value))
|
|
6
|
+
return invalid();
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
function reference(value) {
|
|
10
|
+
if (typeof value !== "string" || !/^[A-Z][A-Z0-9_]{1,127}$/.test(value))
|
|
11
|
+
return invalid();
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
const localHost = (hostname) => ["localhost", "[::1]"].includes(hostname) || /^127\./.test(hostname) || /\.(test|localhost|invalid)$/.test(hostname);
|
|
15
|
+
export function validateEnvironmentSelection(input) {
|
|
16
|
+
const selection = record(input);
|
|
17
|
+
keys(selection, ["runtimeEnvironment", "targetEnvironment"]);
|
|
18
|
+
const runtimeEnvironment = environment(selection.runtimeEnvironment);
|
|
19
|
+
const targetEnvironment = selection.targetEnvironment === undefined ? runtimeEnvironment : environment(selection.targetEnvironment);
|
|
20
|
+
if (runtimeEnvironment !== "development" && targetEnvironment !== runtimeEnvironment)
|
|
21
|
+
invalid();
|
|
22
|
+
return Object.freeze({ runtimeEnvironment, targetEnvironment });
|
|
23
|
+
}
|
|
24
|
+
function postgresBinding(input) {
|
|
25
|
+
const binding = record(input);
|
|
26
|
+
keys(binding, ["urlEnv", "hostname", "port", "database", "username"]);
|
|
27
|
+
const urlEnv = reference(binding.urlEnv);
|
|
28
|
+
for (const key of ["hostname", "database", "username"])
|
|
29
|
+
if (typeof binding[key] !== "string" || !binding[key].trim())
|
|
30
|
+
invalid();
|
|
31
|
+
if (!Number.isInteger(binding.port) || Number(binding.port) < 1 || Number(binding.port) > 65535)
|
|
32
|
+
invalid();
|
|
33
|
+
return { urlEnv, hostname: binding.hostname, port: binding.port, database: binding.database, username: binding.username };
|
|
34
|
+
}
|
|
35
|
+
export function assertPostgresBinding(url, input) {
|
|
36
|
+
const binding = postgresBinding(input);
|
|
37
|
+
try {
|
|
38
|
+
const connection = new URL(url);
|
|
39
|
+
if (connection.hash || [...connection.searchParams.keys()].some(key => !["sslmode", "sslrootcert", "sslcert", "sslkey", "sslpassword", "connect_timeout", "application_name"].includes(key)))
|
|
40
|
+
invalid();
|
|
41
|
+
if (!["postgres:", "postgresql:"].includes(connection.protocol) || connection.hostname.toLowerCase() !== binding.hostname.toLowerCase() || Number(connection.port || 5432) !== binding.port || decodeURIComponent(connection.pathname.slice(1)) !== binding.database || decodeURIComponent(connection.username) !== binding.username)
|
|
42
|
+
invalid();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
invalid();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Pure selection only. The host reads the selected secret variables on its server. */
|
|
49
|
+
export function resolveEnvironmentConfig(input, selection) {
|
|
50
|
+
const selected = validateEnvironmentSelection(selection);
|
|
51
|
+
const config = record(input);
|
|
52
|
+
keys(config, ["schemaVersion", "centers", "runtimes", "authBasePath", "returnTo", "errorPath"]);
|
|
53
|
+
if (config.schemaVersion !== 1)
|
|
54
|
+
invalid();
|
|
55
|
+
const centers = record(config.centers), runtimes = record(config.runtimes);
|
|
56
|
+
keys(centers, ENVIRONMENTS);
|
|
57
|
+
keys(runtimes, ENVIRONMENTS);
|
|
58
|
+
const issuers = new Set(), origins = new Set(), secretRefs = new Set(), databaseRefs = new Set(), databaseIdentities = new Set(), clientIds = new Set();
|
|
59
|
+
const canonicalCenters = {};
|
|
60
|
+
for (const [name, issuer] of Object.entries(centers)) {
|
|
61
|
+
const canonical = canonicalizeIssuer(issuer);
|
|
62
|
+
if (issuers.has(canonical) || (name !== "development" && localHost(new URL(canonical).hostname)))
|
|
63
|
+
invalid();
|
|
64
|
+
issuers.add(canonical);
|
|
65
|
+
canonicalCenters[environment(name)] = canonical;
|
|
66
|
+
}
|
|
67
|
+
let result;
|
|
68
|
+
for (const [runtimeName, value] of Object.entries(runtimes)) {
|
|
69
|
+
const runtime = environment(runtimeName), profile = record(value);
|
|
70
|
+
keys(profile, ["platformOrigin", "sessionSecretEnv", "database", "clients"]);
|
|
71
|
+
const origin = validateOrigin(profile.platformOrigin), sessionSecretEnv = reference(profile.sessionSecretEnv);
|
|
72
|
+
if (origins.has(origin) || secretRefs.has(sessionSecretEnv) || (runtime !== "development" && localHost(new URL(origin).hostname)))
|
|
73
|
+
invalid();
|
|
74
|
+
origins.add(origin);
|
|
75
|
+
secretRefs.add(sessionSecretEnv);
|
|
76
|
+
const database = profile.database === undefined ? undefined : postgresBinding(profile.database);
|
|
77
|
+
if (database) {
|
|
78
|
+
const identity = JSON.stringify([database.hostname.toLowerCase(), database.port, database.database, database.username]);
|
|
79
|
+
if (databaseRefs.has(database.urlEnv) || secretRefs.has(database.urlEnv) || databaseIdentities.has(identity))
|
|
80
|
+
invalid();
|
|
81
|
+
databaseRefs.add(database.urlEnv);
|
|
82
|
+
secretRefs.add(database.urlEnv);
|
|
83
|
+
databaseIdentities.add(identity);
|
|
84
|
+
}
|
|
85
|
+
const clients = record(profile.clients);
|
|
86
|
+
keys(clients, ENVIRONMENTS);
|
|
87
|
+
for (const [targetName, value] of Object.entries(clients)) {
|
|
88
|
+
const target = environment(targetName);
|
|
89
|
+
validateEnvironmentSelection({ runtimeEnvironment: runtime, targetEnvironment: target });
|
|
90
|
+
const client = record(value);
|
|
91
|
+
keys(client, ["clientId", "clientSecretEnv"]);
|
|
92
|
+
const clientSecretEnv = reference(client.clientSecretEnv), issuer = canonicalCenters[target];
|
|
93
|
+
if (!issuer || secretRefs.has(clientSecretEnv))
|
|
94
|
+
invalid();
|
|
95
|
+
secretRefs.add(clientSecretEnv);
|
|
96
|
+
const clientKey = issuer + "\n" + client.clientId;
|
|
97
|
+
if (clientIds.has(clientKey))
|
|
98
|
+
invalid();
|
|
99
|
+
clientIds.add(clientKey);
|
|
100
|
+
const manifest = parseManifest({ schemaVersion: 1, issuer, platformOrigin: origin, clientId: client.clientId, clientSecretEnv, sessionSecretEnv, authBasePath: config.authBasePath, returnTo: config.returnTo, errorPath: config.errorPath });
|
|
101
|
+
if (runtime === selected.runtimeEnvironment && target === selected.targetEnvironment)
|
|
102
|
+
result = { manifest, environment: selected, ...(database ? { database } : {}) };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!result)
|
|
106
|
+
return invalid();
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type PublicConfig } from "./public.js";
|
|
2
|
+
export declare function readPublicJson(url: string, maxBytes: number, failure?: "CENTER_UNAVAILABLE" | "UNTRUSTED_METADATA", headers?: Record<string, string>): Promise<Record<string, unknown>>;
|
|
3
|
+
export declare function validateDiscovery(data: Record<string, unknown>, c: PublicConfig, expectedEnvironment?: string): Record<string, unknown>;
|
|
4
|
+
export declare function validateJwks(data: Record<string, unknown>): void;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { IntegrationError } from "./public.js";
|
|
2
|
+
export async function readPublicJson(url, maxBytes, failure = "CENTER_UNAVAILABLE", headers = {}) {
|
|
3
|
+
const controller = new AbortController();
|
|
4
|
+
const timer = setTimeout(() => controller.abort(), 5000);
|
|
5
|
+
try {
|
|
6
|
+
const response = await fetch(url, { method: "GET", redirect: "error", credentials: "omit", cache: "no-store", signal: controller.signal, headers: { Accept: "application/json", ...headers } });
|
|
7
|
+
if (!response.ok || !response.body)
|
|
8
|
+
throw new IntegrationError(failure);
|
|
9
|
+
const reader = response.body.getReader();
|
|
10
|
+
let size = 0;
|
|
11
|
+
const chunks = [];
|
|
12
|
+
try {
|
|
13
|
+
while (true) {
|
|
14
|
+
const chunk = await reader.read();
|
|
15
|
+
if (chunk.done)
|
|
16
|
+
break;
|
|
17
|
+
size += chunk.value.length;
|
|
18
|
+
if (size > maxBytes)
|
|
19
|
+
throw new IntegrationError("UNTRUSTED_METADATA");
|
|
20
|
+
chunks.push(chunk.value);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
await reader.cancel();
|
|
25
|
+
}
|
|
26
|
+
const bytes = new Uint8Array(size);
|
|
27
|
+
let offset = 0;
|
|
28
|
+
for (const chunk of chunks) {
|
|
29
|
+
bytes.set(chunk, offset);
|
|
30
|
+
offset += chunk.length;
|
|
31
|
+
}
|
|
32
|
+
let data;
|
|
33
|
+
try {
|
|
34
|
+
data = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new IntegrationError("UNTRUSTED_METADATA");
|
|
38
|
+
}
|
|
39
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
40
|
+
throw new IntegrationError("UNTRUSTED_METADATA");
|
|
41
|
+
return data;
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
throw e instanceof IntegrationError ? e : new IntegrationError(failure);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function validateDiscovery(data, c, expectedEnvironment) {
|
|
51
|
+
if (expectedEnvironment !== undefined && data.user_center_environment !== expectedEnvironment)
|
|
52
|
+
throw new IntegrationError("ENVIRONMENT_MISMATCH");
|
|
53
|
+
const invalid = () => { throw new IntegrationError("UNTRUSTED_METADATA"); };
|
|
54
|
+
if (data.issuer !== c.canonicalIssuer)
|
|
55
|
+
invalid();
|
|
56
|
+
for (const [key, path] of Object.entries({ authorization_endpoint: "/oauth2/authorize", token_endpoint: "/oauth2/token", userinfo_endpoint: "/oauth2/userinfo", jwks_uri: "/jwks" }))
|
|
57
|
+
if (data[key] !== c.canonicalIssuer + path)
|
|
58
|
+
invalid();
|
|
59
|
+
for (const [key, values] of Object.entries({ response_types_supported: ["code"], grant_types_supported: ["authorization_code"], code_challenge_methods_supported: ["S256"], scopes_supported: ["openid", "profile", "email"] }))
|
|
60
|
+
if (!Array.isArray(data[key]) || values.some(x => !data[key].includes(x)))
|
|
61
|
+
invalid();
|
|
62
|
+
if (!Array.isArray(data.id_token_signing_alg_values_supported) || !data.id_token_signing_alg_values_supported.length || data.id_token_signing_alg_values_supported.some(a => typeof a !== "string" || !/^(?:RS(?:256|384|512)|PS(?:256|384|512)|ES(?:256|384|512)|EdDSA|Ed25519)$/.test(a)))
|
|
63
|
+
invalid();
|
|
64
|
+
if (data.token_endpoint_auth_methods_supported !== undefined && (!Array.isArray(data.token_endpoint_auth_methods_supported) || !data.token_endpoint_auth_methods_supported.includes("client_secret_basic")))
|
|
65
|
+
invalid();
|
|
66
|
+
return data;
|
|
67
|
+
}
|
|
68
|
+
export function validateJwks(data) {
|
|
69
|
+
if (!Array.isArray(data.keys) || !data.keys.length || data.keys.some(k => !k || typeof k !== "object" || Array.isArray(k) || typeof k.kty !== "string" || ["d", "p", "q", "dp", "dq", "qi", "oth", "k"].some(p => p in k) || !((k.kty === "RSA" && typeof k.n === "string" && k.n && typeof k.e === "string" && k.e) || ((k.kty === "EC" || k.kty === "OKP") && typeof k.crv === "string" && k.crv && typeof k.x === "string" && k.x && (k.kty === "OKP" || (typeof k.y === "string" && k.y))))))
|
|
70
|
+
throw new IntegrationError("UNTRUSTED_METADATA");
|
|
71
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export declare const TOOL_VERSION = "0.1.8";
|
|
2
|
+
export declare const ERROR_CODES: readonly ["INVALID_CONFIG", "ENVIRONMENT_MISMATCH", "INVALID_RETURN_TO", "INVALID_LOGIN_REQUEST", "LOGIN_UNAVAILABLE", "SIGN_OUT_UNCONFIRMED", "CENTER_UNAVAILABLE", "UNTRUSTED_METADATA", "DEPENDENCY_UNSUPPORTED", "IDENTITY_CONFLICT", "ACCOUNT_CONFLICT", "BUSINESS_RESOLUTION_FAILED"];
|
|
3
|
+
export type ErrorCode = typeof ERROR_CODES[number];
|
|
4
|
+
export declare class IntegrationError extends Error {
|
|
5
|
+
readonly code: ErrorCode;
|
|
6
|
+
constructor(code: ErrorCode);
|
|
7
|
+
}
|
|
8
|
+
export interface ReturnPolicy {
|
|
9
|
+
fallback: string;
|
|
10
|
+
allowedPaths: string[];
|
|
11
|
+
allowedPathPrefixes: string[];
|
|
12
|
+
allowedQueryKeys: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface Manifest {
|
|
15
|
+
schemaVersion: 1;
|
|
16
|
+
issuer: string;
|
|
17
|
+
platformOrigin: string;
|
|
18
|
+
clientId: string;
|
|
19
|
+
authBasePath: string;
|
|
20
|
+
clientSecretEnv: string;
|
|
21
|
+
sessionSecretEnv: string;
|
|
22
|
+
returnTo: ReturnPolicy;
|
|
23
|
+
errorPath: string;
|
|
24
|
+
}
|
|
25
|
+
export interface PublicConfig extends Manifest {
|
|
26
|
+
canonicalIssuer: string;
|
|
27
|
+
centerOrigin: string;
|
|
28
|
+
providerId: string;
|
|
29
|
+
callbackUrl: string;
|
|
30
|
+
discoveryUrl: string;
|
|
31
|
+
cookiePrefix: string;
|
|
32
|
+
}
|
|
33
|
+
export interface SafeSession {
|
|
34
|
+
user: {
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
email: string;
|
|
38
|
+
emailVerified: boolean;
|
|
39
|
+
};
|
|
40
|
+
expiresAt: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function record(value: unknown): Record<string, unknown>;
|
|
43
|
+
export declare function keys(value: Record<string, unknown>, allowed: readonly string[]): void;
|
|
44
|
+
export declare function canonicalizeIssuer(input: unknown): string;
|
|
45
|
+
export declare function validateOrigin(input: unknown): string;
|
|
46
|
+
export declare function validateAuthBasePath(input?: unknown): string;
|
|
47
|
+
export declare function validateReturnTo(value: unknown, config: Pick<Manifest, "returnTo" | "authBasePath">): string;
|
|
48
|
+
export declare function parseManifest(input: unknown): Manifest;
|
|
49
|
+
export declare function parseLoginRequest(input: unknown, config: Manifest): {
|
|
50
|
+
returnTo: string;
|
|
51
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
export const TOOL_VERSION = "0.1.8";
|
|
2
|
+
export const ERROR_CODES = ["INVALID_CONFIG", "ENVIRONMENT_MISMATCH", "INVALID_RETURN_TO", "INVALID_LOGIN_REQUEST", "LOGIN_UNAVAILABLE", "SIGN_OUT_UNCONFIRMED", "CENTER_UNAVAILABLE", "UNTRUSTED_METADATA", "DEPENDENCY_UNSUPPORTED", "IDENTITY_CONFLICT", "ACCOUNT_CONFLICT", "BUSINESS_RESOLUTION_FAILED"];
|
|
3
|
+
export class IntegrationError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code) {
|
|
6
|
+
super(code);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.name = "IntegrationError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const bad = () => { throw new IntegrationError("INVALID_CONFIG"); };
|
|
12
|
+
export function record(value) {
|
|
13
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
14
|
+
return bad();
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
export function keys(value, allowed) { if (Object.keys(value).some(k => !allowed.includes(k)))
|
|
18
|
+
bad(); }
|
|
19
|
+
const clean = (s, max) => typeof s === "string" && s.length > 0 && s.length <= max && !/[\s\u0000-\u001f\u007f\\]/u.test(s);
|
|
20
|
+
export function canonicalizeIssuer(input) {
|
|
21
|
+
if (!clean(input, 2048) || /[?#]/.test(input))
|
|
22
|
+
return bad();
|
|
23
|
+
const match = /^https:\/\/([^/]+)(.*)$/i.exec(input);
|
|
24
|
+
if (!match || match[1].includes("@"))
|
|
25
|
+
return bad();
|
|
26
|
+
const path = match[2];
|
|
27
|
+
if (path && path !== "/" && (!/^\/[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*$/.test(path) || path.split("/").some(x => x === "." || x === "..")))
|
|
28
|
+
return bad();
|
|
29
|
+
try {
|
|
30
|
+
const u = new URL(input);
|
|
31
|
+
if (u.protocol !== "https:" || !u.hostname || u.username || u.password)
|
|
32
|
+
return bad();
|
|
33
|
+
return u.origin + (path === "/" ? "" : path);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return bad();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function validateOrigin(input) {
|
|
40
|
+
if (!clean(input, 2048))
|
|
41
|
+
return bad();
|
|
42
|
+
try {
|
|
43
|
+
const u = new URL(input);
|
|
44
|
+
if (u.protocol !== "https:" || input !== u.origin || u.hostname === "localhost" || u.hostname.endsWith(".localhost") || /^127\./.test(u.hostname) || u.hostname === "[::1]")
|
|
45
|
+
return bad();
|
|
46
|
+
return input;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return bad();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function localPath(input) {
|
|
53
|
+
if (!clean(input, 2048) || !input.startsWith("/") || input.includes("//") || /[?#]/.test(input) || /%(?:2f|5c|25)/i.test(input))
|
|
54
|
+
return false;
|
|
55
|
+
try {
|
|
56
|
+
return !input.split("/").some(s => { const p = decodeURIComponent(s); return p === "." || p === ".." || /[\u0000-\u001f\u007f\\/]/.test(p); });
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function validateAuthBasePath(input = "/api/auth") {
|
|
63
|
+
if (!localPath(input) || input.length > 256 || input.endsWith("/"))
|
|
64
|
+
return bad();
|
|
65
|
+
return input;
|
|
66
|
+
}
|
|
67
|
+
const reserved = /^(?:code|state|token|secret|password|flow|authorization|cookie|session|nonce|access_token|refresh_token|id_token|client_secret|code_verifier|code_challenge|sessiontoken)$/i;
|
|
68
|
+
function strings(value) {
|
|
69
|
+
if (!Array.isArray(value) || value.some(s => typeof s !== "string") || new Set(value).size !== value.length)
|
|
70
|
+
return bad();
|
|
71
|
+
return [...value];
|
|
72
|
+
}
|
|
73
|
+
export function validateReturnTo(value, config) {
|
|
74
|
+
const fail = () => { throw new IntegrationError("INVALID_RETURN_TO"); };
|
|
75
|
+
if (value === undefined)
|
|
76
|
+
value = config.returnTo.fallback;
|
|
77
|
+
if (!clean(value, 2048) || value.includes("#"))
|
|
78
|
+
return fail();
|
|
79
|
+
const split = value.indexOf("?");
|
|
80
|
+
const path = split < 0 ? value : value.slice(0, split);
|
|
81
|
+
if (!localPath(path) || path === config.authBasePath || path.startsWith(config.authBasePath + "/"))
|
|
82
|
+
return fail();
|
|
83
|
+
const p = config.returnTo;
|
|
84
|
+
if (!p.allowedPaths.includes(path) && !p.allowedPathPrefixes.some(prefix => prefix === "/" || path === prefix || path.startsWith(prefix + "/")))
|
|
85
|
+
return fail();
|
|
86
|
+
const query = new URLSearchParams(split < 0 ? "" : value.slice(split + 1));
|
|
87
|
+
const seen = new Set();
|
|
88
|
+
for (const [k, v] of query) {
|
|
89
|
+
if (seen.has(k) || reserved.test(k) || !p.allowedQueryKeys.includes(k) || /[\u0000-\u001f\u007f]/.test(k + v))
|
|
90
|
+
return fail();
|
|
91
|
+
seen.add(k);
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
export function parseManifest(input) {
|
|
96
|
+
const m = record(input);
|
|
97
|
+
keys(m, ["schemaVersion", "issuer", "platformOrigin", "clientId", "authBasePath", "clientSecretEnv", "sessionSecretEnv", "returnTo", "errorPath"]);
|
|
98
|
+
if (m.schemaVersion !== 1 || typeof m.clientId !== "string" || m.clientId.length < 1 || m.clientId.length > 256 || /[\u0000-\u001f\u007f]/.test(m.clientId) || !m.clientId.trim())
|
|
99
|
+
return bad();
|
|
100
|
+
const issuer = canonicalizeIssuer(m.issuer);
|
|
101
|
+
const platformOrigin = validateOrigin(m.platformOrigin);
|
|
102
|
+
const authBasePath = validateAuthBasePath(m.authBasePath);
|
|
103
|
+
for (const field of [m.clientSecretEnv, m.sessionSecretEnv])
|
|
104
|
+
if (typeof field !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(field))
|
|
105
|
+
return bad();
|
|
106
|
+
if (m.clientSecretEnv === m.sessionSecretEnv)
|
|
107
|
+
return bad();
|
|
108
|
+
const r = record(m.returnTo);
|
|
109
|
+
keys(r, ["fallback", "allowedPaths", "allowedPathPrefixes", "allowedQueryKeys"]);
|
|
110
|
+
const returnTo = { fallback: r.fallback, allowedPaths: strings(r.allowedPaths ?? []), allowedPathPrefixes: strings(r.allowedPathPrefixes ?? []), allowedQueryKeys: strings(r.allowedQueryKeys ?? []) };
|
|
111
|
+
if (!returnTo.allowedPaths.length && !returnTo.allowedPathPrefixes.length)
|
|
112
|
+
return bad();
|
|
113
|
+
for (const path of [...returnTo.allowedPaths, ...returnTo.allowedPathPrefixes])
|
|
114
|
+
if (!localPath(path) || (path !== "/" && path.endsWith("/")))
|
|
115
|
+
return bad();
|
|
116
|
+
for (const key of returnTo.allowedQueryKeys)
|
|
117
|
+
if (!/^[A-Za-z_][A-Za-z0-9_.-]*$/.test(key) || reserved.test(key))
|
|
118
|
+
return bad();
|
|
119
|
+
if (!localPath(m.errorPath) || m.errorPath === authBasePath || m.errorPath.startsWith(authBasePath + "/"))
|
|
120
|
+
return bad();
|
|
121
|
+
const manifest = { schemaVersion: 1, issuer, platformOrigin, clientId: m.clientId, authBasePath, clientSecretEnv: m.clientSecretEnv, sessionSecretEnv: m.sessionSecretEnv, returnTo, errorPath: m.errorPath };
|
|
122
|
+
try {
|
|
123
|
+
validateReturnTo(returnTo.fallback, manifest);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return bad();
|
|
127
|
+
}
|
|
128
|
+
return manifest;
|
|
129
|
+
}
|
|
130
|
+
export function parseLoginRequest(input, config) {
|
|
131
|
+
if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some(k => k !== "returnTo") || ("returnTo" in input && typeof input.returnTo !== "string"))
|
|
132
|
+
throw new IntegrationError("INVALID_LOGIN_REQUEST");
|
|
133
|
+
return { returnTo: validateReturnTo(input.returnTo, config) };
|
|
134
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type PublicConfig, type SafeSession } from "../../integration-contracts/src/public.js";
|
|
2
|
+
export declare function createUserCenterClient(publicConfig: PublicConfig): {
|
|
3
|
+
startLogin(input?: {
|
|
4
|
+
returnTo?: string;
|
|
5
|
+
}): Promise<{
|
|
6
|
+
url: string;
|
|
7
|
+
}>;
|
|
8
|
+
getSession(): Promise<SafeSession | null>;
|
|
9
|
+
signOut(): Promise<void>;
|
|
10
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { IntegrationError, parseManifest, parseLoginRequest } from "../../integration-contracts/src/public.js";
|
|
2
|
+
export function createUserCenterClient(publicConfig) {
|
|
3
|
+
const config = parseManifest({ schemaVersion: publicConfig.schemaVersion, issuer: publicConfig.issuer, platformOrigin: publicConfig.platformOrigin, clientId: publicConfig.clientId, authBasePath: publicConfig.authBasePath, clientSecretEnv: publicConfig.clientSecretEnv, sessionSecretEnv: publicConfig.sessionSecretEnv, returnTo: publicConfig.returnTo, errorPath: publicConfig.errorPath });
|
|
4
|
+
async function call(path, method, code, body) {
|
|
5
|
+
if (globalThis.location?.origin !== config.platformOrigin)
|
|
6
|
+
throw new IntegrationError(code);
|
|
7
|
+
try {
|
|
8
|
+
const r = await fetch(config.authBasePath + path, { method, credentials: "same-origin", cache: "no-store", redirect: "error", headers: body === undefined ? {} : { "Content-Type": "application/json" }, ...(body === undefined ? {} : { body: JSON.stringify(body) }) });
|
|
9
|
+
if (!r.ok)
|
|
10
|
+
throw new IntegrationError(code);
|
|
11
|
+
return await r.json();
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
throw new IntegrationError(code);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
async startLogin(input = {}) {
|
|
19
|
+
const body = parseLoginRequest(input, config);
|
|
20
|
+
const data = await call("/sign-in/social", "POST", "LOGIN_UNAVAILABLE", body);
|
|
21
|
+
try {
|
|
22
|
+
const url = new URL(data.url);
|
|
23
|
+
const issuer = new URL(config.issuer);
|
|
24
|
+
if (url.origin !== issuer.origin || url.pathname !== issuer.pathname.replace(/\/$/, "") + "/oauth2/authorize")
|
|
25
|
+
throw new Error();
|
|
26
|
+
return { url: url.href };
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new IntegrationError("LOGIN_UNAVAILABLE");
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
async getSession() { return await call("/get-session", "GET", "CENTER_UNAVAILABLE"); },
|
|
33
|
+
async signOut() { const data = await call("/sign-out", "POST", "SIGN_OUT_UNCONFIRMED", {}); if (data?.success !== true)
|
|
34
|
+
throw new IntegrationError("SIGN_OUT_UNCONFIRMED"); },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../integration-contracts/src/config.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../integration-contracts/src/public.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../integration-contracts/src/public.js";
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
import { type BetterAuthOptions } from "better-auth";
|
|
3
|
+
import type { OAuthProvider } from "better-auth/oauth2";
|
|
4
|
+
import { type EnvironmentSelection } from "../../integration-contracts/src/environment.js";
|
|
5
|
+
import { type ErrorCode, type PublicConfig } from "../../integration-contracts/src/public.js";
|
|
6
|
+
export interface Identity {
|
|
7
|
+
issuer: string;
|
|
8
|
+
subject: string;
|
|
9
|
+
authUserId: string;
|
|
10
|
+
profile: {
|
|
11
|
+
name: string;
|
|
12
|
+
email: string;
|
|
13
|
+
emailVerified: boolean;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export type { SafeSession } from "../../integration-contracts/src/public.js";
|
|
17
|
+
export type Principal<T> = {
|
|
18
|
+
status: "anonymous";
|
|
19
|
+
} | {
|
|
20
|
+
status: "mapping-required";
|
|
21
|
+
identity: Identity;
|
|
22
|
+
} | {
|
|
23
|
+
status: "authenticated";
|
|
24
|
+
identity: Identity;
|
|
25
|
+
user: T;
|
|
26
|
+
};
|
|
27
|
+
export type ResolutionContext = Readonly<{
|
|
28
|
+
requestId: string;
|
|
29
|
+
signal: AbortSignal;
|
|
30
|
+
}>;
|
|
31
|
+
export type Event = Readonly<{
|
|
32
|
+
code: ErrorCode | "OK";
|
|
33
|
+
phase: "ready" | "identity" | "business" | "login" | "callback" | "logout";
|
|
34
|
+
at: string;
|
|
35
|
+
requestId: string;
|
|
36
|
+
version: string;
|
|
37
|
+
}>;
|
|
38
|
+
export interface AuthOptions<T extends {
|
|
39
|
+
id: string;
|
|
40
|
+
}> {
|
|
41
|
+
manifest: unknown;
|
|
42
|
+
database: NonNullable<BetterAuthOptions["database"]>;
|
|
43
|
+
clientSecret: string;
|
|
44
|
+
sessionSecret: string;
|
|
45
|
+
environment?: EnvironmentSelection;
|
|
46
|
+
authUserPolicy: "allow-new" | "existing-only";
|
|
47
|
+
sessionTtlSeconds?: number;
|
|
48
|
+
resolveBusinessUser: (identity: Identity, context: ResolutionContext) => Promise<{
|
|
49
|
+
status: "ready";
|
|
50
|
+
user: T;
|
|
51
|
+
} | {
|
|
52
|
+
status: "unmapped";
|
|
53
|
+
}>;
|
|
54
|
+
onEvent?: (event: Event) => unknown;
|
|
55
|
+
}
|
|
56
|
+
export declare function createUserCenterAuth<T extends {
|
|
57
|
+
id: string;
|
|
58
|
+
}>(options: AuthOptions<T>): {
|
|
59
|
+
publicConfig: PublicConfig;
|
|
60
|
+
ready: () => Promise<void>;
|
|
61
|
+
getAuth: () => Promise<import("better-auth").Auth<{
|
|
62
|
+
baseURL: string;
|
|
63
|
+
basePath: string;
|
|
64
|
+
secret: string;
|
|
65
|
+
database: any;
|
|
66
|
+
trustedOrigins: string[];
|
|
67
|
+
emailAndPassword: {
|
|
68
|
+
enabled: false;
|
|
69
|
+
};
|
|
70
|
+
user: {
|
|
71
|
+
deleteUser: {
|
|
72
|
+
enabled: false;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
session: {
|
|
76
|
+
expiresIn: number;
|
|
77
|
+
disableSessionRefresh: true;
|
|
78
|
+
cookieCache: {
|
|
79
|
+
enabled: false;
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
account: {
|
|
83
|
+
accountLinking: {
|
|
84
|
+
enabled: false;
|
|
85
|
+
};
|
|
86
|
+
encryptOAuthTokens: true;
|
|
87
|
+
};
|
|
88
|
+
advanced: {
|
|
89
|
+
cookiePrefix: string;
|
|
90
|
+
useSecureCookies: true;
|
|
91
|
+
crossSubDomainCookies: {
|
|
92
|
+
enabled: false;
|
|
93
|
+
};
|
|
94
|
+
defaultCookieAttributes: {
|
|
95
|
+
secure: true;
|
|
96
|
+
httpOnly: true;
|
|
97
|
+
sameSite: "lax";
|
|
98
|
+
path: string;
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
onAPIError: {
|
|
102
|
+
errorURL: string;
|
|
103
|
+
};
|
|
104
|
+
logger: {
|
|
105
|
+
disabled: true;
|
|
106
|
+
};
|
|
107
|
+
rateLimit: {
|
|
108
|
+
enabled: true;
|
|
109
|
+
storage: "memory";
|
|
110
|
+
};
|
|
111
|
+
plugins: [{
|
|
112
|
+
init: (ctx: import("better-auth").AuthContext) => Promise<{
|
|
113
|
+
context: {
|
|
114
|
+
socialProviders: OAuthProvider<object, Partial<import("better-auth").ProviderOptions<object>>>[];
|
|
115
|
+
genericOAuthPendingDiscovery: Map<string, {
|
|
116
|
+
config: import("better-auth/plugins/generic-oauth").GenericOAuthConfig;
|
|
117
|
+
attempt?: Promise<void> | undefined;
|
|
118
|
+
}>;
|
|
119
|
+
};
|
|
120
|
+
}>;
|
|
121
|
+
hooks: {
|
|
122
|
+
before: {
|
|
123
|
+
matcher: (context: import("better-auth").HookEndpointContext) => boolean;
|
|
124
|
+
handler: import("better-auth").Middleware<import("better-auth").MiddlewareOptions, (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<void>>;
|
|
125
|
+
}[];
|
|
126
|
+
};
|
|
127
|
+
id: "generic-oauth";
|
|
128
|
+
version: string;
|
|
129
|
+
options: import("better-auth/plugins/generic-oauth").GenericOAuthOptions<string>;
|
|
130
|
+
$ERROR_CODES: {
|
|
131
|
+
INVALID_OAUTH_CONFIGURATION: import("better-auth").RawError<"INVALID_OAUTH_CONFIGURATION">;
|
|
132
|
+
TOKEN_URL_NOT_FOUND: import("better-auth").RawError<"TOKEN_URL_NOT_FOUND">;
|
|
133
|
+
};
|
|
134
|
+
}];
|
|
135
|
+
}>>;
|
|
136
|
+
getIdentity: (headers: Headers) => Promise<Identity | null>;
|
|
137
|
+
getPrincipal: (headers: Headers) => Promise<Principal<T>>;
|
|
138
|
+
handlers: {
|
|
139
|
+
GET: (request: Request) => Promise<Response>;
|
|
140
|
+
POST: (request: Request) => Promise<Response>;
|
|
141
|
+
};
|
|
142
|
+
};
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { betterAuth } from "better-auth";
|
|
4
|
+
import { genericOAuth } from "better-auth/plugins/generic-oauth";
|
|
5
|
+
import { createAuthMiddleware } from "better-auth/api";
|
|
6
|
+
import { createRemoteJWKSet, customFetch } from "jose";
|
|
7
|
+
import { describeIntegration } from "../../integration-contracts/src/config.js";
|
|
8
|
+
import { validateEnvironmentSelection } from "../../integration-contracts/src/environment.js";
|
|
9
|
+
import { IntegrationError, TOOL_VERSION, parseLoginRequest, validateReturnTo } from "../../integration-contracts/src/public.js";
|
|
10
|
+
import { readPublicJson, validateDiscovery, validateJwks } from "../../integration-contracts/src/metadata.js";
|
|
11
|
+
const safeHeaders = { "Cache-Control": "no-store", "Referrer-Policy": "no-referrer" };
|
|
12
|
+
const json = (data, status = 200, extra = {}) => Response.json(data, { status, headers: { ...safeHeaders, ...extra } });
|
|
13
|
+
function bounded(work, ms, code, onTimeout) {
|
|
14
|
+
let timer;
|
|
15
|
+
return Promise.race([work, new Promise((_, reject) => { timer = setTimeout(() => { onTimeout?.(); reject(new IntegrationError(code)); }, ms); })]).finally(() => clearTimeout(timer));
|
|
16
|
+
}
|
|
17
|
+
function claims(idToken, c) {
|
|
18
|
+
try {
|
|
19
|
+
const data = JSON.parse(Buffer.from(idToken.split(".")[1], "base64url").toString("utf8"));
|
|
20
|
+
if (data.iss !== c.canonicalIssuer || typeof data.sub !== "string" || !data.sub.trim())
|
|
21
|
+
return null;
|
|
22
|
+
return data;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function createUserCenterAuth(options) {
|
|
29
|
+
const c = describeIntegration(options.manifest);
|
|
30
|
+
const environment = options.environment === undefined ? undefined : validateEnvironmentSelection(options.environment);
|
|
31
|
+
if (!options.database || typeof options.clientSecret !== "string" || !options.clientSecret.trim() || typeof options.sessionSecret !== "string" || !options.sessionSecret.trim() || options.sessionSecret.length < 32 || options.sessionSecret === options.clientSecret || !["allow-new", "existing-only"].includes(options.authUserPolicy) || typeof options.resolveBusinessUser !== "function")
|
|
32
|
+
throw new IntegrationError("INVALID_CONFIG");
|
|
33
|
+
const ttl = options.sessionTtlSeconds ?? 86400;
|
|
34
|
+
if (!Number.isInteger(ttl) || ttl < 300 || ttl > 86400)
|
|
35
|
+
throw new IntegrationError("INVALID_CONFIG");
|
|
36
|
+
// Capture explicit server values. Mutating the caller's object cannot change policy later.
|
|
37
|
+
const { database, clientSecret, sessionSecret, resolveBusinessUser, onEvent } = options;
|
|
38
|
+
const existingOnly = options.authUserPolicy === "existing-only";
|
|
39
|
+
function emit(phase, code, requestId = randomUUID()) {
|
|
40
|
+
try {
|
|
41
|
+
void Promise.resolve(onEvent?.(Object.freeze({ phase, code, requestId, at: new Date().toISOString(), version: TOOL_VERSION }))).catch(() => { });
|
|
42
|
+
}
|
|
43
|
+
catch { /* Observation must not change authentication. */ }
|
|
44
|
+
}
|
|
45
|
+
function buildAuth(metadata, state) {
|
|
46
|
+
const algorithms = metadata.id_token_signing_alg_values_supported;
|
|
47
|
+
const jwksUrl = c.canonicalIssuer + "/jwks";
|
|
48
|
+
const jwks = createRemoteJWKSet(new URL(jwksUrl), { timeoutDuration: 5000, [customFetch]: async (url) => {
|
|
49
|
+
if (url !== jwksUrl)
|
|
50
|
+
throw new IntegrationError("UNTRUSTED_METADATA");
|
|
51
|
+
const data = await readPublicJson(jwksUrl, 1048576);
|
|
52
|
+
validateJwks(data);
|
|
53
|
+
return Response.json(data);
|
|
54
|
+
} });
|
|
55
|
+
function guardProviders(providers) {
|
|
56
|
+
if (state.failure)
|
|
57
|
+
throw state.failure;
|
|
58
|
+
function reject(code) {
|
|
59
|
+
const error = new IntegrationError(code);
|
|
60
|
+
state.invalidate(error);
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
const matching = providers.filter(p => p.id === c.providerId);
|
|
64
|
+
// A partial official initialization must never become a reusable instance.
|
|
65
|
+
if (!matching.length)
|
|
66
|
+
reject("CENTER_UNAVAILABLE");
|
|
67
|
+
if (matching.length !== 1)
|
|
68
|
+
reject("UNTRUSTED_METADATA");
|
|
69
|
+
for (const provider of matching) {
|
|
70
|
+
if (provider.issuer !== c.canonicalIssuer || !provider.idToken || !("jwks" in provider.idToken) || provider.requiresIdTokenNonce !== true || !provider.idToken.algorithms || provider.idToken.algorithms.length !== algorithms.length || provider.idToken.algorithms.some(a => !algorithms.includes(a)))
|
|
71
|
+
reject("UNTRUSTED_METADATA");
|
|
72
|
+
// Mutate the public provider object in place: its official verifier closes over it.
|
|
73
|
+
provider.idToken.jwks = jwks;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const plugin = genericOAuth({ config: [{
|
|
77
|
+
providerId: c.providerId, name: "User Center", clientId: c.clientId, clientSecret,
|
|
78
|
+
discoveryUrl: c.discoveryUrl, authorizationUrl: c.canonicalIssuer + "/oauth2/authorize", tokenUrl: c.canonicalIssuer + "/oauth2/token", userInfoUrl: c.canonicalIssuer + "/oauth2/userinfo",
|
|
79
|
+
scopes: ["openid", "profile", "email"], responseType: "code", responseMode: "query", pkce: true, requireIdTokenVerification: true, disableIdTokenNonceBinding: false,
|
|
80
|
+
tokenEndpointAuth: { method: "client_secret_basic" }, allowIdpInitiated: false, disableProviderLogout: true, disableSignUp: existingOnly, disableImplicitSignUp: existingOnly,
|
|
81
|
+
async getUserInfo(tokens) {
|
|
82
|
+
// This official extension runs only after the provider verifies signature and nonce.
|
|
83
|
+
const verified = claims(tokens.idToken, c);
|
|
84
|
+
if (!verified || !tokens.accessToken)
|
|
85
|
+
return null;
|
|
86
|
+
try {
|
|
87
|
+
const data = await readPublicJson(c.canonicalIssuer + "/oauth2/userinfo", 65536, "CENTER_UNAVAILABLE", { Authorization: `Bearer ${tokens.accessToken}` });
|
|
88
|
+
if (data.sub !== verified.sub || data.email_verified !== true || typeof data.email !== "string" || !data.email.trim() || typeof data.name !== "string")
|
|
89
|
+
return null;
|
|
90
|
+
return { sub: verified.sub, id: verified.sub, email: data.email, emailVerified: true, name: data.name };
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
accountSubject({ tokens, profile }) { const verified = claims(tokens.idToken, c); return verified && verified.sub === profile.sub ? verified.sub : ""; },
|
|
97
|
+
}] });
|
|
98
|
+
// Guard every API call, including retained auth objects, before and after official hooks.
|
|
99
|
+
const providerGuard = { matcher: () => true, handler: createAuthMiddleware(async (ctx) => { guardProviders(ctx.context.socialProviders); }) };
|
|
100
|
+
const guardedPlugin = { ...plugin, init: async (...args) => {
|
|
101
|
+
const result = await plugin.init(...args);
|
|
102
|
+
guardProviders(result.context.socialProviders);
|
|
103
|
+
return result;
|
|
104
|
+
}, hooks: { ...plugin.hooks, before: [providerGuard, ...plugin.hooks.before, providerGuard] } };
|
|
105
|
+
const auth = betterAuth({
|
|
106
|
+
baseURL: c.platformOrigin, basePath: c.authBasePath, secret: sessionSecret, database, trustedOrigins: [c.platformOrigin],
|
|
107
|
+
emailAndPassword: { enabled: false }, user: { deleteUser: { enabled: false } },
|
|
108
|
+
session: { expiresIn: ttl, disableSessionRefresh: true, cookieCache: { enabled: false } },
|
|
109
|
+
account: { accountLinking: { enabled: false }, encryptOAuthTokens: true },
|
|
110
|
+
advanced: { cookiePrefix: c.cookiePrefix, useSecureCookies: true, crossSubDomainCookies: { enabled: false }, defaultCookieAttributes: { secure: true, httpOnly: true, sameSite: "lax", path: "/" } },
|
|
111
|
+
onAPIError: { errorURL: c.platformOrigin + c.errorPath }, logger: { disabled: true }, rateLimit: { enabled: true, storage: "memory" }, plugins: [guardedPlugin],
|
|
112
|
+
});
|
|
113
|
+
return { auth, assertTrusted: async () => { guardProviders((await auth.$context).socialProviders); } };
|
|
114
|
+
}
|
|
115
|
+
let preparation;
|
|
116
|
+
async function loadAuth() {
|
|
117
|
+
if (!preparation) {
|
|
118
|
+
const state = { invalidate(error) {
|
|
119
|
+
state.failure ??= error;
|
|
120
|
+
if (preparation === attempt)
|
|
121
|
+
preparation = undefined;
|
|
122
|
+
} };
|
|
123
|
+
const work = (async () => {
|
|
124
|
+
const metadata = validateDiscovery(await readPublicJson(c.discoveryUrl, 1048576), c, environment?.targetEnvironment);
|
|
125
|
+
if (state.failure)
|
|
126
|
+
throw state.failure;
|
|
127
|
+
const prepared = buildAuth(metadata, state);
|
|
128
|
+
await prepared.assertTrusted();
|
|
129
|
+
return prepared;
|
|
130
|
+
})();
|
|
131
|
+
// All callers share one deadline; an expired generation cannot poison a later one.
|
|
132
|
+
const attempt = bounded(work, 10000, "CENTER_UNAVAILABLE", () => state.invalidate(new IntegrationError("CENTER_UNAVAILABLE")));
|
|
133
|
+
preparation = attempt;
|
|
134
|
+
void attempt.catch(error => state.invalidate(error instanceof IntegrationError ? error : new IntegrationError("CENTER_UNAVAILABLE")));
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const prepared = await preparation;
|
|
138
|
+
await prepared.assertTrusted();
|
|
139
|
+
return prepared.auth;
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
const error = e instanceof IntegrationError ? e : new IntegrationError("CENTER_UNAVAILABLE");
|
|
143
|
+
emit("ready", error.code);
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function getAuth() {
|
|
148
|
+
return loadAuth();
|
|
149
|
+
}
|
|
150
|
+
async function getIdentity(headers) {
|
|
151
|
+
try {
|
|
152
|
+
const auth = await getAuth();
|
|
153
|
+
const session = await auth.api.getSession({ headers });
|
|
154
|
+
if (!session)
|
|
155
|
+
return null;
|
|
156
|
+
const accounts = await auth.api.listUserAccounts({ headers });
|
|
157
|
+
const matches = accounts.filter(a => a.providerId === c.providerId);
|
|
158
|
+
if (!matches.length)
|
|
159
|
+
return null;
|
|
160
|
+
if (matches.length !== 1 || matches[0].userId !== session.user.id || typeof matches[0].accountId !== "string" || !matches[0].accountId.trim())
|
|
161
|
+
throw new IntegrationError("IDENTITY_CONFLICT");
|
|
162
|
+
return { issuer: c.canonicalIssuer, subject: matches[0].accountId, authUserId: session.user.id, profile: { name: session.user.name, email: session.user.email, emailVerified: session.user.emailVerified } };
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
throw e instanceof IntegrationError ? e : new IntegrationError("CENTER_UNAVAILABLE");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function getPrincipal(headers) {
|
|
169
|
+
const identity = await getIdentity(headers);
|
|
170
|
+
if (!identity)
|
|
171
|
+
return { status: "anonymous" };
|
|
172
|
+
const requestId = randomUUID();
|
|
173
|
+
const controller = new AbortController();
|
|
174
|
+
try {
|
|
175
|
+
const result = await bounded(Promise.resolve().then(() => resolveBusinessUser(identity, Object.freeze({ requestId, signal: controller.signal }))), 5000, "BUSINESS_RESOLUTION_FAILED", () => controller.abort());
|
|
176
|
+
if (result?.status === "unmapped")
|
|
177
|
+
return { status: "mapping-required", identity };
|
|
178
|
+
if (result?.status !== "ready" || typeof result.user?.id !== "string" || !result.user.id.trim())
|
|
179
|
+
throw new IntegrationError("BUSINESS_RESOLUTION_FAILED");
|
|
180
|
+
emit("business", "OK", requestId);
|
|
181
|
+
return { status: "authenticated", identity, user: result.user };
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
emit("business", "BUSINESS_RESOLUTION_FAILED", requestId);
|
|
185
|
+
throw new IntegrationError("BUSINESS_RESOLUTION_FAILED");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function safeSession(headers) {
|
|
189
|
+
const auth = await getAuth();
|
|
190
|
+
const session = await auth.api.getSession({ headers });
|
|
191
|
+
return session ? { user: { id: session.user.id, name: session.user.name, email: session.user.email, emailVerified: session.user.emailVerified }, expiresAt: session.session.expiresAt.toISOString() } : null;
|
|
192
|
+
}
|
|
193
|
+
function redirectError(code, response) {
|
|
194
|
+
const headers = new Headers(safeHeaders);
|
|
195
|
+
headers.set("Location", c.platformOrigin + c.errorPath + "?code=" + code);
|
|
196
|
+
// Preserve state cleanup, but never deliver a session on a rejected redirect.
|
|
197
|
+
if (response)
|
|
198
|
+
for (const cookie of response.headers.getSetCookie())
|
|
199
|
+
if (!cookie.includes("session_token="))
|
|
200
|
+
headers.append("Set-Cookie", cookie);
|
|
201
|
+
return new Response(null, { status: 302, headers });
|
|
202
|
+
}
|
|
203
|
+
async function handle(request) {
|
|
204
|
+
const url = new URL(request.url);
|
|
205
|
+
const path = url.pathname.slice(c.authBasePath.length);
|
|
206
|
+
if (url.origin !== c.platformOrigin || !url.pathname.startsWith(c.authBasePath + "/"))
|
|
207
|
+
return json({ code: "NOT_FOUND" }, 404);
|
|
208
|
+
const methods = { "/sign-in/social": "POST", "/get-session": "GET", "/sign-out": "POST", ["/callback/" + c.providerId]: "GET" };
|
|
209
|
+
if (!methods[path])
|
|
210
|
+
return json({ code: "NOT_FOUND" }, 404);
|
|
211
|
+
if (request.method !== methods[path])
|
|
212
|
+
return json({ code: "METHOD_NOT_ALLOWED" }, 405, { Allow: methods[path] });
|
|
213
|
+
if (request.method === "POST" && (request.headers.get("Origin") !== c.platformOrigin || request.headers.get("Sec-Fetch-Site") === "cross-site"))
|
|
214
|
+
return json({ code: "FORBIDDEN" }, 403);
|
|
215
|
+
try {
|
|
216
|
+
if (path === "/sign-in/social") {
|
|
217
|
+
if (url.search || !/^application\/json(?:;|$)/i.test(request.headers.get("Content-Type") ?? ""))
|
|
218
|
+
return json({ code: "INVALID_LOGIN_REQUEST" }, 400);
|
|
219
|
+
const text = await request.text();
|
|
220
|
+
if (text.length > 4096)
|
|
221
|
+
return json({ code: "INVALID_LOGIN_REQUEST" }, 400);
|
|
222
|
+
let body;
|
|
223
|
+
try {
|
|
224
|
+
body = JSON.parse(text);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return json({ code: "INVALID_LOGIN_REQUEST" }, 400);
|
|
228
|
+
}
|
|
229
|
+
const { returnTo } = parseLoginRequest(body, c);
|
|
230
|
+
const rebuilt = JSON.stringify({ provider: c.providerId, callbackURL: c.platformOrigin + returnTo, newUserCallbackURL: c.platformOrigin + returnTo, errorCallbackURL: c.platformOrigin + c.errorPath, disableRedirect: true });
|
|
231
|
+
const headers = new Headers(request.headers);
|
|
232
|
+
headers.delete("Content-Length");
|
|
233
|
+
headers.delete("Transfer-Encoding");
|
|
234
|
+
headers.set("Content-Type", "application/json");
|
|
235
|
+
const response = await (await loadAuth()).handler(new Request(request.url, { method: "POST", headers, body: rebuilt }));
|
|
236
|
+
if (!response.ok)
|
|
237
|
+
return json({ code: "LOGIN_UNAVAILABLE" }, response.status >= 500 ? 503 : 400);
|
|
238
|
+
const data = await response.json();
|
|
239
|
+
const target = new URL(data.url);
|
|
240
|
+
if (target.origin !== c.centerOrigin || target.pathname !== new URL(c.canonicalIssuer + "/oauth2/authorize").pathname || target.username || target.password || target.hash)
|
|
241
|
+
throw new IntegrationError("LOGIN_UNAVAILABLE");
|
|
242
|
+
const out = json({ url: target.href });
|
|
243
|
+
for (const cookie of response.headers.getSetCookie())
|
|
244
|
+
out.headers.append("Set-Cookie", cookie);
|
|
245
|
+
emit("login", "OK");
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
if (path === "/get-session")
|
|
249
|
+
return json(await safeSession(request.headers));
|
|
250
|
+
if (path.startsWith("/callback/")) {
|
|
251
|
+
if (["code", "state", "error"].some(k => url.searchParams.getAll(k).length > 1))
|
|
252
|
+
return redirectError("LOGIN_UNAVAILABLE");
|
|
253
|
+
const response = await (await loadAuth()).handler(request);
|
|
254
|
+
const location = response.headers.get("Location");
|
|
255
|
+
if (!location)
|
|
256
|
+
return redirectError("LOGIN_UNAVAILABLE", response);
|
|
257
|
+
const target = new URL(location, c.platformOrigin);
|
|
258
|
+
if (target.origin !== c.platformOrigin || target.username || target.password || target.hash)
|
|
259
|
+
return redirectError("LOGIN_UNAVAILABLE", response);
|
|
260
|
+
if (target.pathname === c.errorPath)
|
|
261
|
+
return redirectError(target.searchParams.get("error") === "account_not_linked" ? "ACCOUNT_CONFLICT" : "LOGIN_UNAVAILABLE", response);
|
|
262
|
+
try {
|
|
263
|
+
validateReturnTo(target.pathname + target.search, c);
|
|
264
|
+
if (target.hash)
|
|
265
|
+
throw new Error();
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return redirectError("INVALID_RETURN_TO", response);
|
|
269
|
+
}
|
|
270
|
+
const headers = new Headers(response.headers);
|
|
271
|
+
for (const [k, v] of Object.entries(safeHeaders))
|
|
272
|
+
headers.set(k, v);
|
|
273
|
+
emit("callback", "OK");
|
|
274
|
+
return new Response(null, { status: response.status, headers });
|
|
275
|
+
}
|
|
276
|
+
const auth = await getAuth();
|
|
277
|
+
const response = await auth.handler(request);
|
|
278
|
+
let confirmed = false;
|
|
279
|
+
if (response.ok)
|
|
280
|
+
try {
|
|
281
|
+
confirmed = await auth.api.getSession({ headers: request.headers, query: { disableCookieCache: true, disableRefresh: true } }) === null;
|
|
282
|
+
}
|
|
283
|
+
catch { /* A failed confirmation read must not declare revocation. */ }
|
|
284
|
+
const out = confirmed ? json({ success: true }) : json({ code: "SIGN_OUT_UNCONFIRMED" }, 503);
|
|
285
|
+
for (const cookie of response.headers.getSetCookie())
|
|
286
|
+
out.headers.append("Set-Cookie", cookie);
|
|
287
|
+
emit("logout", confirmed ? "OK" : "SIGN_OUT_UNCONFIRMED");
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
catch (e) {
|
|
291
|
+
const code = e instanceof IntegrationError ? e.code : "CENTER_UNAVAILABLE";
|
|
292
|
+
if (path.startsWith("/callback/"))
|
|
293
|
+
return redirectError(code);
|
|
294
|
+
return json({ code }, ["INVALID_LOGIN_REQUEST", "INVALID_RETURN_TO"].includes(code) ? 400 : code === "IDENTITY_CONFLICT" ? 409 : 503);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return { publicConfig: describeIntegration(options.manifest), ready: async () => { await getAuth(); emit("ready", "OK"); }, getAuth, getIdentity, getPrincipal, handlers: { GET: handle, POST: handle } };
|
|
298
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@user-center/next-better-auth",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.8",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist",
|
|
7
|
+
"README.md",
|
|
8
|
+
"LICENSE"
|
|
9
|
+
],
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=22"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/next-better-auth/src/public.d.ts",
|
|
16
|
+
"import": "./dist/next-better-auth/src/public.js"
|
|
17
|
+
},
|
|
18
|
+
"./public": {
|
|
19
|
+
"types": "./dist/next-better-auth/src/public.d.ts",
|
|
20
|
+
"import": "./dist/next-better-auth/src/public.js"
|
|
21
|
+
},
|
|
22
|
+
"./config": {
|
|
23
|
+
"types": "./dist/next-better-auth/src/config.d.ts",
|
|
24
|
+
"import": "./dist/next-better-auth/src/config.js"
|
|
25
|
+
},
|
|
26
|
+
"./server": {
|
|
27
|
+
"types": "./dist/next-better-auth/src/server.d.ts",
|
|
28
|
+
"import": "./dist/next-better-auth/src/server.js"
|
|
29
|
+
},
|
|
30
|
+
"./client": {
|
|
31
|
+
"types": "./dist/next-better-auth/src/client.d.ts",
|
|
32
|
+
"import": "./dist/next-better-auth/src/client.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"server-only": "0.0.1",
|
|
37
|
+
"jose": "6.2.13"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"better-auth": "1.7.12"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"better-auth": "1.7.12",
|
|
44
|
+
"typescript": "^5",
|
|
45
|
+
"@types/node": "^24"
|
|
46
|
+
},
|
|
47
|
+
"description": "Controlled User Center authentication integration for Next App Router and Better Auth.",
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/ZXFTech/user-center.git",
|
|
52
|
+
"directory": "packages/next-better-auth"
|
|
53
|
+
},
|
|
54
|
+
"homepage": "https://github.com/ZXFTech/user-center#readme",
|
|
55
|
+
"bugs": {
|
|
56
|
+
"url": "https://github.com/ZXFTech/user-center/issues"
|
|
57
|
+
},
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"registry": "https://registry.npmjs.org/",
|
|
60
|
+
"access": "public",
|
|
61
|
+
"tag": "next"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "node ../../scripts/build-integration.mjs next-better-auth",
|
|
65
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
66
|
+
}
|
|
6
67
|
}
|