@procedures/better-auth 0.0.0-bootstrap.0 → 0.12.1
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 +75 -1
- package/dist/.tsbuildinfo +1 -0
- package/dist/auth.d.ts +22 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +40 -0
- package/dist/auth.js.map +1 -0
- package/dist/config.d.ts +46 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +44 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/principal-of.d.ts +51 -0
- package/dist/principal-of.d.ts.map +1 -0
- package/dist/principal-of.js +84 -0
- package/dist/principal-of.js.map +1 -0
- package/dist/principals.d.ts +59 -0
- package/dist/principals.d.ts.map +1 -0
- package/dist/principals.js +112 -0
- package/dist/principals.js.map +1 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/package.json +59 -6
- package/src/auth.ts +55 -0
- package/src/better-auth.test.ts +274 -0
- package/src/config.ts +77 -0
- package/src/index.ts +5 -0
- package/src/principal-of.ts +104 -0
- package/src/principals.ts +150 -0
- package/src/types.ts +7 -0
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Db, MongoClient } from "mongodb";
|
|
2
|
+
import type { BetterAuthConfig } from "./config.js";
|
|
3
|
+
/**
|
|
4
|
+
* The parts of a Better Auth server that an app uses. Mount `handler` on `config.basePath`, for
|
|
5
|
+
* example with `toNodeHandler` of `better-auth/node`. `SessionPrincipals` reads `api.getSession`.
|
|
6
|
+
*/
|
|
7
|
+
export interface BetterAuthServer {
|
|
8
|
+
/** The handler of the auth routes. */
|
|
9
|
+
readonly handler: (request: Request) => Promise<Response>;
|
|
10
|
+
readonly api: {
|
|
11
|
+
/** The session and the user of the cookies of a request, or null. */
|
|
12
|
+
getSession(context: {
|
|
13
|
+
headers: Headers;
|
|
14
|
+
}): Promise<{
|
|
15
|
+
user: Record<string, unknown>;
|
|
16
|
+
session: Record<string, unknown>;
|
|
17
|
+
} | null>;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** This function makes the Better Auth server of a configuration. */
|
|
21
|
+
export declare function createBetterAuth(db: Db, client: MongoClient, config: BetterAuthConfig): BetterAuthServer;
|
|
22
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,sCAAsC;IACtC,QAAQ,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1D,QAAQ,CAAC,GAAG,EAAE;QACZ,qEAAqE;QACrE,UAAU,CAAC,OAAO,EAAE;YAAE,OAAO,EAAE,OAAO,CAAA;SAAE,GAAG,OAAO,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;YAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;SAAE,GAAG,IAAI,CAAC,CAAC;KAChI,CAAC;CACH;AAED,qEAAqE;AACrE,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,CAyBxG"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Better Auth server over MongoDB: the sign-in with GitHub and Google, the sessions in `httpOnly`
|
|
3
|
+
* cookies, the groups (the organization plugin) and the passkeys (WebAuthn). The app stores no
|
|
4
|
+
* password. Better Auth does the OAuth state, PKCE and the CSRF checks.
|
|
5
|
+
*
|
|
6
|
+
* Give Better Auth its own database: its `user` collection must not mix with the data of the app.
|
|
7
|
+
* The app mounts `auth.handler` on `config.basePath`: the routes belong to the app.
|
|
8
|
+
*/
|
|
9
|
+
import { betterAuth } from "better-auth";
|
|
10
|
+
import { mongodbAdapter } from "better-auth/adapters/mongodb";
|
|
11
|
+
import { organization } from "better-auth/plugins";
|
|
12
|
+
import { passkey } from "@better-auth/passkey";
|
|
13
|
+
/** This function makes the Better Auth server of a configuration. */
|
|
14
|
+
export function createBetterAuth(db, client, config) {
|
|
15
|
+
const testHooks = {
|
|
16
|
+
user: {
|
|
17
|
+
create: {
|
|
18
|
+
before: async (user) => ({ data: { ...user, emailVerified: true } }),
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
return betterAuth({
|
|
23
|
+
appName: config.appName,
|
|
24
|
+
basePath: config.basePath,
|
|
25
|
+
baseURL: config.baseURL,
|
|
26
|
+
secret: config.secret,
|
|
27
|
+
database: mongodbAdapter(db, { client }),
|
|
28
|
+
trustedOrigins: [...config.trustedOrigins],
|
|
29
|
+
socialProviders: {
|
|
30
|
+
...(config.github ? { github: { ...config.github } } : {}),
|
|
31
|
+
...(config.google ? { google: { ...config.google } } : {}),
|
|
32
|
+
},
|
|
33
|
+
emailAndPassword: { enabled: config.testAccounts },
|
|
34
|
+
// A test account has no email check: the test server trusts its email
|
|
35
|
+
...(config.testAccounts ? { databaseHooks: testHooks } : {}),
|
|
36
|
+
// The passkeys expect the origins of the app, not the Origin header of each request
|
|
37
|
+
plugins: [organization(), passkey({ rpID: config.rpID, rpName: config.appName, origin: [...config.trustedOrigins] })],
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAiB/C,qEAAqE;AACrE,MAAM,UAAU,gBAAgB,CAAC,EAAM,EAAE,MAAmB,EAAE,MAAwB;IACpF,MAAM,SAAS,GAAG;QAChB,IAAI,EAAE;YACJ,MAAM,EAAE;gBACN,MAAM,EAAE,KAAK,EAAE,IAA6B,EAA8C,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,GAAG,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,EAAE,CAAC;aAC1I;SACF;KACF,CAAC;IACF,OAAO,UAAU,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,QAAQ,EAAE,cAAc,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC;QACxC,cAAc,EAAE,CAAC,GAAG,MAAM,CAAC,cAAc,CAAC;QAC1C,eAAe,EAAE;YACf,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC1D,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC3D;QACD,gBAAgB,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,YAAY,EAAE;QAClD,sEAAsE;QACtE,GAAG,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5D,oFAAoF;QACpF,OAAO,EAAE,CAAC,YAAY,EAAE,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,GAAG,MAAM,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC;KACtH,CAAC,CAAC;AACL,CAAC"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The configuration of a Better Auth server from the environment.
|
|
3
|
+
*
|
|
4
|
+
* - `BETTER_AUTH_SECRET`: the secret that signs the session cookies (16 characters or more).
|
|
5
|
+
* - `AUTH_BASE_URL`: the public origin of the server (default `http://localhost:<port>`).
|
|
6
|
+
* - `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET`: the sign-in with GitHub.
|
|
7
|
+
* - `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`: the sign-in with Google.
|
|
8
|
+
* - `AUTH_TEST_ACCOUNTS=1`: accounts with an email and a password, for the tests. Production refuses it.
|
|
9
|
+
* - `PASSKEY_RP_ID`: the domain of the passkeys (default: the host of the first trusted origin).
|
|
10
|
+
*/
|
|
11
|
+
/** The client id and the secret of an OAuth provider. */
|
|
12
|
+
export interface OAuthClient {
|
|
13
|
+
readonly clientId: string;
|
|
14
|
+
readonly clientSecret: string;
|
|
15
|
+
}
|
|
16
|
+
/** The configuration that `createBetterAuth` reads. */
|
|
17
|
+
export interface BetterAuthConfig {
|
|
18
|
+
/** The name of the app. Passkeys show it. */
|
|
19
|
+
readonly appName: string;
|
|
20
|
+
/** The path of the auth routes (default `/auth`). */
|
|
21
|
+
readonly basePath: string;
|
|
22
|
+
readonly secret: string;
|
|
23
|
+
readonly baseURL: string;
|
|
24
|
+
/** The origins of the app. The CSRF checks and the passkeys accept only these origins. */
|
|
25
|
+
readonly trustedOrigins: readonly string[];
|
|
26
|
+
readonly github?: OAuthClient | undefined;
|
|
27
|
+
readonly google?: OAuthClient | undefined;
|
|
28
|
+
/** Accounts with an email and a password, for the tests. Their email counts as verified. */
|
|
29
|
+
readonly testAccounts: boolean;
|
|
30
|
+
/** The relying party of the passkeys: a domain of the origins of the app. */
|
|
31
|
+
readonly rpID: string;
|
|
32
|
+
}
|
|
33
|
+
/** The options of `betterAuthConfig`. */
|
|
34
|
+
export interface BetterAuthConfigOptions {
|
|
35
|
+
/** The port of the server, for the default base URL. */
|
|
36
|
+
readonly port: number;
|
|
37
|
+
/** The origins of the app. */
|
|
38
|
+
readonly trustedOrigins: readonly string[];
|
|
39
|
+
/** The name of the app (default "App"). */
|
|
40
|
+
readonly appName?: string | undefined;
|
|
41
|
+
/** The path of the auth routes (default `/auth`). */
|
|
42
|
+
readonly basePath?: string | undefined;
|
|
43
|
+
}
|
|
44
|
+
/** This function reads the configuration from the environment. It gives a message when the server cannot start. */
|
|
45
|
+
export declare function betterAuthConfig(env: Readonly<Record<string, string | undefined>>, options: BetterAuthConfigOptions): BetterAuthConfig | string;
|
|
46
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,yDAAyD;AACzD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,uDAAuD;AACvD,MAAM,WAAW,gBAAgB;IAC/B,6CAA6C;IAC7C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,qDAAqD;IACrD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,0FAA0F;IAC1F,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAC1C,4FAA4F;IAC5F,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,yCAAyC;AACzC,MAAM,WAAW,uBAAuB;IACtC,wDAAwD;IACxD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8BAA8B;IAC9B,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,2CAA2C;IAC3C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,qDAAqD;IACrD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC;AAED,mHAAmH;AACnH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,uBAAuB,GAAG,gBAAgB,GAAG,MAAM,CA4B/I"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The configuration of a Better Auth server from the environment.
|
|
3
|
+
*
|
|
4
|
+
* - `BETTER_AUTH_SECRET`: the secret that signs the session cookies (16 characters or more).
|
|
5
|
+
* - `AUTH_BASE_URL`: the public origin of the server (default `http://localhost:<port>`).
|
|
6
|
+
* - `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET`: the sign-in with GitHub.
|
|
7
|
+
* - `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`: the sign-in with Google.
|
|
8
|
+
* - `AUTH_TEST_ACCOUNTS=1`: accounts with an email and a password, for the tests. Production refuses it.
|
|
9
|
+
* - `PASSKEY_RP_ID`: the domain of the passkeys (default: the host of the first trusted origin).
|
|
10
|
+
*/
|
|
11
|
+
/** This function reads the configuration from the environment. It gives a message when the server cannot start. */
|
|
12
|
+
export function betterAuthConfig(env, options) {
|
|
13
|
+
const secret = env["BETTER_AUTH_SECRET"] ?? "";
|
|
14
|
+
if (secret.length < 16)
|
|
15
|
+
return "BETTER_AUTH_SECRET must have 16 characters or more";
|
|
16
|
+
if (env["NODE_ENV"] === "production" && env["AUTH_TEST_ACCOUNTS"] === "1")
|
|
17
|
+
return "AUTH_TEST_ACCOUNTS is refused in production";
|
|
18
|
+
const pair = (id, key) => {
|
|
19
|
+
const clientId = env[id];
|
|
20
|
+
const clientSecret = env[key];
|
|
21
|
+
return clientId && clientSecret ? { clientId, clientSecret } : undefined;
|
|
22
|
+
};
|
|
23
|
+
let firstHost = "localhost";
|
|
24
|
+
try {
|
|
25
|
+
firstHost = new URL(options.trustedOrigins[0] ?? "http://localhost").hostname;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// An origin that is not a URL: the passkeys use localhost
|
|
29
|
+
}
|
|
30
|
+
const github = pair("GITHUB_CLIENT_ID", "GITHUB_CLIENT_SECRET");
|
|
31
|
+
const google = pair("GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET");
|
|
32
|
+
return {
|
|
33
|
+
appName: options.appName ?? "App",
|
|
34
|
+
basePath: options.basePath ?? "/auth",
|
|
35
|
+
secret,
|
|
36
|
+
baseURL: env["AUTH_BASE_URL"] || `http://localhost:${options.port}`,
|
|
37
|
+
trustedOrigins: options.trustedOrigins,
|
|
38
|
+
...(github ? { github } : {}),
|
|
39
|
+
...(google ? { google } : {}),
|
|
40
|
+
testAccounts: env["AUTH_TEST_ACCOUNTS"] === "1",
|
|
41
|
+
rpID: env["PASSKEY_RP_ID"] || firstHost,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAsCH,mHAAmH;AACnH,MAAM,UAAU,gBAAgB,CAAC,GAAiD,EAAE,OAAgC;IAClH,MAAM,MAAM,GAAG,GAAG,CAAC,oBAAoB,CAAC,IAAI,EAAE,CAAC;IAC/C,IAAI,MAAM,CAAC,MAAM,GAAG,EAAE;QAAE,OAAO,oDAAoD,CAAC;IACpF,IAAI,GAAG,CAAC,UAAU,CAAC,KAAK,YAAY,IAAI,GAAG,CAAC,oBAAoB,CAAC,KAAK,GAAG;QAAE,OAAO,6CAA6C,CAAC;IAChI,MAAM,IAAI,GAAG,CAAC,EAAU,EAAE,GAAW,EAA2B,EAAE;QAChE,MAAM,QAAQ,GAAG,GAAG,CAAC,EAAE,CAAC,CAAC;QACzB,MAAM,YAAY,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;QAC9B,OAAO,QAAQ,IAAI,YAAY,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAC3E,CAAC,CAAC;IACF,IAAI,SAAS,GAAG,WAAW,CAAC;IAC5B,IAAI,CAAC;QACH,SAAS,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,kBAAkB,CAAC,CAAC,QAAQ,CAAC;IAChF,CAAC;IAAC,MAAM,CAAC;QACP,0DAA0D;IAC5D,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,kBAAkB,EAAE,sBAAsB,CAAC,CAAC;IAChE,MAAM,MAAM,GAAG,IAAI,CAAC,kBAAkB,EAAE,sBAAsB,CAAC,CAAC;IAChE,OAAO;QACL,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,KAAK;QACjC,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,OAAO;QACrC,MAAM;QACN,OAAO,EAAE,GAAG,CAAC,eAAe,CAAC,IAAI,oBAAoB,OAAO,CAAC,IAAI,EAAE;QACnE,cAAc,EAAE,OAAO,CAAC,cAAc;QACtC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7B,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7B,YAAY,EAAE,GAAG,CAAC,oBAAoB,CAAC,KAAK,GAAG;QAC/C,IAAI,EAAE,GAAG,CAAC,eAAe,CAAC,IAAI,SAAS;KACxC,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The request hook of a procedures server (`principalOf`) over Better Auth. It gives the principal
|
|
3
|
+
* of a network request:
|
|
4
|
+
*
|
|
5
|
+
* - The agent token in the bearer token: the principal `agent`. With the act-as header, the agent
|
|
6
|
+
* acts for that user, with the rights of the user and no more. The `actAs` policy tells for whom.
|
|
7
|
+
* Without the policy, the hook refuses each act-as request.
|
|
8
|
+
* - Else the session cookie: the principal of the signed-in user.
|
|
9
|
+
* - Else the anonymous principal with the `anonymous` narrowing. Without that option, the hook gives
|
|
10
|
+
* no principal, and the server refuses the request.
|
|
11
|
+
*
|
|
12
|
+
* A bearer token that is not the agent token goes on to the token check of the server. A server
|
|
13
|
+
* with this hook sets no `token`: its token check runs after the hook, and a cookie session has no
|
|
14
|
+
* bearer token.
|
|
15
|
+
*/
|
|
16
|
+
import { type CapabilityRequest, type Credentials, type PrincipalOf } from "@procedures/client";
|
|
17
|
+
import type { SessionPrincipals, SignedInUser } from "./principals.js";
|
|
18
|
+
/** The `via` of each principal that the hook gives. */
|
|
19
|
+
export declare const VIA: {
|
|
20
|
+
readonly session: "better-auth";
|
|
21
|
+
readonly agent: "agent-token";
|
|
22
|
+
readonly agentAs: "agent-as";
|
|
23
|
+
readonly anonymous: "anonymous";
|
|
24
|
+
};
|
|
25
|
+
/** The id of the principal of the agent token. */
|
|
26
|
+
export declare const AGENT_ID = "agent";
|
|
27
|
+
/** The id of the principal of a request without a user. */
|
|
28
|
+
export declare const ANONYMOUS_ID = "anonymous";
|
|
29
|
+
/** The policy of act-as: true when the agent can act for this user. */
|
|
30
|
+
export type ActAsPolicy = (user: SignedInUser, credentials: Credentials) => boolean | Promise<boolean>;
|
|
31
|
+
/** The options of `betterAuthPrincipalOf`. */
|
|
32
|
+
export interface BetterAuthPrincipalOfOptions {
|
|
33
|
+
/** The users of the sessions. */
|
|
34
|
+
readonly principals: SessionPrincipals;
|
|
35
|
+
/** The token of the agent. Without it, no request is the agent. */
|
|
36
|
+
readonly agentToken?: string | undefined;
|
|
37
|
+
/** The header that names the user for whom the agent acts (default `x-act-as`). */
|
|
38
|
+
readonly actAsHeader?: string | undefined;
|
|
39
|
+
/** For whom the agent can act. Without it, the hook refuses each act-as request. */
|
|
40
|
+
readonly actAs?: ActAsPolicy | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* The narrowing of a request without a user (for example `{ effects: { writes: [] } }` for read
|
|
43
|
+
* only). Without it (the default), the hook gives no principal, and the server refuses the request.
|
|
44
|
+
*/
|
|
45
|
+
readonly anonymous?: CapabilityRequest | false | undefined;
|
|
46
|
+
}
|
|
47
|
+
/** This function makes the headers of a request from its credentials, for the session lookup. */
|
|
48
|
+
export declare function headersOf(credentials: Credentials): Headers;
|
|
49
|
+
/** This function gives the request hook of a procedures server over Better Auth. */
|
|
50
|
+
export declare function betterAuthPrincipalOf(options: BetterAuthPrincipalOfOptions): PrincipalOf;
|
|
51
|
+
//# sourceMappingURL=principal-of.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"principal-of.d.ts","sourceRoot":"","sources":["../src/principal-of.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAgB,KAAK,iBAAiB,EAAE,KAAK,WAAW,EAAkB,KAAK,WAAW,EAAwB,MAAM,oBAAoB,CAAC;AACpJ,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEvE,uDAAuD;AACvD,eAAO,MAAM,GAAG;aACd,OAAO,EAAE,aAAa;aACtB,KAAK,EAAE,aAAa;aACpB,OAAO,EAAE,UAAU;aACnB,SAAS,EAAE,WAAW;CACd,CAAC;AAEX,kDAAkD;AAClD,eAAO,MAAM,QAAQ,UAAU,CAAC;AAChC,2DAA2D;AAC3D,eAAO,MAAM,YAAY,cAAc,CAAC;AAExC,uEAAuE;AACvE,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEvG,8CAA8C;AAC9C,MAAM,WAAW,4BAA4B;IAC3C,iCAAiC;IACjC,QAAQ,CAAC,UAAU,EAAE,iBAAiB,CAAC;IACvC,mEAAmE;IACnE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,mFAAmF;IACnF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,oFAAoF;IACpF,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IACzC;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,iBAAiB,GAAG,KAAK,GAAG,SAAS,CAAC;CAC5D;AAOD,iGAAiG;AACjG,wBAAgB,SAAS,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAgB3D;AAED,oFAAoF;AACpF,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,WAAW,CA2BxF"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The request hook of a procedures server (`principalOf`) over Better Auth. It gives the principal
|
|
3
|
+
* of a network request:
|
|
4
|
+
*
|
|
5
|
+
* - The agent token in the bearer token: the principal `agent`. With the act-as header, the agent
|
|
6
|
+
* acts for that user, with the rights of the user and no more. The `actAs` policy tells for whom.
|
|
7
|
+
* Without the policy, the hook refuses each act-as request.
|
|
8
|
+
* - Else the session cookie: the principal of the signed-in user.
|
|
9
|
+
* - Else the anonymous principal with the `anonymous` narrowing. Without that option, the hook gives
|
|
10
|
+
* no principal, and the server refuses the request.
|
|
11
|
+
*
|
|
12
|
+
* A bearer token that is not the agent token goes on to the token check of the server. A server
|
|
13
|
+
* with this hook sets no `token`: its token check runs after the hook, and a cookie session has no
|
|
14
|
+
* bearer token.
|
|
15
|
+
*/
|
|
16
|
+
import { tokenMatches } from "@procedures/client";
|
|
17
|
+
/** The `via` of each principal that the hook gives. */
|
|
18
|
+
export const VIA = {
|
|
19
|
+
session: "better-auth",
|
|
20
|
+
agent: "agent-token",
|
|
21
|
+
agentAs: "agent-as",
|
|
22
|
+
anonymous: "anonymous",
|
|
23
|
+
};
|
|
24
|
+
/** The id of the principal of the agent token. */
|
|
25
|
+
export const AGENT_ID = "agent";
|
|
26
|
+
/** The id of the principal of a request without a user. */
|
|
27
|
+
export const ANONYMOUS_ID = "anonymous";
|
|
28
|
+
/** The claims of a user: the verified email, the name and the groups. */
|
|
29
|
+
function userClaims(user) {
|
|
30
|
+
return { ...(user.email ? { email: user.email } : {}), ...(user.name ? { name: user.name } : {}), groups: user.groups };
|
|
31
|
+
}
|
|
32
|
+
/** This function makes the headers of a request from its credentials, for the session lookup. */
|
|
33
|
+
export function headersOf(credentials) {
|
|
34
|
+
const headers = new Headers();
|
|
35
|
+
for (const [name, value] of Object.entries(credentials.headers ?? {})) {
|
|
36
|
+
try {
|
|
37
|
+
headers.set(name, value);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// A header name that the Fetch API refuses: the session lookup does not need it
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!headers.has("cookie") && credentials.cookies) {
|
|
44
|
+
const cookie = Object.entries(credentials.cookies)
|
|
45
|
+
.map(([name, value]) => `${name}=${encodeURIComponent(value)}`)
|
|
46
|
+
.join("; ");
|
|
47
|
+
if (cookie)
|
|
48
|
+
headers.set("cookie", cookie);
|
|
49
|
+
}
|
|
50
|
+
return headers;
|
|
51
|
+
}
|
|
52
|
+
/** This function gives the request hook of a procedures server over Better Auth. */
|
|
53
|
+
export function betterAuthPrincipalOf(options) {
|
|
54
|
+
const actAsHeader = (options.actAsHeader ?? "x-act-as").toLowerCase();
|
|
55
|
+
const anonymous = options.anonymous === false ? undefined : options.anonymous;
|
|
56
|
+
/** The identity of a request without a user, or undefined when the server refuses it. */
|
|
57
|
+
const anonymousIdentity = (claims) => anonymous === undefined
|
|
58
|
+
? undefined
|
|
59
|
+
: // The via is `anonymous` also for an agent that acts for an unknown user: the via decides the subject (refer to subjectOf of @procedures/access)
|
|
60
|
+
{ principal: { id: ANONYMOUS_ID, via: VIA.anonymous, ...(claims ? { claims } : {}) }, narrowing: anonymous };
|
|
61
|
+
const identity = (principal) => ({ principal });
|
|
62
|
+
return async (credentials) => {
|
|
63
|
+
if (options.agentToken && tokenMatches(options.agentToken, credentials.bearer)) {
|
|
64
|
+
const target = credentials.headers?.[actAsHeader];
|
|
65
|
+
if (!target)
|
|
66
|
+
return identity({ id: AGENT_ID, via: VIA.agent, claims: { agent: true } });
|
|
67
|
+
if (!options.actAs)
|
|
68
|
+
return undefined;
|
|
69
|
+
const user = await options.principals.ofUser(target);
|
|
70
|
+
// An unknown user has no rights: the agent acts as an anonymous request, and a refused
|
|
71
|
+
// act-as gives no principal (never the bare agent, which is a service account)
|
|
72
|
+
if (!user)
|
|
73
|
+
return anonymousIdentity({ agent: true, actAs: target });
|
|
74
|
+
if (!(await options.actAs(user, credentials)))
|
|
75
|
+
return undefined;
|
|
76
|
+
return identity({ id: user.id, via: VIA.agentAs, claims: { ...userClaims(user), agent: true, actAs: user.id } });
|
|
77
|
+
}
|
|
78
|
+
const user = await options.principals.ofHeaders(headersOf(credentials));
|
|
79
|
+
if (user)
|
|
80
|
+
return identity({ id: user.id, via: VIA.session, claims: userClaims(user) });
|
|
81
|
+
return anonymousIdentity();
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=principal-of.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"principal-of.js","sourceRoot":"","sources":["../src/principal-of.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The person of a request: the session cookie gives the user, and the `member` records of the
|
|
3
|
+
* organization plugin give the groups. A short memo keeps the boot of a page, which sends many
|
|
4
|
+
* requests at the same time, to one session lookup.
|
|
5
|
+
*/
|
|
6
|
+
import { type Db } from "mongodb";
|
|
7
|
+
/** A group of a user: an organization of the organization plugin, with the role of the user. */
|
|
8
|
+
export interface UserGroup {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly role: string;
|
|
11
|
+
}
|
|
12
|
+
/** A signed-in user. `email` is present only when it is verified. */
|
|
13
|
+
export interface SignedInUser {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly email?: string | undefined;
|
|
16
|
+
readonly name?: string | undefined;
|
|
17
|
+
readonly groups: readonly UserGroup[];
|
|
18
|
+
}
|
|
19
|
+
/** The part of a Better Auth server that `SessionPrincipals` uses. */
|
|
20
|
+
export interface SessionSource {
|
|
21
|
+
readonly api: {
|
|
22
|
+
getSession(context: {
|
|
23
|
+
headers: Headers;
|
|
24
|
+
}): Promise<{
|
|
25
|
+
user?: unknown;
|
|
26
|
+
} | null | undefined>;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** The options of `SessionPrincipals`. */
|
|
30
|
+
export interface SessionPrincipalsOptions {
|
|
31
|
+
/** The Better Auth server. */
|
|
32
|
+
readonly auth: SessionSource;
|
|
33
|
+
/** The database of Better Auth: its `user` and `member` collections. */
|
|
34
|
+
readonly db: Db;
|
|
35
|
+
/** Use an email that is not verified, for a test server only (default false). */
|
|
36
|
+
readonly trustUnverifiedEmail?: boolean | undefined;
|
|
37
|
+
/** The time that a session lookup stays, in milliseconds (default 5000). */
|
|
38
|
+
readonly sessionMemoMs?: number | undefined;
|
|
39
|
+
/** The time that a groups lookup stays, in milliseconds (default 10000). */
|
|
40
|
+
readonly groupsMemoMs?: number | undefined;
|
|
41
|
+
/** The clock (default `Date.now`). The tests give their own. */
|
|
42
|
+
readonly now?: (() => number) | undefined;
|
|
43
|
+
/** The function that reports a failed lookup (default `console.warn`). */
|
|
44
|
+
readonly warn?: ((message: string) => void) | undefined;
|
|
45
|
+
}
|
|
46
|
+
/** The users of the sessions of the requests. */
|
|
47
|
+
export declare class SessionPrincipals {
|
|
48
|
+
#private;
|
|
49
|
+
constructor(options: SessionPrincipalsOptions);
|
|
50
|
+
/** This method gives the user of the session cookie of the headers, or undefined when there is no session. */
|
|
51
|
+
ofHeaders(headers: Headers): Promise<SignedInUser | undefined>;
|
|
52
|
+
/** This method gives a user by id from the records of Better Auth, or undefined when the user is unknown. */
|
|
53
|
+
ofUser(id: string): Promise<SignedInUser | undefined>;
|
|
54
|
+
/** This method gives the groups of a user: the `member` records of the organization plugin. */
|
|
55
|
+
groupsOf(id: string): Promise<readonly UserGroup[]>;
|
|
56
|
+
/** This method forgets the memo of the groups of a user, for example after a change of a membership. */
|
|
57
|
+
forgetGroups(id: string): void;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=principals.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"principals.d.ts","sourceRoot":"","sources":["../src/principals.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAY,KAAK,EAAE,EAAE,MAAM,SAAS,CAAC;AAE5C,gGAAgG;AAChG,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,qEAAqE;AACrE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC;CACvC;AAED,sEAAsE;AACtE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,EAAE;QAAE,UAAU,CAAC,OAAO,EAAE;YAAE,OAAO,EAAE,OAAO,CAAA;SAAE,GAAG,OAAO,CAAC;YAAE,IAAI,CAAC,EAAE,OAAO,CAAA;SAAE,GAAG,IAAI,GAAG,SAAS,CAAC,CAAA;KAAE,CAAC;CAC7G;AAED,0CAA0C;AAC1C,MAAM,WAAW,wBAAwB;IACvC,8BAA8B;IAC9B,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,wEAAwE;IACxE,QAAQ,CAAC,EAAE,EAAE,EAAE,CAAC;IAChB,iFAAiF;IACjF,QAAQ,CAAC,oBAAoB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACpD,4EAA4E;IAC5E,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C,4EAA4E;IAC5E,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C,gEAAgE;IAChE,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,SAAS,CAAC;IAC1C,0EAA0E;IAC1E,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;CACzD;AASD,iDAAiD;AACjD,qBAAa,iBAAiB;;IAK5B,YAAY,OAAO,EAAE,wBAAwB,EAE5C;IAeD,8GAA8G;IAC9G,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAW7D;IAsBD,6GAA6G;IACvG,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAS1D;IAED,+FAA+F;IAC/F,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,SAAS,EAAE,CAAC,CAiBlD;IAED,wGAAwG;IACxG,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAE7B;CACF"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The person of a request: the session cookie gives the user, and the `member` records of the
|
|
3
|
+
* organization plugin give the groups. A short memo keeps the boot of a page, which sends many
|
|
4
|
+
* requests at the same time, to one session lookup.
|
|
5
|
+
*/
|
|
6
|
+
import { ObjectId } from "mongodb";
|
|
7
|
+
const MAX_MEMO = 5_000;
|
|
8
|
+
/** The ids to look for: the text, and the `ObjectId` when the text is one. */
|
|
9
|
+
function idsOf(id) {
|
|
10
|
+
return ObjectId.isValid(id) ? [id, new ObjectId(id)] : [id];
|
|
11
|
+
}
|
|
12
|
+
/** The users of the sessions of the requests. */
|
|
13
|
+
export class SessionPrincipals {
|
|
14
|
+
#options;
|
|
15
|
+
#sessions = new Map();
|
|
16
|
+
#groups = new Map();
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.#options = options;
|
|
19
|
+
}
|
|
20
|
+
#now() {
|
|
21
|
+
return (this.#options.now ?? Date.now)();
|
|
22
|
+
}
|
|
23
|
+
#warn(message) {
|
|
24
|
+
(this.#options.warn ?? ((text) => console.warn(text)))(message);
|
|
25
|
+
}
|
|
26
|
+
#email(user) {
|
|
27
|
+
const verified = user["emailVerified"] === true || this.#options.trustUnverifiedEmail === true;
|
|
28
|
+
return typeof user["email"] === "string" && user["email"] && verified ? user["email"] : undefined;
|
|
29
|
+
}
|
|
30
|
+
/** This method gives the user of the session cookie of the headers, or undefined when there is no session. */
|
|
31
|
+
ofHeaders(headers) {
|
|
32
|
+
const cookie = headers.get("cookie") ?? "";
|
|
33
|
+
if (!cookie)
|
|
34
|
+
return Promise.resolve(undefined);
|
|
35
|
+
const now = this.#now();
|
|
36
|
+
const ttl = this.#options.sessionMemoMs ?? 5_000;
|
|
37
|
+
const hit = this.#sessions.get(cookie);
|
|
38
|
+
if (hit && now - hit.at < ttl)
|
|
39
|
+
return hit.user;
|
|
40
|
+
const user = this.#resolve(headers);
|
|
41
|
+
this.#sessions.set(cookie, { at: now, user });
|
|
42
|
+
if (this.#sessions.size > MAX_MEMO)
|
|
43
|
+
prune(this.#sessions, now, ttl);
|
|
44
|
+
return user;
|
|
45
|
+
}
|
|
46
|
+
async #resolve(headers) {
|
|
47
|
+
let session;
|
|
48
|
+
try {
|
|
49
|
+
session = await this.#options.auth.api.getSession({ headers });
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const user = session?.user;
|
|
55
|
+
if (typeof user !== "object" || user === null)
|
|
56
|
+
return undefined;
|
|
57
|
+
const record = user;
|
|
58
|
+
if (record["id"] === undefined || record["id"] === null)
|
|
59
|
+
return undefined;
|
|
60
|
+
return this.#userOf(String(record["id"]), record);
|
|
61
|
+
}
|
|
62
|
+
async #userOf(id, record) {
|
|
63
|
+
const email = this.#email(record);
|
|
64
|
+
const name = typeof record["name"] === "string" && record["name"] ? record["name"] : undefined;
|
|
65
|
+
return { id, ...(email ? { email } : {}), ...(name ? { name } : {}), groups: await this.groupsOf(id) };
|
|
66
|
+
}
|
|
67
|
+
/** This method gives a user by id from the records of Better Auth, or undefined when the user is unknown. */
|
|
68
|
+
async ofUser(id) {
|
|
69
|
+
let record;
|
|
70
|
+
try {
|
|
71
|
+
record = (await this.#options.db.collection("user").findOne({ _id: { $in: idsOf(id) } }));
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
this.#warn(`[better-auth] the lookup of a user failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
return record ? this.#userOf(id, record) : undefined;
|
|
78
|
+
}
|
|
79
|
+
/** This method gives the groups of a user: the `member` records of the organization plugin. */
|
|
80
|
+
groupsOf(id) {
|
|
81
|
+
const now = this.#now();
|
|
82
|
+
const ttl = this.#options.groupsMemoMs ?? 10_000;
|
|
83
|
+
const hit = this.#groups.get(id);
|
|
84
|
+
if (hit && now - hit.at < ttl)
|
|
85
|
+
return hit.groups;
|
|
86
|
+
const groups = (async () => {
|
|
87
|
+
try {
|
|
88
|
+
const rows = (await this.#options.db.collection("member").find({ userId: { $in: idsOf(id) } }).toArray());
|
|
89
|
+
return rows.map((row) => ({ id: String(row["organizationId"]), role: String(row["role"] ?? "member") }));
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
this.#warn(`[better-auth] the lookup of the groups failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
})();
|
|
96
|
+
this.#groups.set(id, { at: now, groups });
|
|
97
|
+
if (this.#groups.size > MAX_MEMO)
|
|
98
|
+
prune(this.#groups, now, ttl);
|
|
99
|
+
return groups;
|
|
100
|
+
}
|
|
101
|
+
/** This method forgets the memo of the groups of a user, for example after a change of a membership. */
|
|
102
|
+
forgetGroups(id) {
|
|
103
|
+
this.#groups.delete(id);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** This function removes the expired entries of a memo. */
|
|
107
|
+
function prune(map, now, ttl) {
|
|
108
|
+
for (const [key, entry] of map)
|
|
109
|
+
if (now - entry.at >= ttl)
|
|
110
|
+
map.delete(key);
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=principals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"principals.js","sourceRoot":"","sources":["../src/principals.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,13 +1,66 @@
|
|
|
1
1
|
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/package",
|
|
2
3
|
"name": "@procedures/better-auth",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
4
|
+
"version": "0.12.1",
|
|
5
|
+
"description": "Better Auth for a procedures server: the sign-in with GitHub, Google and passkeys over MongoDB, and a request hook that gives the principal of a session or of an agent token.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"procedures",
|
|
8
|
+
"better-auth",
|
|
9
|
+
"authentication",
|
|
10
|
+
"oauth",
|
|
11
|
+
"passkey",
|
|
12
|
+
"session",
|
|
13
|
+
"principal"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://mark1russell7.github.io/client/",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/mark1russell7/client/issues"
|
|
18
|
+
},
|
|
5
19
|
"repository": {
|
|
6
20
|
"type": "git",
|
|
7
|
-
"url": "git+https://github.com/mark1russell7/client.git"
|
|
21
|
+
"url": "git+https://github.com/mark1russell7/client.git",
|
|
22
|
+
"directory": "packages/better-auth"
|
|
8
23
|
},
|
|
9
24
|
"license": "MIT",
|
|
10
|
-
"
|
|
11
|
-
|
|
25
|
+
"author": "Mark Russell (https://github.com/mark1russell7)",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"import": "./dist/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"src"
|
|
38
|
+
],
|
|
39
|
+
"sideEffects": false,
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@mark1russell7/cue": "^1.0.0",
|
|
42
|
+
"mongodb": "^7.7.0",
|
|
43
|
+
"typescript": "^7.0.2",
|
|
44
|
+
"vitest": "^5.0.3"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=22"
|
|
48
|
+
},
|
|
49
|
+
"client": null,
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@better-auth/passkey": "^1.7.10",
|
|
52
|
+
"@procedures/client": "0.12.1",
|
|
53
|
+
"better-auth": "^1.7.10",
|
|
54
|
+
"zod": "^4.6.5"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"mongodb": "^6.0.0 || ^7.0.0"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsc -b",
|
|
61
|
+
"typecheck": "tsc --noEmit",
|
|
62
|
+
"clean": "rm -rf dist .tsbuildinfo",
|
|
63
|
+
"test": "vitest run --passWithNoTests",
|
|
64
|
+
"test:watch": "vitest"
|
|
12
65
|
}
|
|
13
|
-
}
|
|
66
|
+
}
|