@usegraft/auth 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.js +140 -0
- package/package.json +41 -0
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/dist/index.d.ts
ADDED
|
@@ -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,140 @@
|
|
|
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
|
+
}));
|
|
63
|
+
} catch (err) {
|
|
64
|
+
const code = err.code ?? "verification failed";
|
|
65
|
+
throw tokenInvalid(`${code} (issuer "${this.config.issuer}")`);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
kind: this.config.actorKind ?? "agent",
|
|
69
|
+
id: payload.sub,
|
|
70
|
+
scopes: readScopes(payload, this.config.scopesClaim)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
function createOidcVerifier(issuers) {
|
|
75
|
+
const verifiers = new Map(issuers.map((i) => [i.issuer, new IssuerVerifier(i)]));
|
|
76
|
+
return async (token) => {
|
|
77
|
+
let iss;
|
|
78
|
+
try {
|
|
79
|
+
iss = decodeJwt(token).iss;
|
|
80
|
+
} catch {
|
|
81
|
+
throw tokenInvalid("the token is not a decodable JWT");
|
|
82
|
+
}
|
|
83
|
+
if (!iss) throw tokenInvalid("the token carries no `iss` claim");
|
|
84
|
+
const verifier = verifiers.get(iss);
|
|
85
|
+
if (!verifier) {
|
|
86
|
+
throw tokenInvalid(`issuer "${iss}" is not trusted by this deployment`, {
|
|
87
|
+
trustedIssuers: [...verifiers.keys()]
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return verifier.verify(token);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function betterAuthIssuer(options) {
|
|
94
|
+
const basePath = options.basePath ?? "/api/auth";
|
|
95
|
+
return {
|
|
96
|
+
issuer: options.url,
|
|
97
|
+
jwks: `${options.url.replace(/\/$/, "")}${basePath}/jwks`,
|
|
98
|
+
audience: options.audience === null ? void 0 : options.audience ?? options.url,
|
|
99
|
+
actorKind: options.actorKind
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/resolver.ts
|
|
104
|
+
import { GraftError as GraftError2 } from "@usegraft/contracts";
|
|
105
|
+
var ANONYMOUS = { kind: "anonymous" };
|
|
106
|
+
function createActorResolver(options = {}) {
|
|
107
|
+
const devTokens = new Map(Object.entries(options.devTokens ?? {}));
|
|
108
|
+
devTokens.delete("");
|
|
109
|
+
const verifyJwt = options.issuers?.length ? createOidcVerifier(options.issuers) : void 0;
|
|
110
|
+
return async (request) => {
|
|
111
|
+
const header = request.headers.get("authorization");
|
|
112
|
+
if (!header) return ANONYMOUS;
|
|
113
|
+
const match = /^Bearer\s+(.+)$/i.exec(header);
|
|
114
|
+
if (!match) return ANONYMOUS;
|
|
115
|
+
const token = match[1].trim();
|
|
116
|
+
const dev = devTokens.get(token);
|
|
117
|
+
if (dev) return dev;
|
|
118
|
+
if (verifyJwt) return verifyJwt(token);
|
|
119
|
+
throw new GraftError2({
|
|
120
|
+
code: "TOKEN_INVALID",
|
|
121
|
+
message: "A bearer token was sent, but this deployment has no trusted issuers configured.",
|
|
122
|
+
fix: "Configure `issuers` (or a dev token) in createActorResolver, or call without an Authorization header if anonymous access is intended."
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/scopes.ts
|
|
128
|
+
function requireScopes(...required) {
|
|
129
|
+
return ({ actor }) => {
|
|
130
|
+
if (actor.kind === "anonymous") return false;
|
|
131
|
+
const held = actor.scopes ?? [];
|
|
132
|
+
return required.every((scope) => held.includes(scope));
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
export {
|
|
136
|
+
betterAuthIssuer,
|
|
137
|
+
createActorResolver,
|
|
138
|
+
createOidcVerifier,
|
|
139
|
+
requireScopes
|
|
140
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usegraft/auth",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/AndersonDesign1/graft.git",
|
|
8
|
+
"directory": "packages/auth"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"module": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"jose": "^6.0.0",
|
|
28
|
+
"@usegraft/contracts": "0.1.0",
|
|
29
|
+
"@usegraft/core": "0.1.0"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=22.16"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
36
|
+
"dev": "tsup src/index.ts --format esm --watch",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"lint": "oxlint ."
|
|
40
|
+
}
|
|
41
|
+
}
|