@orangecheck/auth-core 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/README.md +91 -0
- package/dist/index.d.mts +42 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +106 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +94 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 OrangeCheck
|
|
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,91 @@
|
|
|
1
|
+
# `@orangecheck/auth-core`
|
|
2
|
+
|
|
3
|
+
**Cross-subdomain session primitives for the OrangeCheck ecosystem.**
|
|
4
|
+
|
|
5
|
+
A ~3 KB package that handles:
|
|
6
|
+
|
|
7
|
+
- Ed25519 JWT sign / verify with `jose`
|
|
8
|
+
- Cross-subdomain cookie serialize / clear / parse
|
|
9
|
+
- Typed `SessionPayload` (`sub`, `addr`, `jti`, …)
|
|
10
|
+
|
|
11
|
+
No React, no DB, no framework. Usable from Node, Edge, or browser.
|
|
12
|
+
|
|
13
|
+
## The model
|
|
14
|
+
|
|
15
|
+
There is **one auth host** (by convention `https://ochk.io`) that holds the
|
|
16
|
+
private JWK and issues tokens. Every **consumer subdomain** (`attest.ochk.io`,
|
|
17
|
+
`lock.ochk.io`, `docs.ochk.io`, …) only holds the public JWK and verifies
|
|
18
|
+
locally — no network round-trip.
|
|
19
|
+
|
|
20
|
+
The session cookie is set with `Domain=.ochk.io`, so once a user signs in on
|
|
21
|
+
any subdomain, every other subdomain sees them as signed-in immediately.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
yarn add @orangecheck/auth-core
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Generate a key pair
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
node scripts/gen-auth-keys.mjs
|
|
33
|
+
# → prints OC_AUTH_KID, OC_AUTH_PRIVATE_JWK, OC_AUTH_PUBLIC_JWK
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`OC_AUTH_PRIVATE_JWK` lives only on the auth host. The public JWK + KID go on
|
|
37
|
+
every subdomain.
|
|
38
|
+
|
|
39
|
+
## On the auth host
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { signSession, serializeSessionCookie } from '@orangecheck/auth-core';
|
|
43
|
+
|
|
44
|
+
const token = await signSession(
|
|
45
|
+
{ sub: accountId, addr: btcAddress, jti: crypto.randomUUID() },
|
|
46
|
+
{
|
|
47
|
+
kid: process.env.OC_AUTH_KID!,
|
|
48
|
+
privateJwk: process.env.OC_AUTH_PRIVATE_JWK!,
|
|
49
|
+
publicJwk: process.env.OC_AUTH_PUBLIC_JWK!,
|
|
50
|
+
issuer: 'https://ochk.io',
|
|
51
|
+
},
|
|
52
|
+
60 * 60 * 24 * 30
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
res.setHeader(
|
|
56
|
+
'Set-Cookie',
|
|
57
|
+
serializeSessionCookie(token, { domain: '.ochk.io', maxAge: 60 * 60 * 24 * 30 })
|
|
58
|
+
);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## On any consumer subdomain
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { readSessionCookie, verifySessionToken } from '@orangecheck/auth-core';
|
|
65
|
+
|
|
66
|
+
const token = readSessionCookie(req.headers.cookie);
|
|
67
|
+
if (!token) return res.status(401).end();
|
|
68
|
+
|
|
69
|
+
const payload = await verifySessionToken(token, {
|
|
70
|
+
publicJwk: process.env.OC_AUTH_PUBLIC_JWK!,
|
|
71
|
+
issuer: 'https://ochk.io',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!payload) return res.status(401).end();
|
|
75
|
+
|
|
76
|
+
// payload.addr is the verified Bitcoin address.
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Guarantees
|
|
80
|
+
|
|
81
|
+
- **No shared secret.** Consumer apps cannot forge sessions — only the auth
|
|
82
|
+
host can, because only it holds the private key.
|
|
83
|
+
- **Offline-verifiable.** Given the public JWK, any process can verify a
|
|
84
|
+
token — no database, no auth server round-trip.
|
|
85
|
+
- **Stateless.** Revocation is the caller's problem. This package does
|
|
86
|
+
crypto only; if you need immediate-revoke semantics, layer a DB check
|
|
87
|
+
on the auth host.
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { CryptoKey, KeyObject, JWTPayload } from 'jose';
|
|
2
|
+
|
|
3
|
+
declare const JWT_ALG: "EdDSA";
|
|
4
|
+
declare const SESSION_COOKIE: "oc_session";
|
|
5
|
+
declare const DEFAULT_ISSUER: "https://ochk.io";
|
|
6
|
+
type AuthKey = CryptoKey | KeyObject;
|
|
7
|
+
interface SessionPayload extends JWTPayload {
|
|
8
|
+
sub: string;
|
|
9
|
+
addr: string;
|
|
10
|
+
jti: string;
|
|
11
|
+
}
|
|
12
|
+
interface VerifyConfig {
|
|
13
|
+
publicJwk: string;
|
|
14
|
+
issuer?: string;
|
|
15
|
+
}
|
|
16
|
+
interface SignConfig extends VerifyConfig {
|
|
17
|
+
privateJwk: string;
|
|
18
|
+
kid: string;
|
|
19
|
+
}
|
|
20
|
+
declare function loadPublicKey(publicJwk: string): Promise<AuthKey>;
|
|
21
|
+
declare function loadPrivateKey(privateJwk: string): Promise<AuthKey>;
|
|
22
|
+
declare function parsePublicJwk(publicJwk: string): Record<string, unknown>;
|
|
23
|
+
declare function signSession(claims: {
|
|
24
|
+
sub: string;
|
|
25
|
+
addr: string;
|
|
26
|
+
jti: string;
|
|
27
|
+
}, cfg: SignConfig, ttlSeconds: number): Promise<string>;
|
|
28
|
+
declare function verifySessionToken(token: string, cfg: VerifyConfig): Promise<SessionPayload | null>;
|
|
29
|
+
interface CookieOptions {
|
|
30
|
+
domain?: string;
|
|
31
|
+
maxAge?: number;
|
|
32
|
+
expires?: Date;
|
|
33
|
+
path?: string;
|
|
34
|
+
secure?: boolean;
|
|
35
|
+
httpOnly?: boolean;
|
|
36
|
+
sameSite?: 'Lax' | 'Strict' | 'None';
|
|
37
|
+
}
|
|
38
|
+
declare function serializeSessionCookie(token: string, opts?: CookieOptions): string;
|
|
39
|
+
declare function clearSessionCookieHeader(opts?: Pick<CookieOptions, 'domain' | 'path' | 'secure'>): string;
|
|
40
|
+
declare function readSessionCookie(cookieHeader: string | null | undefined): string | null;
|
|
41
|
+
|
|
42
|
+
export { type AuthKey, type CookieOptions, DEFAULT_ISSUER, JWT_ALG, SESSION_COOKIE, type SessionPayload, type SignConfig, type VerifyConfig, clearSessionCookieHeader, loadPrivateKey, loadPublicKey, parsePublicJwk, readSessionCookie, serializeSessionCookie, signSession, verifySessionToken };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { CryptoKey, KeyObject, JWTPayload } from 'jose';
|
|
2
|
+
|
|
3
|
+
declare const JWT_ALG: "EdDSA";
|
|
4
|
+
declare const SESSION_COOKIE: "oc_session";
|
|
5
|
+
declare const DEFAULT_ISSUER: "https://ochk.io";
|
|
6
|
+
type AuthKey = CryptoKey | KeyObject;
|
|
7
|
+
interface SessionPayload extends JWTPayload {
|
|
8
|
+
sub: string;
|
|
9
|
+
addr: string;
|
|
10
|
+
jti: string;
|
|
11
|
+
}
|
|
12
|
+
interface VerifyConfig {
|
|
13
|
+
publicJwk: string;
|
|
14
|
+
issuer?: string;
|
|
15
|
+
}
|
|
16
|
+
interface SignConfig extends VerifyConfig {
|
|
17
|
+
privateJwk: string;
|
|
18
|
+
kid: string;
|
|
19
|
+
}
|
|
20
|
+
declare function loadPublicKey(publicJwk: string): Promise<AuthKey>;
|
|
21
|
+
declare function loadPrivateKey(privateJwk: string): Promise<AuthKey>;
|
|
22
|
+
declare function parsePublicJwk(publicJwk: string): Record<string, unknown>;
|
|
23
|
+
declare function signSession(claims: {
|
|
24
|
+
sub: string;
|
|
25
|
+
addr: string;
|
|
26
|
+
jti: string;
|
|
27
|
+
}, cfg: SignConfig, ttlSeconds: number): Promise<string>;
|
|
28
|
+
declare function verifySessionToken(token: string, cfg: VerifyConfig): Promise<SessionPayload | null>;
|
|
29
|
+
interface CookieOptions {
|
|
30
|
+
domain?: string;
|
|
31
|
+
maxAge?: number;
|
|
32
|
+
expires?: Date;
|
|
33
|
+
path?: string;
|
|
34
|
+
secure?: boolean;
|
|
35
|
+
httpOnly?: boolean;
|
|
36
|
+
sameSite?: 'Lax' | 'Strict' | 'None';
|
|
37
|
+
}
|
|
38
|
+
declare function serializeSessionCookie(token: string, opts?: CookieOptions): string;
|
|
39
|
+
declare function clearSessionCookieHeader(opts?: Pick<CookieOptions, 'domain' | 'path' | 'secure'>): string;
|
|
40
|
+
declare function readSessionCookie(cookieHeader: string | null | undefined): string | null;
|
|
41
|
+
|
|
42
|
+
export { type AuthKey, type CookieOptions, DEFAULT_ISSUER, JWT_ALG, SESSION_COOKIE, type SessionPayload, type SignConfig, type VerifyConfig, clearSessionCookieHeader, loadPrivateKey, loadPublicKey, parsePublicJwk, readSessionCookie, serializeSessionCookie, signSession, verifySessionToken };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var jose = require('jose');
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
var JWT_ALG = "EdDSA";
|
|
7
|
+
var SESSION_COOKIE = "oc_session";
|
|
8
|
+
var DEFAULT_ISSUER = "https://ochk.io";
|
|
9
|
+
function decodeJwk(value) {
|
|
10
|
+
const json = typeof Buffer !== "undefined" ? Buffer.from(value, "base64url").toString("utf8") : new TextDecoder().decode(base64UrlDecode(value));
|
|
11
|
+
const parsed = JSON.parse(json);
|
|
12
|
+
if (!parsed || typeof parsed !== "object") {
|
|
13
|
+
throw new Error("[@orangecheck/auth-core] JWK env did not decode to an object");
|
|
14
|
+
}
|
|
15
|
+
return parsed;
|
|
16
|
+
}
|
|
17
|
+
function base64UrlDecode(input) {
|
|
18
|
+
const pad = input.length % 4 === 0 ? "" : "=".repeat(4 - input.length % 4);
|
|
19
|
+
const b64 = (input + pad).replace(/-/g, "+").replace(/_/g, "/");
|
|
20
|
+
const bin = typeof atob !== "undefined" ? atob(b64) : "";
|
|
21
|
+
const bytes = new Uint8Array(bin.length);
|
|
22
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
23
|
+
return bytes;
|
|
24
|
+
}
|
|
25
|
+
var publicKeyPromise = null;
|
|
26
|
+
var publicKeyCache = null;
|
|
27
|
+
function loadPublicKey(publicJwk) {
|
|
28
|
+
if (!publicKeyPromise || publicKeyCache !== publicJwk) {
|
|
29
|
+
publicKeyCache = publicJwk;
|
|
30
|
+
publicKeyPromise = jose.importJWK(decodeJwk(publicJwk), JWT_ALG);
|
|
31
|
+
}
|
|
32
|
+
return publicKeyPromise;
|
|
33
|
+
}
|
|
34
|
+
var privateKeyPromise = null;
|
|
35
|
+
var privateKeyCache = null;
|
|
36
|
+
function loadPrivateKey(privateJwk) {
|
|
37
|
+
if (!privateKeyPromise || privateKeyCache !== privateJwk) {
|
|
38
|
+
privateKeyCache = privateJwk;
|
|
39
|
+
privateKeyPromise = jose.importJWK(decodeJwk(privateJwk), JWT_ALG);
|
|
40
|
+
}
|
|
41
|
+
return privateKeyPromise;
|
|
42
|
+
}
|
|
43
|
+
function parsePublicJwk(publicJwk) {
|
|
44
|
+
return decodeJwk(publicJwk);
|
|
45
|
+
}
|
|
46
|
+
async function signSession(claims, cfg, ttlSeconds) {
|
|
47
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
48
|
+
return new jose.SignJWT(claims).setProtectedHeader({ alg: JWT_ALG, typ: "JWT", kid: cfg.kid }).setIssuer(cfg.issuer ?? DEFAULT_ISSUER).setIssuedAt(now).setExpirationTime(now + ttlSeconds).sign(await loadPrivateKey(cfg.privateJwk));
|
|
49
|
+
}
|
|
50
|
+
async function verifySessionToken(token, cfg) {
|
|
51
|
+
try {
|
|
52
|
+
const res = await jose.jwtVerify(token, await loadPublicKey(cfg.publicJwk), {
|
|
53
|
+
algorithms: [JWT_ALG],
|
|
54
|
+
issuer: cfg.issuer ?? DEFAULT_ISSUER
|
|
55
|
+
});
|
|
56
|
+
const p = res.payload;
|
|
57
|
+
if (!p.sub || !p.addr || !p.jti) return null;
|
|
58
|
+
return p;
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function serializeSessionCookie(token, opts = {}) {
|
|
64
|
+
const parts = [`${SESSION_COOKIE}=${token}`];
|
|
65
|
+
parts.push(`Path=${opts.path ?? "/"}`);
|
|
66
|
+
if (opts.httpOnly ?? true) parts.push("HttpOnly");
|
|
67
|
+
parts.push(`SameSite=${opts.sameSite ?? "Lax"}`);
|
|
68
|
+
if (opts.secure ?? true) parts.push("Secure");
|
|
69
|
+
if (opts.domain) parts.push(`Domain=${opts.domain}`);
|
|
70
|
+
if (opts.maxAge != null) parts.push(`Max-Age=${opts.maxAge}`);
|
|
71
|
+
if (opts.expires) parts.push(`Expires=${opts.expires.toUTCString()}`);
|
|
72
|
+
return parts.join("; ");
|
|
73
|
+
}
|
|
74
|
+
function clearSessionCookieHeader(opts = {}) {
|
|
75
|
+
return serializeSessionCookie("", {
|
|
76
|
+
...opts,
|
|
77
|
+
maxAge: 0,
|
|
78
|
+
expires: /* @__PURE__ */ new Date(0)
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function readSessionCookie(cookieHeader) {
|
|
82
|
+
if (!cookieHeader) return null;
|
|
83
|
+
const prefix = `${SESSION_COOKIE}=`;
|
|
84
|
+
for (const part of cookieHeader.split(";")) {
|
|
85
|
+
const trimmed = part.trim();
|
|
86
|
+
if (trimmed.startsWith(prefix)) {
|
|
87
|
+
const val = trimmed.slice(prefix.length);
|
|
88
|
+
return val.length > 0 ? val : null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
exports.DEFAULT_ISSUER = DEFAULT_ISSUER;
|
|
95
|
+
exports.JWT_ALG = JWT_ALG;
|
|
96
|
+
exports.SESSION_COOKIE = SESSION_COOKIE;
|
|
97
|
+
exports.clearSessionCookieHeader = clearSessionCookieHeader;
|
|
98
|
+
exports.loadPrivateKey = loadPrivateKey;
|
|
99
|
+
exports.loadPublicKey = loadPublicKey;
|
|
100
|
+
exports.parsePublicJwk = parsePublicJwk;
|
|
101
|
+
exports.readSessionCookie = readSessionCookie;
|
|
102
|
+
exports.serializeSessionCookie = serializeSessionCookie;
|
|
103
|
+
exports.signSession = signSession;
|
|
104
|
+
exports.verifySessionToken = verifySessionToken;
|
|
105
|
+
//# sourceMappingURL=index.js.map
|
|
106
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["importJWK","SignJWT","jwtVerify"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @orangecheck/auth-core\n *\n * Cross-subdomain oc_session JWT verification + cookie helpers.\n *\n * The auth HOST (whoever owns sign-in) holds a private key and issues\n * tokens with `signSession()`. Every CONSUMER subdomain holds only the\n * public JWK and verifies tokens locally with `verifySessionToken()` —\n * no network round-trip, no shared secret.\n *\n * The ochk.io ecosystem picks Ed25519 (EdDSA) with JWKs distributed\n * via environment variable. The public key is also published at\n * `/.well-known/jwks.json` on the auth host for dynamic discovery.\n */\n\nimport {\n importJWK,\n jwtVerify,\n SignJWT,\n type CryptoKey,\n type JWTPayload,\n type KeyObject,\n} from 'jose';\n\nexport const JWT_ALG = 'EdDSA' as const;\nexport const SESSION_COOKIE = 'oc_session' as const;\nexport const DEFAULT_ISSUER = 'https://ochk.io' as const;\n\nexport type AuthKey = CryptoKey | KeyObject;\n\nexport interface SessionPayload extends JWTPayload {\n sub: string;\n addr: string;\n jti: string;\n}\n\nexport interface VerifyConfig {\n /** Base64url-encoded JWK containing the Ed25519 public key. */\n publicJwk: string;\n /** Expected `iss` claim. Tokens with a different issuer are rejected. */\n issuer?: string;\n}\n\nexport interface SignConfig extends VerifyConfig {\n /** Base64url-encoded JWK containing the Ed25519 private key. */\n privateJwk: string;\n /** Short key id placed in the JWT header (`kid`). Must match the JWK. */\n kid: string;\n}\n\n// ─── JWK helpers ────────────────────────────────────────────────────────\n\nfunction decodeJwk(value: string): Record<string, unknown> {\n const json =\n typeof Buffer !== 'undefined'\n ? Buffer.from(value, 'base64url').toString('utf8')\n : new TextDecoder().decode(base64UrlDecode(value));\n const parsed = JSON.parse(json) as Record<string, unknown>;\n if (!parsed || typeof parsed !== 'object') {\n throw new Error('[@orangecheck/auth-core] JWK env did not decode to an object');\n }\n return parsed;\n}\n\nfunction base64UrlDecode(input: string): Uint8Array {\n const pad = input.length % 4 === 0 ? '' : '='.repeat(4 - (input.length % 4));\n const b64 = (input + pad).replace(/-/g, '+').replace(/_/g, '/');\n const bin = typeof atob !== 'undefined' ? atob(b64) : '';\n const bytes = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);\n return bytes;\n}\n\nlet publicKeyPromise: Promise<AuthKey> | null = null;\nlet publicKeyCache: string | null = null;\nexport function loadPublicKey(publicJwk: string): Promise<AuthKey> {\n if (!publicKeyPromise || publicKeyCache !== publicJwk) {\n publicKeyCache = publicJwk;\n publicKeyPromise = importJWK(decodeJwk(publicJwk), JWT_ALG) as Promise<AuthKey>;\n }\n return publicKeyPromise;\n}\n\nlet privateKeyPromise: Promise<AuthKey> | null = null;\nlet privateKeyCache: string | null = null;\nexport function loadPrivateKey(privateJwk: string): Promise<AuthKey> {\n if (!privateKeyPromise || privateKeyCache !== privateJwk) {\n privateKeyCache = privateJwk;\n privateKeyPromise = importJWK(decodeJwk(privateJwk), JWT_ALG) as Promise<AuthKey>;\n }\n return privateKeyPromise;\n}\n\n/** Parse the `OC_AUTH_PUBLIC_JWK` env var back into a JWK object. */\nexport function parsePublicJwk(publicJwk: string): Record<string, unknown> {\n return decodeJwk(publicJwk);\n}\n\n// ─── Sign / verify ──────────────────────────────────────────────────────\n\n/**\n * Mint a fresh oc_session JWT. Only called on the auth host (the only\n * place that holds the private key).\n *\n * The caller owns persisting `jti` to a revocation list if they want\n * revocation semantics — auth-core is deliberately stateless.\n */\nexport async function signSession(\n claims: { sub: string; addr: string; jti: string },\n cfg: SignConfig,\n ttlSeconds: number\n): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\n return new SignJWT(claims)\n .setProtectedHeader({ alg: JWT_ALG, typ: 'JWT', kid: cfg.kid })\n .setIssuer(cfg.issuer ?? DEFAULT_ISSUER)\n .setIssuedAt(now)\n .setExpirationTime(now + ttlSeconds)\n .sign(await loadPrivateKey(cfg.privateJwk));\n}\n\n/**\n * Crypto-only JWT verify. Returns the payload on success, `null` on\n * any failure (bad signature, expired, wrong issuer, malformed).\n *\n * Safe for consumer subdomains — they only need the public JWK.\n * Revocation-aware checks live on the auth host.\n */\nexport async function verifySessionToken(\n token: string,\n cfg: VerifyConfig\n): Promise<SessionPayload | null> {\n try {\n const res = await jwtVerify(token, await loadPublicKey(cfg.publicJwk), {\n algorithms: [JWT_ALG],\n issuer: cfg.issuer ?? DEFAULT_ISSUER,\n });\n const p = res.payload as SessionPayload;\n if (!p.sub || !p.addr || !p.jti) return null;\n return p;\n } catch {\n return null;\n }\n}\n\n// ─── Cookie helpers ─────────────────────────────────────────────────────\n\nexport interface CookieOptions {\n domain?: string;\n maxAge?: number;\n expires?: Date;\n path?: string;\n secure?: boolean;\n httpOnly?: boolean;\n sameSite?: 'Lax' | 'Strict' | 'None';\n}\n\n/**\n * Serialize a cookie header value for `Set-Cookie`. Defaults match the\n * ochk.io session shape: HttpOnly, SameSite=Lax, Path=/, Secure in prod.\n */\nexport function serializeSessionCookie(\n token: string,\n opts: CookieOptions = {}\n): string {\n const parts: string[] = [`${SESSION_COOKIE}=${token}`];\n parts.push(`Path=${opts.path ?? '/'}`);\n if (opts.httpOnly ?? true) parts.push('HttpOnly');\n parts.push(`SameSite=${opts.sameSite ?? 'Lax'}`);\n if (opts.secure ?? true) parts.push('Secure');\n if (opts.domain) parts.push(`Domain=${opts.domain}`);\n if (opts.maxAge != null) parts.push(`Max-Age=${opts.maxAge}`);\n if (opts.expires) parts.push(`Expires=${opts.expires.toUTCString()}`);\n return parts.join('; ');\n}\n\n/** Clear the session cookie. Must match the Domain used when it was set. */\nexport function clearSessionCookieHeader(opts: Pick<CookieOptions, 'domain' | 'path' | 'secure'> = {}): string {\n return serializeSessionCookie('', {\n ...opts,\n maxAge: 0,\n expires: new Date(0),\n });\n}\n\n/**\n * Read the oc_session token out of a raw `Cookie:` header string.\n * Returns `null` if the cookie is missing or empty.\n */\nexport function readSessionCookie(cookieHeader: string | null | undefined): string | null {\n if (!cookieHeader) return null;\n const prefix = `${SESSION_COOKIE}=`;\n for (const part of cookieHeader.split(';')) {\n const trimmed = part.trim();\n if (trimmed.startsWith(prefix)) {\n const val = trimmed.slice(prefix.length);\n return val.length > 0 ? val : null;\n }\n }\n return null;\n}\n"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { importJWK, SignJWT, jwtVerify } from 'jose';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
var JWT_ALG = "EdDSA";
|
|
5
|
+
var SESSION_COOKIE = "oc_session";
|
|
6
|
+
var DEFAULT_ISSUER = "https://ochk.io";
|
|
7
|
+
function decodeJwk(value) {
|
|
8
|
+
const json = typeof Buffer !== "undefined" ? Buffer.from(value, "base64url").toString("utf8") : new TextDecoder().decode(base64UrlDecode(value));
|
|
9
|
+
const parsed = JSON.parse(json);
|
|
10
|
+
if (!parsed || typeof parsed !== "object") {
|
|
11
|
+
throw new Error("[@orangecheck/auth-core] JWK env did not decode to an object");
|
|
12
|
+
}
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
15
|
+
function base64UrlDecode(input) {
|
|
16
|
+
const pad = input.length % 4 === 0 ? "" : "=".repeat(4 - input.length % 4);
|
|
17
|
+
const b64 = (input + pad).replace(/-/g, "+").replace(/_/g, "/");
|
|
18
|
+
const bin = typeof atob !== "undefined" ? atob(b64) : "";
|
|
19
|
+
const bytes = new Uint8Array(bin.length);
|
|
20
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
21
|
+
return bytes;
|
|
22
|
+
}
|
|
23
|
+
var publicKeyPromise = null;
|
|
24
|
+
var publicKeyCache = null;
|
|
25
|
+
function loadPublicKey(publicJwk) {
|
|
26
|
+
if (!publicKeyPromise || publicKeyCache !== publicJwk) {
|
|
27
|
+
publicKeyCache = publicJwk;
|
|
28
|
+
publicKeyPromise = importJWK(decodeJwk(publicJwk), JWT_ALG);
|
|
29
|
+
}
|
|
30
|
+
return publicKeyPromise;
|
|
31
|
+
}
|
|
32
|
+
var privateKeyPromise = null;
|
|
33
|
+
var privateKeyCache = null;
|
|
34
|
+
function loadPrivateKey(privateJwk) {
|
|
35
|
+
if (!privateKeyPromise || privateKeyCache !== privateJwk) {
|
|
36
|
+
privateKeyCache = privateJwk;
|
|
37
|
+
privateKeyPromise = importJWK(decodeJwk(privateJwk), JWT_ALG);
|
|
38
|
+
}
|
|
39
|
+
return privateKeyPromise;
|
|
40
|
+
}
|
|
41
|
+
function parsePublicJwk(publicJwk) {
|
|
42
|
+
return decodeJwk(publicJwk);
|
|
43
|
+
}
|
|
44
|
+
async function signSession(claims, cfg, ttlSeconds) {
|
|
45
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
46
|
+
return new SignJWT(claims).setProtectedHeader({ alg: JWT_ALG, typ: "JWT", kid: cfg.kid }).setIssuer(cfg.issuer ?? DEFAULT_ISSUER).setIssuedAt(now).setExpirationTime(now + ttlSeconds).sign(await loadPrivateKey(cfg.privateJwk));
|
|
47
|
+
}
|
|
48
|
+
async function verifySessionToken(token, cfg) {
|
|
49
|
+
try {
|
|
50
|
+
const res = await jwtVerify(token, await loadPublicKey(cfg.publicJwk), {
|
|
51
|
+
algorithms: [JWT_ALG],
|
|
52
|
+
issuer: cfg.issuer ?? DEFAULT_ISSUER
|
|
53
|
+
});
|
|
54
|
+
const p = res.payload;
|
|
55
|
+
if (!p.sub || !p.addr || !p.jti) return null;
|
|
56
|
+
return p;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function serializeSessionCookie(token, opts = {}) {
|
|
62
|
+
const parts = [`${SESSION_COOKIE}=${token}`];
|
|
63
|
+
parts.push(`Path=${opts.path ?? "/"}`);
|
|
64
|
+
if (opts.httpOnly ?? true) parts.push("HttpOnly");
|
|
65
|
+
parts.push(`SameSite=${opts.sameSite ?? "Lax"}`);
|
|
66
|
+
if (opts.secure ?? true) parts.push("Secure");
|
|
67
|
+
if (opts.domain) parts.push(`Domain=${opts.domain}`);
|
|
68
|
+
if (opts.maxAge != null) parts.push(`Max-Age=${opts.maxAge}`);
|
|
69
|
+
if (opts.expires) parts.push(`Expires=${opts.expires.toUTCString()}`);
|
|
70
|
+
return parts.join("; ");
|
|
71
|
+
}
|
|
72
|
+
function clearSessionCookieHeader(opts = {}) {
|
|
73
|
+
return serializeSessionCookie("", {
|
|
74
|
+
...opts,
|
|
75
|
+
maxAge: 0,
|
|
76
|
+
expires: /* @__PURE__ */ new Date(0)
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function readSessionCookie(cookieHeader) {
|
|
80
|
+
if (!cookieHeader) return null;
|
|
81
|
+
const prefix = `${SESSION_COOKIE}=`;
|
|
82
|
+
for (const part of cookieHeader.split(";")) {
|
|
83
|
+
const trimmed = part.trim();
|
|
84
|
+
if (trimmed.startsWith(prefix)) {
|
|
85
|
+
const val = trimmed.slice(prefix.length);
|
|
86
|
+
return val.length > 0 ? val : null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export { DEFAULT_ISSUER, JWT_ALG, SESSION_COOKIE, clearSessionCookieHeader, loadPrivateKey, loadPublicKey, parsePublicJwk, readSessionCookie, serializeSessionCookie, signSession, verifySessionToken };
|
|
93
|
+
//# sourceMappingURL=index.mjs.map
|
|
94
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @orangecheck/auth-core\n *\n * Cross-subdomain oc_session JWT verification + cookie helpers.\n *\n * The auth HOST (whoever owns sign-in) holds a private key and issues\n * tokens with `signSession()`. Every CONSUMER subdomain holds only the\n * public JWK and verifies tokens locally with `verifySessionToken()` —\n * no network round-trip, no shared secret.\n *\n * The ochk.io ecosystem picks Ed25519 (EdDSA) with JWKs distributed\n * via environment variable. The public key is also published at\n * `/.well-known/jwks.json` on the auth host for dynamic discovery.\n */\n\nimport {\n importJWK,\n jwtVerify,\n SignJWT,\n type CryptoKey,\n type JWTPayload,\n type KeyObject,\n} from 'jose';\n\nexport const JWT_ALG = 'EdDSA' as const;\nexport const SESSION_COOKIE = 'oc_session' as const;\nexport const DEFAULT_ISSUER = 'https://ochk.io' as const;\n\nexport type AuthKey = CryptoKey | KeyObject;\n\nexport interface SessionPayload extends JWTPayload {\n sub: string;\n addr: string;\n jti: string;\n}\n\nexport interface VerifyConfig {\n /** Base64url-encoded JWK containing the Ed25519 public key. */\n publicJwk: string;\n /** Expected `iss` claim. Tokens with a different issuer are rejected. */\n issuer?: string;\n}\n\nexport interface SignConfig extends VerifyConfig {\n /** Base64url-encoded JWK containing the Ed25519 private key. */\n privateJwk: string;\n /** Short key id placed in the JWT header (`kid`). Must match the JWK. */\n kid: string;\n}\n\n// ─── JWK helpers ────────────────────────────────────────────────────────\n\nfunction decodeJwk(value: string): Record<string, unknown> {\n const json =\n typeof Buffer !== 'undefined'\n ? Buffer.from(value, 'base64url').toString('utf8')\n : new TextDecoder().decode(base64UrlDecode(value));\n const parsed = JSON.parse(json) as Record<string, unknown>;\n if (!parsed || typeof parsed !== 'object') {\n throw new Error('[@orangecheck/auth-core] JWK env did not decode to an object');\n }\n return parsed;\n}\n\nfunction base64UrlDecode(input: string): Uint8Array {\n const pad = input.length % 4 === 0 ? '' : '='.repeat(4 - (input.length % 4));\n const b64 = (input + pad).replace(/-/g, '+').replace(/_/g, '/');\n const bin = typeof atob !== 'undefined' ? atob(b64) : '';\n const bytes = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);\n return bytes;\n}\n\nlet publicKeyPromise: Promise<AuthKey> | null = null;\nlet publicKeyCache: string | null = null;\nexport function loadPublicKey(publicJwk: string): Promise<AuthKey> {\n if (!publicKeyPromise || publicKeyCache !== publicJwk) {\n publicKeyCache = publicJwk;\n publicKeyPromise = importJWK(decodeJwk(publicJwk), JWT_ALG) as Promise<AuthKey>;\n }\n return publicKeyPromise;\n}\n\nlet privateKeyPromise: Promise<AuthKey> | null = null;\nlet privateKeyCache: string | null = null;\nexport function loadPrivateKey(privateJwk: string): Promise<AuthKey> {\n if (!privateKeyPromise || privateKeyCache !== privateJwk) {\n privateKeyCache = privateJwk;\n privateKeyPromise = importJWK(decodeJwk(privateJwk), JWT_ALG) as Promise<AuthKey>;\n }\n return privateKeyPromise;\n}\n\n/** Parse the `OC_AUTH_PUBLIC_JWK` env var back into a JWK object. */\nexport function parsePublicJwk(publicJwk: string): Record<string, unknown> {\n return decodeJwk(publicJwk);\n}\n\n// ─── Sign / verify ──────────────────────────────────────────────────────\n\n/**\n * Mint a fresh oc_session JWT. Only called on the auth host (the only\n * place that holds the private key).\n *\n * The caller owns persisting `jti` to a revocation list if they want\n * revocation semantics — auth-core is deliberately stateless.\n */\nexport async function signSession(\n claims: { sub: string; addr: string; jti: string },\n cfg: SignConfig,\n ttlSeconds: number\n): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\n return new SignJWT(claims)\n .setProtectedHeader({ alg: JWT_ALG, typ: 'JWT', kid: cfg.kid })\n .setIssuer(cfg.issuer ?? DEFAULT_ISSUER)\n .setIssuedAt(now)\n .setExpirationTime(now + ttlSeconds)\n .sign(await loadPrivateKey(cfg.privateJwk));\n}\n\n/**\n * Crypto-only JWT verify. Returns the payload on success, `null` on\n * any failure (bad signature, expired, wrong issuer, malformed).\n *\n * Safe for consumer subdomains — they only need the public JWK.\n * Revocation-aware checks live on the auth host.\n */\nexport async function verifySessionToken(\n token: string,\n cfg: VerifyConfig\n): Promise<SessionPayload | null> {\n try {\n const res = await jwtVerify(token, await loadPublicKey(cfg.publicJwk), {\n algorithms: [JWT_ALG],\n issuer: cfg.issuer ?? DEFAULT_ISSUER,\n });\n const p = res.payload as SessionPayload;\n if (!p.sub || !p.addr || !p.jti) return null;\n return p;\n } catch {\n return null;\n }\n}\n\n// ─── Cookie helpers ─────────────────────────────────────────────────────\n\nexport interface CookieOptions {\n domain?: string;\n maxAge?: number;\n expires?: Date;\n path?: string;\n secure?: boolean;\n httpOnly?: boolean;\n sameSite?: 'Lax' | 'Strict' | 'None';\n}\n\n/**\n * Serialize a cookie header value for `Set-Cookie`. Defaults match the\n * ochk.io session shape: HttpOnly, SameSite=Lax, Path=/, Secure in prod.\n */\nexport function serializeSessionCookie(\n token: string,\n opts: CookieOptions = {}\n): string {\n const parts: string[] = [`${SESSION_COOKIE}=${token}`];\n parts.push(`Path=${opts.path ?? '/'}`);\n if (opts.httpOnly ?? true) parts.push('HttpOnly');\n parts.push(`SameSite=${opts.sameSite ?? 'Lax'}`);\n if (opts.secure ?? true) parts.push('Secure');\n if (opts.domain) parts.push(`Domain=${opts.domain}`);\n if (opts.maxAge != null) parts.push(`Max-Age=${opts.maxAge}`);\n if (opts.expires) parts.push(`Expires=${opts.expires.toUTCString()}`);\n return parts.join('; ');\n}\n\n/** Clear the session cookie. Must match the Domain used when it was set. */\nexport function clearSessionCookieHeader(opts: Pick<CookieOptions, 'domain' | 'path' | 'secure'> = {}): string {\n return serializeSessionCookie('', {\n ...opts,\n maxAge: 0,\n expires: new Date(0),\n });\n}\n\n/**\n * Read the oc_session token out of a raw `Cookie:` header string.\n * Returns `null` if the cookie is missing or empty.\n */\nexport function readSessionCookie(cookieHeader: string | null | undefined): string | null {\n if (!cookieHeader) return null;\n const prefix = `${SESSION_COOKIE}=`;\n for (const part of cookieHeader.split(';')) {\n const trimmed = part.trim();\n if (trimmed.startsWith(prefix)) {\n const val = trimmed.slice(prefix.length);\n return val.length > 0 ? val : null;\n }\n }\n return null;\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@orangecheck/auth-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Cross-subdomain oc_session JWT verification and cookie helpers. The auth primitive every OrangeCheck app imports.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"orangecheck",
|
|
7
|
+
"bitcoin",
|
|
8
|
+
"auth",
|
|
9
|
+
"jwt",
|
|
10
|
+
"ed25519",
|
|
11
|
+
"sso",
|
|
12
|
+
"sybil-resistance"
|
|
13
|
+
],
|
|
14
|
+
"author": "OrangeCheck",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"homepage": "https://ochk.io",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/orangecheck/oc-packages.git",
|
|
20
|
+
"directory": "auth-core"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/orangecheck/oc-packages/issues"
|
|
24
|
+
},
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"module": "./dist/index.mjs",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.mjs",
|
|
32
|
+
"require": "./dist/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsup",
|
|
42
|
+
"dev": "tsup --watch",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"test:watch": "vitest",
|
|
45
|
+
"type-check": "tsc --noEmit",
|
|
46
|
+
"clean": "rm -rf dist",
|
|
47
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"jose": "^6.2.2"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^25.6.0",
|
|
54
|
+
"tsup": "^8.5.1",
|
|
55
|
+
"typescript": "^6.0.3",
|
|
56
|
+
"vitest": "^4.1.5"
|
|
57
|
+
}
|
|
58
|
+
}
|