@usegraft/auth 0.0.0-canary-20260831153011

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anderson Joseph
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 ADDED
@@ -0,0 +1,47 @@
1
+ # @usegraft/auth
2
+
3
+ > Graft verifies identity; it does not mint it. Turn a request into a scoped actor by verifying bearer JWTs against trusted issuers.
4
+
5
+ Part of [Graft](https://github.com/AndersonDesign1/graft), a CMS built so an AI agent is the primary operator.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm i @usegraft/auth
11
+ ```
12
+
13
+ ## Resolve an actor
14
+
15
+ ```ts
16
+ import { createActorResolver, betterAuthIssuer } from "@usegraft/auth";
17
+
18
+ const resolveActor = createActorResolver({
19
+ issuers: [betterAuthIssuer("https://example.com")],
20
+ });
21
+ ```
22
+
23
+ Hand that to `graft serve`, the MCP handler, or the functions handler. It verifies the bearer JWT against the issuer's JWKS via OIDC discovery and returns a `FunctionActor` carrying a stable id and the scopes from the standard `scope` claim.
24
+
25
+ Because it verifies rather than issues, an external IdP drops in unchanged. A Better Auth instance your app hosts and an enterprise IdP take the same path.
26
+
27
+ ## Require scopes
28
+
29
+ ```ts
30
+ import { requireScopes } from "@usegraft/auth";
31
+
32
+ export const listSubmissions = defineFunction({
33
+ name: "listSubmissions",
34
+ authorize: requireScopes("submissions:read"),
35
+ // …
36
+ });
37
+ ```
38
+
39
+ Being signed in earns nothing. An account gets scopes because something granted them, which is the whole point: open sign-up plus implicit scopes means one free registration reads everything.
40
+
41
+ ## Environment
42
+
43
+ `GRAFT_TRUSTED_ISSUERS` for OIDC issuers, `GRAFT_DEV_TOKEN` and `GRAFT_DEV_SCOPES` for a static local identity. Anonymous MCP is refused unless `GRAFT_MCP_ALLOW_ANONYMOUS=1`, which nothing sets for you.
44
+
45
+ ---
46
+
47
+ MIT. [Repository](https://github.com/AndersonDesign1/graft) · [Changelog](https://github.com/AndersonDesign1/graft/blob/main/packages/auth/CHANGELOG.md) · [Security policy](https://github.com/AndersonDesign1/graft/blob/main/SECURITY.md)
@@ -0,0 +1,72 @@
1
+ import { FunctionActor } from '@usegraft/core';
2
+ import { JSONWebKeySet } from 'jose';
3
+
4
+ interface TrustedIssuer {
5
+ /** Expected `iss` claim, exactly as the issuer mints it (e.g. "https://auth.example.com"). */
6
+ issuer: string;
7
+ /**
8
+ * Where the issuer's public keys live: a JWKS URL, or an inline JSON Web Key
9
+ * Set (pinned keys — no network). Omitted → OIDC discovery at
10
+ * `<issuer>/.well-known/openid-configuration`.
11
+ */
12
+ jwks?: string | JSONWebKeySet;
13
+ /** Expected `aud` claim. Unchecked when omitted. */
14
+ audience?: string | string[];
15
+ /** What kind of actor this issuer authenticates. Defaults to "agent". */
16
+ actorKind?: "agent" | "human";
17
+ /**
18
+ * Claim carrying scopes. Defaults to trying `scope` (space-separated string,
19
+ * the OAuth2 convention), then `scopes`, then `permissions` (arrays).
20
+ */
21
+ scopesClaim?: string;
22
+ }
23
+ /**
24
+ * Verifies bearer JWTs against a list of trusted issuers. The token's own
25
+ * `iss` claim picks the issuer config; an unlisted issuer is TOKEN_INVALID
26
+ * (details name the trusted ones — self-teaching, an agent learns where to go).
27
+ */
28
+ declare function createOidcVerifier(issuers: readonly TrustedIssuer[]): (token: string) => Promise<FunctionActor>;
29
+ /**
30
+ * TrustedIssuer preset for a Better Auth instance (its `jwt`/OAuth-provider
31
+ * plugins publish keys at `<basePath>/jwks` and mint `iss`/`aud` = its URL).
32
+ */
33
+ declare function betterAuthIssuer(options: {
34
+ /** The Better Auth base URL (its `iss`), e.g. "http://localhost:3000". */
35
+ url: string;
36
+ /** Better Auth mount path. Defaults to "/api/auth". */
37
+ basePath?: string;
38
+ /** Expected audience. Defaults to `url`; pass null to skip the check. */
39
+ audience?: string | string[] | null;
40
+ actorKind?: "agent" | "human";
41
+ }): TrustedIssuer;
42
+
43
+ interface ActorResolverOptions {
44
+ /** OIDC issuers whose JWTs this deployment accepts. */
45
+ issuers?: readonly TrustedIssuer[];
46
+ /**
47
+ * Static bearer tokens → actors, for development and self-host bootstrap.
48
+ * Keep these out of production: no expiry, no scoping ceremony, one shared
49
+ * secret. Example: `{ [process.env.DEV_TOKEN!]: { kind: "human", id: "owner" } }`.
50
+ */
51
+ devTokens?: Record<string, FunctionActor>;
52
+ }
53
+ type ActorResolver = (request: Request) => Promise<FunctionActor>;
54
+ declare function createActorResolver(options?: ActorResolverOptions): ActorResolver;
55
+
56
+ /**
57
+ * requireScopes — an access rule for defineFunction that honors the scopes a
58
+ * trusted issuer put on the actor's token.
59
+ *
60
+ * access: requireScopes("submissions:read")
61
+ *
62
+ * Strict: anonymous actors are denied, and a missing scopes claim counts as
63
+ * no scopes — trusted-but-unscoped tokens don't pass scope gates. Mint scoped
64
+ * tokens (or give dev tokens explicit `scopes`) for gated functions. With no
65
+ * arguments it degrades to "any non-anonymous actor".
66
+ */
67
+
68
+ declare function requireScopes(...required: readonly string[]): (ctx: {
69
+ actor: FunctionActor;
70
+ }) => boolean;
71
+
72
+ export { type ActorResolver, type ActorResolverOptions, type TrustedIssuer, betterAuthIssuer, createActorResolver, createOidcVerifier, requireScopes };
package/dist/index.js ADDED
@@ -0,0 +1,148 @@
1
+ // src/oidc.ts
2
+ import { GraftError } from "@usegraft/contracts";
3
+ import {
4
+ createLocalJWKSet,
5
+ createRemoteJWKSet,
6
+ decodeJwt,
7
+ jwtVerify
8
+ } from "jose";
9
+ function tokenInvalid(reason, details) {
10
+ return new GraftError({
11
+ code: "TOKEN_INVALID",
12
+ message: `The bearer token could not be verified: ${reason}.`,
13
+ fix: "Mint a fresh token from a trusted issuer and retry. Do not drop the Authorization header to fall back to anonymous \u2014 fix the token instead.",
14
+ details: { reason, ...details }
15
+ });
16
+ }
17
+ function readScopes(payload, scopesClaim) {
18
+ const claims = scopesClaim ? [scopesClaim] : ["scope", "scopes", "permissions"];
19
+ for (const claim of claims) {
20
+ const value = payload[claim];
21
+ if (typeof value === "string") return value.split(" ").filter(Boolean);
22
+ if (Array.isArray(value) && value.every((v) => typeof v === "string")) return value;
23
+ }
24
+ return void 0;
25
+ }
26
+ var IssuerVerifier = class {
27
+ constructor(config) {
28
+ this.config = config;
29
+ }
30
+ config;
31
+ getKey;
32
+ async keySource() {
33
+ if (this.getKey) return this.getKey;
34
+ const { issuer, jwks } = this.config;
35
+ if (jwks && typeof jwks === "object") {
36
+ this.getKey = createLocalJWKSet(jwks);
37
+ } else if (typeof jwks === "string") {
38
+ this.getKey = createRemoteJWKSet(new URL(jwks));
39
+ } else {
40
+ const discoveryUrl = `${issuer.replace(/\/$/, "")}/.well-known/openid-configuration`;
41
+ const res = await fetch(discoveryUrl);
42
+ if (!res.ok) {
43
+ throw tokenInvalid(
44
+ `OIDC discovery for issuer "${issuer}" failed (${res.status} from ${discoveryUrl})`
45
+ );
46
+ }
47
+ const metadata = await res.json();
48
+ if (!metadata.jwks_uri) {
49
+ throw tokenInvalid(`issuer "${issuer}" publishes no jwks_uri in its OIDC metadata`);
50
+ }
51
+ this.getKey = createRemoteJWKSet(new URL(metadata.jwks_uri));
52
+ }
53
+ return this.getKey;
54
+ }
55
+ async verify(token) {
56
+ const getKey = await this.keySource();
57
+ let payload;
58
+ try {
59
+ ({ payload } = await jwtVerify(token, getKey, {
60
+ issuer: this.config.issuer,
61
+ audience: this.config.audience,
62
+ // jose treats `sub` as optional, so a signature-valid token without one
63
+ // used to authenticate as an actor with `id: undefined` — which then
64
+ // collapsed into an IP-keyed rate bucket, wrote null actor ids to the
65
+ // audit log, and filed approvals nobody could be held to.
66
+ requiredClaims: ["sub"]
67
+ }));
68
+ } catch (err) {
69
+ const code = err.code ?? "verification failed";
70
+ throw tokenInvalid(`${code} (issuer "${this.config.issuer}")`);
71
+ }
72
+ if (payload.sub === void 0) {
73
+ throw tokenInvalid(`token carries no "sub" claim (issuer "${this.config.issuer}")`);
74
+ }
75
+ return {
76
+ kind: this.config.actorKind ?? "agent",
77
+ id: payload.sub,
78
+ scopes: readScopes(payload, this.config.scopesClaim)
79
+ };
80
+ }
81
+ };
82
+ function createOidcVerifier(issuers) {
83
+ const verifiers = new Map(issuers.map((i) => [i.issuer, new IssuerVerifier(i)]));
84
+ return async (token) => {
85
+ let iss;
86
+ try {
87
+ iss = decodeJwt(token).iss;
88
+ } catch {
89
+ throw tokenInvalid("the token is not a decodable JWT");
90
+ }
91
+ if (!iss) throw tokenInvalid("the token carries no `iss` claim");
92
+ const verifier = verifiers.get(iss);
93
+ if (!verifier) {
94
+ throw tokenInvalid(`issuer "${iss}" is not trusted by this deployment`, {
95
+ trustedIssuers: [...verifiers.keys()]
96
+ });
97
+ }
98
+ return verifier.verify(token);
99
+ };
100
+ }
101
+ function betterAuthIssuer(options) {
102
+ const basePath = options.basePath ?? "/api/auth";
103
+ return {
104
+ issuer: options.url,
105
+ jwks: `${options.url.replace(/\/$/, "")}${basePath}/jwks`,
106
+ audience: options.audience === null ? void 0 : options.audience ?? options.url,
107
+ actorKind: options.actorKind
108
+ };
109
+ }
110
+
111
+ // src/resolver.ts
112
+ import { GraftError as GraftError2 } from "@usegraft/contracts";
113
+ var ANONYMOUS = { kind: "anonymous" };
114
+ function createActorResolver(options = {}) {
115
+ const devTokens = new Map(Object.entries(options.devTokens ?? {}));
116
+ devTokens.delete("");
117
+ const verifyJwt = options.issuers?.length ? createOidcVerifier(options.issuers) : void 0;
118
+ return async (request) => {
119
+ const header = request.headers.get("authorization");
120
+ if (!header) return ANONYMOUS;
121
+ const match = /^Bearer\s+(.+)$/i.exec(header);
122
+ if (!match) return ANONYMOUS;
123
+ const token = match[1].trim();
124
+ const dev = devTokens.get(token);
125
+ if (dev) return dev;
126
+ if (verifyJwt) return verifyJwt(token);
127
+ throw new GraftError2({
128
+ code: "TOKEN_INVALID",
129
+ message: "A bearer token was sent, but this deployment has no trusted issuers configured.",
130
+ fix: "Configure `issuers` (or a dev token) in createActorResolver, or call without an Authorization header if anonymous access is intended."
131
+ });
132
+ };
133
+ }
134
+
135
+ // src/scopes.ts
136
+ function requireScopes(...required) {
137
+ return ({ actor }) => {
138
+ if (actor.kind === "anonymous") return false;
139
+ const held = actor.scopes ?? [];
140
+ return required.every((scope) => held.includes(scope));
141
+ };
142
+ }
143
+ export {
144
+ betterAuthIssuer,
145
+ createActorResolver,
146
+ createOidcVerifier,
147
+ requireScopes
148
+ };
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@usegraft/auth",
3
+ "version": "0.0.0-canary-20260831153011",
4
+ "description": "Turn a request into a scoped actor by verifying bearer JWTs against trusted OIDC issuers. Graft verifies identity, it does not mint it.",
5
+ "keywords": [
6
+ "agent",
7
+ "ai",
8
+ "auth",
9
+ "better-auth",
10
+ "cms",
11
+ "graft",
12
+ "headless-cms",
13
+ "jwt",
14
+ "mcp",
15
+ "oidc",
16
+ "scopes",
17
+ "typescript"
18
+ ],
19
+ "homepage": "https://github.com/AndersonDesign1/graft#readme",
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/AndersonDesign1/graft.git",
24
+ "directory": "packages/auth"
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "type": "module",
30
+ "main": "./dist/index.js",
31
+ "module": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js"
37
+ }
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "dependencies": {
43
+ "@usegraft/contracts": "0.0.0-canary-20260831153011",
44
+ "@usegraft/core": "0.0.0-canary-20260831153011",
45
+ "jose": "^6.0.0"
46
+ },
47
+ "engines": {
48
+ "node": ">=22.16"
49
+ },
50
+ "scripts": {
51
+ "build": "tsup src/index.ts --format esm --dts --clean",
52
+ "dev": "tsup src/index.ts --format esm --watch",
53
+ "typecheck": "tsc --noEmit",
54
+ "test": "vitest run"
55
+ }
56
+ }