authfyio-backend 0.2.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/README.md +88 -0
- package/dist/admin.d.ts +130 -0
- package/dist/admin.d.ts.map +1 -0
- package/dist/admin.js +166 -0
- package/dist/client.d.ts +88 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +114 -0
- package/dist/cookies.d.ts +3 -0
- package/dist/cookies.d.ts.map +1 -0
- package/dist/cookies.js +20 -0
- package/dist/cookies.spec.d.ts +2 -0
- package/dist/cookies.spec.d.ts.map +1 -0
- package/dist/cookies.spec.js +46 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/middleware.d.ts +16 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +13 -0
- package/dist/payments.d.ts +115 -0
- package/dist/payments.d.ts.map +1 -0
- package/dist/payments.js +81 -0
- package/dist/session.d.ts +29 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +15 -0
- package/dist/webhooks.d.ts +43 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +52 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# authfyio-backend
|
|
2
|
+
|
|
3
|
+
Server-side SDK for verifying Authfyio sessions in Node.js backends. Built for Express, Fastify, Nest, Koa, Hono, bare `http`, or anything that exposes a standard cookie header.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
- Verifies the short-lived `__session` JWT issued by a Authfyio instance against its JWKS endpoint.
|
|
8
|
+
- Parses `Cookie` headers and extracts the session JWT without you having to touch cookie-parsing middleware.
|
|
9
|
+
- Returns fully-typed session claims: `sub` (user id), `sid` (session id), `env` (environment id), plus any custom claims your instance has attached.
|
|
10
|
+
|
|
11
|
+
It does **not** perform user creation, password flows, or OAuth redirects — those belong on the instance API and in the browser SDK (`authfyio-react`, `authfyio-nextjs`). This package exists so your backend can trust a request without a round-trip to the auth service on every call.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install authfyio-backend
|
|
17
|
+
# or within the monorepo
|
|
18
|
+
npm install -w authfyio-backend
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requires Node 20+.
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { AuthfyioBackendClient } from 'authfyio-backend';
|
|
27
|
+
|
|
28
|
+
const af = new AuthfyioBackendClient({
|
|
29
|
+
baseUrl: process.env.AF_API_BASE_URL!, // e.g. https://auth.example.com
|
|
30
|
+
issuer: process.env.AF_JWT_ISSUER, // matches the instance `iss`
|
|
31
|
+
audience: process.env.AF_JWT_AUDIENCE, // optional
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Express
|
|
35
|
+
app.get('/api/me', async (req, res) => {
|
|
36
|
+
const session = await af.getSessionFromRequest(req);
|
|
37
|
+
if (!session) return res.status(401).json({ error: 'unauthenticated' });
|
|
38
|
+
|
|
39
|
+
res.json({
|
|
40
|
+
userId: session.sub,
|
|
41
|
+
sessionId: session.sid,
|
|
42
|
+
environment: session.env,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The client fetches and caches JWKS the first time you verify a token. JWKS rotation is picked up automatically on cache expiry — you do not need to restart your service after a key rotation on the auth side.
|
|
48
|
+
|
|
49
|
+
## API
|
|
50
|
+
|
|
51
|
+
### `new AuthfyioBackendClient(options)`
|
|
52
|
+
|
|
53
|
+
| Option | Type | Required | Description |
|
|
54
|
+
| ----------- | -------- | -------- | ----------------------------------------------------------------------------- |
|
|
55
|
+
| `baseUrl` | `string` | yes | Instance API base URL. JWKS is discovered at `${baseUrl}/.well-known/jwks.json`. |
|
|
56
|
+
| `issuer` | `string` | no | Expected `iss` claim. Strongly recommended in production. |
|
|
57
|
+
| `audience` | `string` | no | Expected `aud` claim, if your instance sets it. |
|
|
58
|
+
|
|
59
|
+
### `client.verifySessionJwt(token)`
|
|
60
|
+
|
|
61
|
+
Verifies the token against the instance's JWKS and returns the claims. Throws if the token is invalid, expired, or signed by an unknown key.
|
|
62
|
+
|
|
63
|
+
### `client.getSessionFromRequest(req)`
|
|
64
|
+
|
|
65
|
+
Pulls the `__session` cookie out of `req.headers.cookie`, then verifies it. Returns `null` when no cookie is present. Throws on an invalid token so your error middleware can decide how to respond.
|
|
66
|
+
|
|
67
|
+
### Low-level helpers
|
|
68
|
+
|
|
69
|
+
- `parseCookieHeader(header)` — returns a plain `Record<string, string>`.
|
|
70
|
+
- `getSessionJwtFromCookieHeader(header)` — returns the `__session` JWT or `null`.
|
|
71
|
+
- `verifySessionJwt(token, options)` — standalone verifier if you prefer not to use the class.
|
|
72
|
+
|
|
73
|
+
## How sessions work (short version)
|
|
74
|
+
|
|
75
|
+
Authfyio uses a two-cookie design:
|
|
76
|
+
|
|
77
|
+
- `__client` (httpOnly, long-lived) holds the refresh-capable session reference. It never leaves the browser → auth-API roundtrip.
|
|
78
|
+
- `__session` (readable by JS, ~60s TTL) is a signed JWT your services verify locally. Short lifetime means a revoked session stops working within the skew window without you having to hit the auth API.
|
|
79
|
+
|
|
80
|
+
This package is the "verify `__session` locally" half. The browser SDK handles the refresh loop.
|
|
81
|
+
|
|
82
|
+
## Error handling
|
|
83
|
+
|
|
84
|
+
`jose`-thrown errors bubble up with their original names (`JWSSignatureVerificationFailed`, `JWTExpired`, etc.). Production services should log the error class, not the token, and return a generic 401 to the client.
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT
|
package/dist/admin.d.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin SDK — server-to-server calls to the instance API's `/v1/admin/*`
|
|
3
|
+
* surface. Authenticates with a secret API key minted from the dashboard.
|
|
4
|
+
*
|
|
5
|
+
* const kolay = createAuthfyioClient({
|
|
6
|
+
* secretKey: process.env.AUTHFYIO_SECRET_KEY!, // sk_live_...
|
|
7
|
+
* });
|
|
8
|
+
* const { users } = await kolay.users.list({ limit: 10 });
|
|
9
|
+
*/
|
|
10
|
+
export type AuthfyioClientOptions = {
|
|
11
|
+
baseUrl: string;
|
|
12
|
+
/** Bearer token starting with `sk_live_` or `sk_test_`. */
|
|
13
|
+
secretKey: string;
|
|
14
|
+
/** Optional fetch override (for Node < 18 or test mocks). */
|
|
15
|
+
fetch?: typeof fetch;
|
|
16
|
+
};
|
|
17
|
+
export type AdminUser = {
|
|
18
|
+
id: string;
|
|
19
|
+
username: string | null;
|
|
20
|
+
externalId: string | null;
|
|
21
|
+
isBanned: boolean;
|
|
22
|
+
banReason: string | null;
|
|
23
|
+
publicMetadata: Record<string, unknown>;
|
|
24
|
+
privateMetadata: Record<string, unknown>;
|
|
25
|
+
unsafeMetadata?: Record<string, unknown>;
|
|
26
|
+
createdAt: string;
|
|
27
|
+
updatedAt: string;
|
|
28
|
+
emails: Array<{
|
|
29
|
+
email: string;
|
|
30
|
+
isPrimary: boolean;
|
|
31
|
+
isVerified: boolean;
|
|
32
|
+
}>;
|
|
33
|
+
};
|
|
34
|
+
export type AdminOrganization = {
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
slug: string;
|
|
38
|
+
createdAt: string;
|
|
39
|
+
};
|
|
40
|
+
export type AdminSession = {
|
|
41
|
+
id: string;
|
|
42
|
+
userId: string;
|
|
43
|
+
status: 'active' | 'ended' | 'expired' | 'revoked';
|
|
44
|
+
activeOrgId: string | null;
|
|
45
|
+
createdAt: string;
|
|
46
|
+
expiresAt: string;
|
|
47
|
+
endedAt: string | null;
|
|
48
|
+
};
|
|
49
|
+
export type AdminInvitation = {
|
|
50
|
+
id: string;
|
|
51
|
+
email: string;
|
|
52
|
+
role: string;
|
|
53
|
+
/** Returned once at creation; use to build your own invite-email URL. */
|
|
54
|
+
token?: string;
|
|
55
|
+
};
|
|
56
|
+
export declare class AuthfyioAdminClient {
|
|
57
|
+
private readonly baseUrl;
|
|
58
|
+
private readonly secretKey;
|
|
59
|
+
private readonly fetchImpl;
|
|
60
|
+
readonly users: UsersResource;
|
|
61
|
+
readonly organizations: OrganizationsResource;
|
|
62
|
+
readonly invitations: InvitationsResource;
|
|
63
|
+
readonly sessions: SessionsResource;
|
|
64
|
+
constructor(opts: AuthfyioClientOptions);
|
|
65
|
+
request<T>(path: string, init?: RequestInit): Promise<T>;
|
|
66
|
+
}
|
|
67
|
+
declare class UsersResource {
|
|
68
|
+
private readonly client;
|
|
69
|
+
constructor(client: AuthfyioAdminClient);
|
|
70
|
+
list(params?: {
|
|
71
|
+
limit?: number;
|
|
72
|
+
offset?: number;
|
|
73
|
+
email?: string;
|
|
74
|
+
}): Promise<{
|
|
75
|
+
users: AdminUser[];
|
|
76
|
+
total: number;
|
|
77
|
+
}>;
|
|
78
|
+
get(userId: string): Promise<AdminUser>;
|
|
79
|
+
update(userId: string, patch: Partial<Pick<AdminUser, 'username' | 'externalId' | 'isBanned' | 'banReason' | 'publicMetadata' | 'privateMetadata'>>): Promise<void>;
|
|
80
|
+
ban(userId: string, reason?: string): Promise<void>;
|
|
81
|
+
unban(userId: string): Promise<void>;
|
|
82
|
+
delete(userId: string): Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
declare class OrganizationsResource {
|
|
85
|
+
private readonly client;
|
|
86
|
+
constructor(client: AuthfyioAdminClient);
|
|
87
|
+
list(params?: {
|
|
88
|
+
limit?: number;
|
|
89
|
+
offset?: number;
|
|
90
|
+
}): Promise<{
|
|
91
|
+
organizations: AdminOrganization[];
|
|
92
|
+
total: number;
|
|
93
|
+
}>;
|
|
94
|
+
create(params: {
|
|
95
|
+
name: string;
|
|
96
|
+
slug: string;
|
|
97
|
+
createdBy?: string;
|
|
98
|
+
}): Promise<AdminOrganization>;
|
|
99
|
+
delete(orgId: string): Promise<void>;
|
|
100
|
+
addMember(orgId: string, params: {
|
|
101
|
+
userId: string;
|
|
102
|
+
role?: string;
|
|
103
|
+
}): Promise<void>;
|
|
104
|
+
removeMember(orgId: string, userId: string): Promise<void>;
|
|
105
|
+
}
|
|
106
|
+
declare class InvitationsResource {
|
|
107
|
+
private readonly client;
|
|
108
|
+
constructor(client: AuthfyioAdminClient);
|
|
109
|
+
create(params: {
|
|
110
|
+
email: string;
|
|
111
|
+
orgId: string;
|
|
112
|
+
role?: string;
|
|
113
|
+
}): Promise<AdminInvitation>;
|
|
114
|
+
revoke(invitationId: string): Promise<void>;
|
|
115
|
+
}
|
|
116
|
+
declare class SessionsResource {
|
|
117
|
+
private readonly client;
|
|
118
|
+
constructor(client: AuthfyioAdminClient);
|
|
119
|
+
list(params?: {
|
|
120
|
+
userId?: string;
|
|
121
|
+
limit?: number;
|
|
122
|
+
}): Promise<{
|
|
123
|
+
sessions: AdminSession[];
|
|
124
|
+
}>;
|
|
125
|
+
revoke(sessionId: string): Promise<void>;
|
|
126
|
+
}
|
|
127
|
+
/** Factory function — */
|
|
128
|
+
export declare function createAuthfyioClient(opts: AuthfyioClientOptions): AuthfyioAdminClient;
|
|
129
|
+
export {};
|
|
130
|
+
//# sourceMappingURL=admin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin.d.ts","sourceRoot":"","sources":["../src/admin.ts"],"names":[],"mappings":"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"}
|
package/dist/admin.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin SDK — server-to-server calls to the instance API's `/v1/admin/*`
|
|
3
|
+
* surface. Authenticates with a secret API key minted from the dashboard.
|
|
4
|
+
*
|
|
5
|
+
* const kolay = createAuthfyioClient({
|
|
6
|
+
* secretKey: process.env.AUTHFYIO_SECRET_KEY!, // sk_live_...
|
|
7
|
+
* });
|
|
8
|
+
* const { users } = await kolay.users.list({ limit: 10 });
|
|
9
|
+
*/
|
|
10
|
+
export class AuthfyioAdminClient {
|
|
11
|
+
baseUrl;
|
|
12
|
+
secretKey;
|
|
13
|
+
fetchImpl;
|
|
14
|
+
users;
|
|
15
|
+
organizations;
|
|
16
|
+
invitations;
|
|
17
|
+
sessions;
|
|
18
|
+
constructor(opts) {
|
|
19
|
+
if (!opts.secretKey?.startsWith('sk_')) {
|
|
20
|
+
throw new Error('AuthfyioAdminClient: secretKey must start with sk_');
|
|
21
|
+
}
|
|
22
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, '');
|
|
23
|
+
this.secretKey = opts.secretKey;
|
|
24
|
+
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
25
|
+
this.users = new UsersResource(this);
|
|
26
|
+
this.organizations = new OrganizationsResource(this);
|
|
27
|
+
this.invitations = new InvitationsResource(this);
|
|
28
|
+
this.sessions = new SessionsResource(this);
|
|
29
|
+
}
|
|
30
|
+
async request(path, init) {
|
|
31
|
+
const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
32
|
+
...init,
|
|
33
|
+
headers: {
|
|
34
|
+
'content-type': 'application/json',
|
|
35
|
+
accept: 'application/json',
|
|
36
|
+
authorization: `Bearer ${this.secretKey}`,
|
|
37
|
+
...(init?.headers ?? {}),
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
const text = await res.text();
|
|
41
|
+
let body = null;
|
|
42
|
+
try {
|
|
43
|
+
body = text ? JSON.parse(text) : null;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
body = null;
|
|
47
|
+
}
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
const err = new Error(body?.message ?? `authfyio_admin_${res.status}`);
|
|
50
|
+
err.status = res.status;
|
|
51
|
+
err.body = body;
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
return body;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
class UsersResource {
|
|
58
|
+
client;
|
|
59
|
+
constructor(client) {
|
|
60
|
+
this.client = client;
|
|
61
|
+
}
|
|
62
|
+
async list(params) {
|
|
63
|
+
const qs = new URLSearchParams();
|
|
64
|
+
if (params?.limit !== undefined)
|
|
65
|
+
qs.set('limit', String(params.limit));
|
|
66
|
+
if (params?.offset !== undefined)
|
|
67
|
+
qs.set('offset', String(params.offset));
|
|
68
|
+
if (params?.email)
|
|
69
|
+
qs.set('email', params.email);
|
|
70
|
+
const body = await this.client.request(`/v1/admin/users${qs.toString() ? '?' + qs : ''}`);
|
|
71
|
+
return { users: body.users ?? [], total: body.total ?? 0 };
|
|
72
|
+
}
|
|
73
|
+
async get(userId) {
|
|
74
|
+
const body = await this.client.request(`/v1/admin/users/${encodeURIComponent(userId)}`);
|
|
75
|
+
return body.user;
|
|
76
|
+
}
|
|
77
|
+
async update(userId, patch) {
|
|
78
|
+
await this.client.request(`/v1/admin/users/${encodeURIComponent(userId)}`, {
|
|
79
|
+
method: 'PATCH',
|
|
80
|
+
body: JSON.stringify(patch),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
async ban(userId, reason) {
|
|
84
|
+
return this.update(userId, { isBanned: true, banReason: reason ?? null });
|
|
85
|
+
}
|
|
86
|
+
async unban(userId) {
|
|
87
|
+
return this.update(userId, { isBanned: false, banReason: null });
|
|
88
|
+
}
|
|
89
|
+
async delete(userId) {
|
|
90
|
+
await this.client.request(`/v1/admin/users/${encodeURIComponent(userId)}`, { method: 'DELETE' });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
class OrganizationsResource {
|
|
94
|
+
client;
|
|
95
|
+
constructor(client) {
|
|
96
|
+
this.client = client;
|
|
97
|
+
}
|
|
98
|
+
async list(params) {
|
|
99
|
+
const qs = new URLSearchParams();
|
|
100
|
+
if (params?.limit !== undefined)
|
|
101
|
+
qs.set('limit', String(params.limit));
|
|
102
|
+
if (params?.offset !== undefined)
|
|
103
|
+
qs.set('offset', String(params.offset));
|
|
104
|
+
const body = await this.client.request(`/v1/admin/organizations${qs.toString() ? '?' + qs : ''}`);
|
|
105
|
+
return { organizations: body.organizations ?? [], total: body.total ?? 0 };
|
|
106
|
+
}
|
|
107
|
+
async create(params) {
|
|
108
|
+
const body = await this.client.request(`/v1/admin/organizations`, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
body: JSON.stringify(params),
|
|
111
|
+
});
|
|
112
|
+
return body.organization;
|
|
113
|
+
}
|
|
114
|
+
async delete(orgId) {
|
|
115
|
+
await this.client.request(`/v1/admin/organizations/${encodeURIComponent(orgId)}`, { method: 'DELETE' });
|
|
116
|
+
}
|
|
117
|
+
async addMember(orgId, params) {
|
|
118
|
+
await this.client.request(`/v1/admin/organizations/${encodeURIComponent(orgId)}/memberships`, {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
body: JSON.stringify({ userId: params.userId, role: params.role ?? 'member' }),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
async removeMember(orgId, userId) {
|
|
124
|
+
await this.client.request(`/v1/admin/organizations/${encodeURIComponent(orgId)}/memberships/${encodeURIComponent(userId)}`, { method: 'DELETE' });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
class InvitationsResource {
|
|
128
|
+
client;
|
|
129
|
+
constructor(client) {
|
|
130
|
+
this.client = client;
|
|
131
|
+
}
|
|
132
|
+
async create(params) {
|
|
133
|
+
const body = await this.client.request(`/v1/admin/invitations`, {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
body: JSON.stringify({ email: params.email, orgId: params.orgId, role: params.role ?? 'member' }),
|
|
136
|
+
});
|
|
137
|
+
return body.invitation;
|
|
138
|
+
}
|
|
139
|
+
async revoke(invitationId) {
|
|
140
|
+
await this.client.request(`/v1/admin/invitations/${encodeURIComponent(invitationId)}`, { method: 'DELETE' });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
class SessionsResource {
|
|
144
|
+
client;
|
|
145
|
+
constructor(client) {
|
|
146
|
+
this.client = client;
|
|
147
|
+
}
|
|
148
|
+
async list(params) {
|
|
149
|
+
const qs = new URLSearchParams();
|
|
150
|
+
if (params?.userId)
|
|
151
|
+
qs.set('userId', params.userId);
|
|
152
|
+
if (params?.limit !== undefined)
|
|
153
|
+
qs.set('limit', String(params.limit));
|
|
154
|
+
const body = await this.client.request(`/v1/admin/sessions${qs.toString() ? '?' + qs : ''}`);
|
|
155
|
+
return { sessions: body.sessions ?? [] };
|
|
156
|
+
}
|
|
157
|
+
async revoke(sessionId) {
|
|
158
|
+
await this.client.request(`/v1/admin/sessions/${encodeURIComponent(sessionId)}/revoke`, {
|
|
159
|
+
method: 'POST',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Factory function — */
|
|
164
|
+
export function createAuthfyioClient(opts) {
|
|
165
|
+
return new AuthfyioAdminClient(opts);
|
|
166
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { type SessionClaims } from './session.js';
|
|
2
|
+
/**
|
|
3
|
+
* Canonical Authfyio API URL. Customers using the hosted SaaS shouldn't
|
|
4
|
+
* have to set anything — the default points at production. Override only
|
|
5
|
+
* when self-hosting or pointing at staging.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DEFAULT_API_BASE_URL = "https://api.authfyio.com";
|
|
8
|
+
export type AuthfyioBackendClientOptions = {
|
|
9
|
+
/**
|
|
10
|
+
* Base URL of the instance API. Defaults to `https://api.authfyio.com`
|
|
11
|
+
* (or `process.env.AF_API_BASE_URL` if set). You only need to override
|
|
12
|
+
* this when self-hosting.
|
|
13
|
+
*/
|
|
14
|
+
baseUrl?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Optional expected issuer. Matches your API's `AF_JWT_ISSUER`. When
|
|
17
|
+
* omitted, defaults to `https://api.authfyio.com`.
|
|
18
|
+
*/
|
|
19
|
+
issuer?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Optional expected audience.
|
|
22
|
+
*/
|
|
23
|
+
audience?: string;
|
|
24
|
+
};
|
|
25
|
+
export type BillingSnapshot = {
|
|
26
|
+
plan: {
|
|
27
|
+
id: string;
|
|
28
|
+
key: string | null;
|
|
29
|
+
name: string;
|
|
30
|
+
} | null;
|
|
31
|
+
features: Array<{
|
|
32
|
+
key: string;
|
|
33
|
+
name: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
}>;
|
|
36
|
+
status: string | null;
|
|
37
|
+
};
|
|
38
|
+
export type HasCheck = {
|
|
39
|
+
plan?: string;
|
|
40
|
+
feature?: string;
|
|
41
|
+
role?: string;
|
|
42
|
+
};
|
|
43
|
+
export declare class AuthfyioBackendClient {
|
|
44
|
+
private opts;
|
|
45
|
+
constructor(opts: AuthfyioBackendClientOptions);
|
|
46
|
+
getJwksUrl(): string;
|
|
47
|
+
private get baseUrl();
|
|
48
|
+
verifySessionJwt(token: string): Promise<SessionClaims>;
|
|
49
|
+
/**
|
|
50
|
+
* Extracts `__session` JWT from a Node/Express/Next request-like object and verifies it.
|
|
51
|
+
*/
|
|
52
|
+
getSessionFromRequest(req: {
|
|
53
|
+
headers?: {
|
|
54
|
+
cookie?: string;
|
|
55
|
+
};
|
|
56
|
+
}): Promise<SessionClaims | null>;
|
|
57
|
+
/**
|
|
58
|
+
* Fetches the current user's active plan + feature list from the instance
|
|
59
|
+
* API. Backs server-side `auth().has({ plan, feature })` in route handlers.
|
|
60
|
+
* Forwards the caller's `__session` cookie so the API can identify them.
|
|
61
|
+
*/
|
|
62
|
+
fetchMyBilling(req: {
|
|
63
|
+
headers?: {
|
|
64
|
+
cookie?: string;
|
|
65
|
+
};
|
|
66
|
+
}): Promise<BillingSnapshot | null>;
|
|
67
|
+
/**
|
|
68
|
+
* One-shot server-side auth snapshot: verifies the session, fetches the
|
|
69
|
+
* caller's billing, and returns a customisable object with a `has()`
|
|
70
|
+
* helper. Use inside route handlers / RSC / server actions:
|
|
71
|
+
*
|
|
72
|
+
* const { userId, has } = await af.auth(req);
|
|
73
|
+
* if (!has({ feature: 'pro_export' })) return forbidden();
|
|
74
|
+
*/
|
|
75
|
+
auth(req: {
|
|
76
|
+
headers?: {
|
|
77
|
+
cookie?: string;
|
|
78
|
+
};
|
|
79
|
+
}): Promise<{
|
|
80
|
+
userId: string | null;
|
|
81
|
+
sessionId: string | null;
|
|
82
|
+
orgId: string | null;
|
|
83
|
+
orgRole: string | null;
|
|
84
|
+
isSignedIn: boolean;
|
|
85
|
+
has: (check: HasCheck) => boolean;
|
|
86
|
+
}>;
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,EAAoB,KAAK,aAAa,EAAgC,MAAM,cAAc,CAAC;AAElG;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,6BAA6B,CAAC;AAE/D,MAAM,MAAM,4BAA4B,GAAG;IACzC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAUF,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC9D,QAAQ,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,qBAAa,qBAAqB;IAChC,OAAO,CAAC,IAAI,CAA+B;gBAE/B,IAAI,EAAE,4BAA4B;IAI9C,UAAU;IAIV,OAAO,KAAK,OAAO,GAElB;IAEK,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAS7D;;OAEG;IACG,qBAAqB,CAAC,GAAG,EAAE;QAAE,OAAO,CAAC,EAAE;YAAE,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAOlG;;;;OAIG;IACG,cAAc,CAClB,GAAG,EAAE;QAAE,OAAO,CAAC,EAAE;YAAE,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,GACrC,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IA+BlC;;;;;;;OAOG;IACG,IAAI,CAAC,GAAG,EAAE;QAAE,OAAO,CAAC,EAAE;YAAE,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,GAAG,OAAO,CAAC;QAC1D,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QACvB,UAAU,EAAE,OAAO,CAAC;QACpB,GAAG,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,OAAO,CAAC;KACnC,CAAC;CA2BH"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { getSessionJwtFromCookieHeader } from './cookies.js';
|
|
2
|
+
import { verifySessionJwt } from './session.js';
|
|
3
|
+
/**
|
|
4
|
+
* Canonical Authfyio API URL. Customers using the hosted SaaS shouldn't
|
|
5
|
+
* have to set anything — the default points at production. Override only
|
|
6
|
+
* when self-hosting or pointing at staging.
|
|
7
|
+
*/
|
|
8
|
+
export const DEFAULT_API_BASE_URL = 'https://api.authfyio.com';
|
|
9
|
+
function resolveBaseUrl(opts) {
|
|
10
|
+
return (opts.baseUrl ??
|
|
11
|
+
(typeof process !== 'undefined' ? process.env?.AF_API_BASE_URL : undefined) ??
|
|
12
|
+
DEFAULT_API_BASE_URL).replace(/\/+$/, '');
|
|
13
|
+
}
|
|
14
|
+
export class AuthfyioBackendClient {
|
|
15
|
+
opts;
|
|
16
|
+
constructor(opts) {
|
|
17
|
+
this.opts = opts;
|
|
18
|
+
}
|
|
19
|
+
getJwksUrl() {
|
|
20
|
+
return `${resolveBaseUrl(this.opts)}/.well-known/jwks.json`;
|
|
21
|
+
}
|
|
22
|
+
get baseUrl() {
|
|
23
|
+
return resolveBaseUrl(this.opts);
|
|
24
|
+
}
|
|
25
|
+
async verifySessionJwt(token) {
|
|
26
|
+
const verifyOpts = {
|
|
27
|
+
jwksUrl: this.getJwksUrl(),
|
|
28
|
+
issuer: this.opts.issuer,
|
|
29
|
+
audience: this.opts.audience,
|
|
30
|
+
};
|
|
31
|
+
return await verifySessionJwt(token, verifyOpts);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Extracts `__session` JWT from a Node/Express/Next request-like object and verifies it.
|
|
35
|
+
*/
|
|
36
|
+
async getSessionFromRequest(req) {
|
|
37
|
+
const cookieHeader = req?.headers?.cookie ?? null;
|
|
38
|
+
const token = getSessionJwtFromCookieHeader(cookieHeader);
|
|
39
|
+
if (!token)
|
|
40
|
+
return null;
|
|
41
|
+
return await this.verifySessionJwt(token);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Fetches the current user's active plan + feature list from the instance
|
|
45
|
+
* API. Backs server-side `auth().has({ plan, feature })` in route handlers.
|
|
46
|
+
* Forwards the caller's `__session` cookie so the API can identify them.
|
|
47
|
+
*/
|
|
48
|
+
async fetchMyBilling(req) {
|
|
49
|
+
const cookieHeader = req?.headers?.cookie ?? '';
|
|
50
|
+
const res = await fetch(`${this.baseUrl}/v1/billing/me`, {
|
|
51
|
+
method: 'GET',
|
|
52
|
+
headers: {
|
|
53
|
+
accept: 'application/json',
|
|
54
|
+
...(cookieHeader ? { cookie: cookieHeader } : {}),
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
if (!res.ok)
|
|
58
|
+
return null;
|
|
59
|
+
const body = (await res.json());
|
|
60
|
+
if (!body)
|
|
61
|
+
return null;
|
|
62
|
+
return {
|
|
63
|
+
plan: body.plan
|
|
64
|
+
? { id: String(body.plan.id), key: body.plan.key ?? null, name: String(body.plan.name ?? '') }
|
|
65
|
+
: null,
|
|
66
|
+
features: Array.isArray(body.features)
|
|
67
|
+
? body.features.map((f) => ({
|
|
68
|
+
key: String(f.key),
|
|
69
|
+
name: String(f.name ?? f.key),
|
|
70
|
+
description: f.description ?? undefined,
|
|
71
|
+
}))
|
|
72
|
+
: [],
|
|
73
|
+
status: body.status ?? null,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* One-shot server-side auth snapshot: verifies the session, fetches the
|
|
78
|
+
* caller's billing, and returns a customisable object with a `has()`
|
|
79
|
+
* helper. Use inside route handlers / RSC / server actions:
|
|
80
|
+
*
|
|
81
|
+
* const { userId, has } = await af.auth(req);
|
|
82
|
+
* if (!has({ feature: 'pro_export' })) return forbidden();
|
|
83
|
+
*/
|
|
84
|
+
async auth(req) {
|
|
85
|
+
const claims = await this.getSessionFromRequest(req);
|
|
86
|
+
if (!claims) {
|
|
87
|
+
return {
|
|
88
|
+
userId: null,
|
|
89
|
+
sessionId: null,
|
|
90
|
+
orgId: null,
|
|
91
|
+
orgRole: null,
|
|
92
|
+
isSignedIn: false,
|
|
93
|
+
has: () => false,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const billing = await this.fetchMyBilling(req);
|
|
97
|
+
return {
|
|
98
|
+
userId: claims.sub ?? null,
|
|
99
|
+
sessionId: claims.sid ?? null,
|
|
100
|
+
orgId: claims.org ?? null,
|
|
101
|
+
orgRole: claims.org_role ?? null,
|
|
102
|
+
isSignedIn: true,
|
|
103
|
+
has: (check) => {
|
|
104
|
+
if (check.role && claims.org_role !== check.role)
|
|
105
|
+
return false;
|
|
106
|
+
if (check.plan && billing?.plan?.key !== check.plan)
|
|
107
|
+
return false;
|
|
108
|
+
if (check.feature && !billing?.features.some((f) => f.key === check.feature))
|
|
109
|
+
return false;
|
|
110
|
+
return true;
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../src/cookies.ts"],"names":[],"mappings":"AAAA,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAYjG;AAED,wBAAgB,6BAA6B,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAGpG"}
|
package/dist/cookies.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function parseCookieHeader(cookieHeader) {
|
|
2
|
+
const out = {};
|
|
3
|
+
if (!cookieHeader)
|
|
4
|
+
return out;
|
|
5
|
+
for (const part of cookieHeader.split(';')) {
|
|
6
|
+
const idx = part.indexOf('=');
|
|
7
|
+
if (idx <= 0)
|
|
8
|
+
continue;
|
|
9
|
+
const k = part.slice(0, idx).trim();
|
|
10
|
+
const v = part.slice(idx + 1).trim();
|
|
11
|
+
if (!k)
|
|
12
|
+
continue;
|
|
13
|
+
out[k] = decodeURIComponent(v);
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
export function getSessionJwtFromCookieHeader(cookieHeader) {
|
|
18
|
+
const cookies = parseCookieHeader(cookieHeader);
|
|
19
|
+
return cookies.__session ?? null;
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookies.spec.d.ts","sourceRoot":"","sources":["../src/cookies.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { getSessionJwtFromCookieHeader, parseCookieHeader } from './cookies.js';
|
|
3
|
+
describe('parseCookieHeader', () => {
|
|
4
|
+
it('returns an empty object for null or empty headers', () => {
|
|
5
|
+
expect(parseCookieHeader(null)).toEqual({});
|
|
6
|
+
expect(parseCookieHeader(undefined)).toEqual({});
|
|
7
|
+
expect(parseCookieHeader('')).toEqual({});
|
|
8
|
+
});
|
|
9
|
+
it('parses a single cookie', () => {
|
|
10
|
+
expect(parseCookieHeader('foo=bar')).toEqual({ foo: 'bar' });
|
|
11
|
+
});
|
|
12
|
+
it('parses multiple cookies separated by "; "', () => {
|
|
13
|
+
expect(parseCookieHeader('a=1; b=2; c=3')).toEqual({ a: '1', b: '2', c: '3' });
|
|
14
|
+
});
|
|
15
|
+
it('url-decodes cookie values', () => {
|
|
16
|
+
expect(parseCookieHeader('ref=https%3A%2F%2Fexample.com')).toEqual({
|
|
17
|
+
ref: 'https://example.com',
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
it('ignores malformed parts without an equals sign', () => {
|
|
21
|
+
expect(parseCookieHeader('a=1; junk; b=2')).toEqual({ a: '1', b: '2' });
|
|
22
|
+
});
|
|
23
|
+
it('allows values that contain an "=" character', () => {
|
|
24
|
+
expect(parseCookieHeader('jwt=eyJhbGciOi.payload=.sig')).toEqual({
|
|
25
|
+
jwt: 'eyJhbGciOi.payload=.sig',
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
it('preserves whitespace around names and values', () => {
|
|
29
|
+
expect(parseCookieHeader(' a = 1 ; b= 2')).toEqual({ a: '1', b: '2' });
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe('getSessionJwtFromCookieHeader', () => {
|
|
33
|
+
it('returns the __session cookie when present', () => {
|
|
34
|
+
expect(getSessionJwtFromCookieHeader('__session=abc123')).toBe('abc123');
|
|
35
|
+
});
|
|
36
|
+
it('returns null when __session is missing', () => {
|
|
37
|
+
expect(getSessionJwtFromCookieHeader('__client=xyz')).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
it('returns null for empty input', () => {
|
|
40
|
+
expect(getSessionJwtFromCookieHeader(null)).toBeNull();
|
|
41
|
+
expect(getSessionJwtFromCookieHeader('')).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
it('prefers __session over __client when both exist', () => {
|
|
44
|
+
expect(getSessionJwtFromCookieHeader('__client=aaa; __session=bbb')).toBe('bbb');
|
|
45
|
+
});
|
|
46
|
+
});
|
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,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SessionClaims } from './session.js';
|
|
2
|
+
import { type AuthfyioBackendClientOptions } from './client.js';
|
|
3
|
+
export type RequireSessionResult<TSession = SessionClaims> = {
|
|
4
|
+
ok: true;
|
|
5
|
+
session: TSession;
|
|
6
|
+
} | {
|
|
7
|
+
ok: false;
|
|
8
|
+
status: 401;
|
|
9
|
+
reason: 'missing_session' | 'invalid_session';
|
|
10
|
+
};
|
|
11
|
+
export declare function requireSession(req: {
|
|
12
|
+
headers?: {
|
|
13
|
+
cookie?: string;
|
|
14
|
+
};
|
|
15
|
+
}, opts: AuthfyioBackendClientOptions): Promise<RequireSessionResult>;
|
|
16
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,OAAO,EAAyB,KAAK,4BAA4B,EAAE,MAAM,aAAa,CAAC;AAEvF,MAAM,MAAM,oBAAoB,CAAC,QAAQ,GAAG,aAAa,IACrD;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,GAC/B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,iBAAiB,GAAG,iBAAiB,CAAA;CAAE,CAAC;AAE9E,wBAAsB,cAAc,CAClC,GAAG,EAAE;IAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,EACtC,IAAI,EAAE,4BAA4B,GACjC,OAAO,CAAC,oBAAoB,CAAC,CAS/B"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AuthfyioBackendClient } from './client.js';
|
|
2
|
+
export async function requireSession(req, opts) {
|
|
3
|
+
const af = new AuthfyioBackendClient(opts);
|
|
4
|
+
try {
|
|
5
|
+
const session = await af.getSessionFromRequest(req);
|
|
6
|
+
if (!session)
|
|
7
|
+
return { ok: false, status: 401, reason: 'missing_session' };
|
|
8
|
+
return { ok: true, session };
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return { ok: false, status: 401, reason: 'invalid_session' };
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend SDK helpers for Authfyio's Stripe-Connect-mediated payments.
|
|
3
|
+
*
|
|
4
|
+
* The recommended flow is:
|
|
5
|
+
* 1. End-user's browser → developer's frontend → Authfyio /v1/payments/intents
|
|
6
|
+
* (cookie-authenticated). The browser calls Authfyio directly so the
|
|
7
|
+
* developer's backend never has to forward Stripe's client_secret.
|
|
8
|
+
*
|
|
9
|
+
* 2. The frontend confirms the PaymentIntent with Stripe.js using the
|
|
10
|
+
* developer's *connected* account context:
|
|
11
|
+
*
|
|
12
|
+
* const stripe = await loadStripe(YOUR_PLATFORM_PUBLISHABLE_KEY, {
|
|
13
|
+
* stripeAccount: response.publishableKeyAccountId,
|
|
14
|
+
* });
|
|
15
|
+
* await stripe.confirmCardPayment(response.clientSecret, …);
|
|
16
|
+
*
|
|
17
|
+
* 3. Authfyio webhooks update the payment row server-side. The developer's
|
|
18
|
+
* backend can then either subscribe to the Authfyio webhook ("payment.succeeded"
|
|
19
|
+
* via WebhookEndpointEntity) or poll `getPayment()` below.
|
|
20
|
+
*
|
|
21
|
+
* This module exposes a thin server-side helper for case (3) and for backends
|
|
22
|
+
* that need to issue refunds programmatically.
|
|
23
|
+
*/
|
|
24
|
+
export type PaymentStatus = 'requires_payment_method' | 'requires_confirmation' | 'requires_action' | 'processing' | 'requires_capture' | 'canceled' | 'succeeded' | 'refunded' | 'partially_refunded' | 'disputed';
|
|
25
|
+
export type PaymentRow = {
|
|
26
|
+
id: string;
|
|
27
|
+
environmentId: string;
|
|
28
|
+
stripeAccountId: string;
|
|
29
|
+
stripePaymentIntentId: string;
|
|
30
|
+
stripeChargeId: string | null;
|
|
31
|
+
amountCents: number;
|
|
32
|
+
applicationFeeCents: number;
|
|
33
|
+
refundedAmountCents: number;
|
|
34
|
+
refundedFeeCents: number;
|
|
35
|
+
currency: string;
|
|
36
|
+
customerEmail: string | null;
|
|
37
|
+
description: string | null;
|
|
38
|
+
status: PaymentStatus;
|
|
39
|
+
feeBpsApplied: number;
|
|
40
|
+
metadata: Record<string, unknown>;
|
|
41
|
+
createdAt: string;
|
|
42
|
+
updatedAt: string;
|
|
43
|
+
};
|
|
44
|
+
export type CreatePaymentIntentOptions = {
|
|
45
|
+
/** Smallest currency unit (cents/kuruş/etc). Must be a positive integer. */
|
|
46
|
+
amountCents: number;
|
|
47
|
+
/** ISO three-letter currency code (e.g. 'usd', 'eur', 'try'). */
|
|
48
|
+
currency: string;
|
|
49
|
+
description?: string;
|
|
50
|
+
customerEmail?: string;
|
|
51
|
+
metadata?: Record<string, string>;
|
|
52
|
+
/**
|
|
53
|
+
* Per-charge override of the platform fee in basis points. If omitted, the
|
|
54
|
+
* Connect-account-level default applies (0.6 %).
|
|
55
|
+
*/
|
|
56
|
+
feeBpsOverride?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Idempotency key passed to Stripe. Use the same key on retries so we never
|
|
59
|
+
* double-charge.
|
|
60
|
+
*/
|
|
61
|
+
idempotencyKey?: string;
|
|
62
|
+
};
|
|
63
|
+
export type CreatePaymentIntentResult = {
|
|
64
|
+
paymentId: string;
|
|
65
|
+
intentId: string;
|
|
66
|
+
clientSecret: string | null;
|
|
67
|
+
publishableKeyAccountId: string;
|
|
68
|
+
amountCents: number;
|
|
69
|
+
applicationFeeCents: number;
|
|
70
|
+
feeBpsApplied: number;
|
|
71
|
+
currency: string;
|
|
72
|
+
status: PaymentStatus;
|
|
73
|
+
};
|
|
74
|
+
export type RefundOptions = {
|
|
75
|
+
amountCents?: number;
|
|
76
|
+
reason?: 'duplicate' | 'fraudulent' | 'requested_by_customer';
|
|
77
|
+
};
|
|
78
|
+
export type PaymentsClientOptions = {
|
|
79
|
+
/** Base URL of the Authfyio instance API, e.g. "http://localhost:3001". */
|
|
80
|
+
baseUrl: string;
|
|
81
|
+
/**
|
|
82
|
+
* Cookie header to forward (the end user's `__session` cookie). This is what
|
|
83
|
+
* authenticates the request — the same flow as the rest of the FAPI.
|
|
84
|
+
*/
|
|
85
|
+
cookieHeader: string;
|
|
86
|
+
/** Optional fetch implementation override (Node 18+ has global fetch). */
|
|
87
|
+
fetchImpl?: typeof fetch;
|
|
88
|
+
};
|
|
89
|
+
export declare class PaymentsClient {
|
|
90
|
+
private baseUrl;
|
|
91
|
+
private cookieHeader;
|
|
92
|
+
private fetchImpl;
|
|
93
|
+
constructor(opts: PaymentsClientOptions);
|
|
94
|
+
/**
|
|
95
|
+
* Create a payment intent on the developer's connected Stripe account.
|
|
96
|
+
* Returns the `clientSecret` to hand to Stripe.js.
|
|
97
|
+
*/
|
|
98
|
+
createIntent(opts: CreatePaymentIntentOptions): Promise<CreatePaymentIntentResult>;
|
|
99
|
+
/** Refund a payment, in full or in part. */
|
|
100
|
+
refund(paymentId: string, opts?: RefundOptions): Promise<{
|
|
101
|
+
refundId: string;
|
|
102
|
+
status: PaymentStatus;
|
|
103
|
+
refundedAmountCents: number;
|
|
104
|
+
}>;
|
|
105
|
+
/** Page through historical payments for the current environment. */
|
|
106
|
+
list(opts?: {
|
|
107
|
+
limit?: number;
|
|
108
|
+
cursor?: string | null;
|
|
109
|
+
}): Promise<{
|
|
110
|
+
items: PaymentRow[];
|
|
111
|
+
nextCursor: string | null;
|
|
112
|
+
}>;
|
|
113
|
+
private request;
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=payments.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payments.d.ts","sourceRoot":"","sources":["../src/payments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,MAAM,MAAM,aAAa,GACrB,yBAAyB,GACzB,uBAAuB,GACvB,iBAAiB,GACjB,YAAY,GACZ,kBAAkB,GAClB,UAAU,GACV,WAAW,GACX,UAAU,GACV,oBAAoB,GACpB,UAAU,CAAC;AAEf,MAAM,MAAM,UAAU,GAAG;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,aAAa,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,4EAA4E;IAC5E,WAAW,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,uBAAuB,EAAE,MAAM,CAAC;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,aAAa,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,WAAW,GAAG,YAAY,GAAG,uBAAuB,CAAC;CAC/D,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,2EAA2E;IAC3E,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B,CAAC;AAEF,qBAAa,cAAc;IACzB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,SAAS,CAAe;gBAEpB,IAAI,EAAE,qBAAqB;IAQvC;;;OAGG;IACG,YAAY,CAAC,IAAI,EAAE,0BAA0B,GAAG,OAAO,CAAC,yBAAyB,CAAC;IAIxF,4CAA4C;IACtC,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC;QACjE,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,aAAa,CAAC;QACtB,mBAAmB,EAAE,MAAM,CAAC;KAC7B,CAAC;IAIF,oEAAoE;IAC9D,IAAI,CAAC,IAAI,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAO,GAAG,OAAO,CAAC;QACzE,KAAK,EAAE,UAAU,EAAE,CAAC;QACpB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;KAC3B,CAAC;YAQY,OAAO;CAwBtB"}
|
package/dist/payments.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend SDK helpers for Authfyio's Stripe-Connect-mediated payments.
|
|
3
|
+
*
|
|
4
|
+
* The recommended flow is:
|
|
5
|
+
* 1. End-user's browser → developer's frontend → Authfyio /v1/payments/intents
|
|
6
|
+
* (cookie-authenticated). The browser calls Authfyio directly so the
|
|
7
|
+
* developer's backend never has to forward Stripe's client_secret.
|
|
8
|
+
*
|
|
9
|
+
* 2. The frontend confirms the PaymentIntent with Stripe.js using the
|
|
10
|
+
* developer's *connected* account context:
|
|
11
|
+
*
|
|
12
|
+
* const stripe = await loadStripe(YOUR_PLATFORM_PUBLISHABLE_KEY, {
|
|
13
|
+
* stripeAccount: response.publishableKeyAccountId,
|
|
14
|
+
* });
|
|
15
|
+
* await stripe.confirmCardPayment(response.clientSecret, …);
|
|
16
|
+
*
|
|
17
|
+
* 3. Authfyio webhooks update the payment row server-side. The developer's
|
|
18
|
+
* backend can then either subscribe to the Authfyio webhook ("payment.succeeded"
|
|
19
|
+
* via WebhookEndpointEntity) or poll `getPayment()` below.
|
|
20
|
+
*
|
|
21
|
+
* This module exposes a thin server-side helper for case (3) and for backends
|
|
22
|
+
* that need to issue refunds programmatically.
|
|
23
|
+
*/
|
|
24
|
+
export class PaymentsClient {
|
|
25
|
+
baseUrl;
|
|
26
|
+
cookieHeader;
|
|
27
|
+
fetchImpl;
|
|
28
|
+
constructor(opts) {
|
|
29
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, '');
|
|
30
|
+
this.cookieHeader = opts.cookieHeader;
|
|
31
|
+
this.fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
32
|
+
if (!this.fetchImpl)
|
|
33
|
+
throw new Error('PaymentsClient: no global fetch and no fetchImpl supplied');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create a payment intent on the developer's connected Stripe account.
|
|
37
|
+
* Returns the `clientSecret` to hand to Stripe.js.
|
|
38
|
+
*/
|
|
39
|
+
async createIntent(opts) {
|
|
40
|
+
return this.request('POST', '/v1/payments/intents', opts);
|
|
41
|
+
}
|
|
42
|
+
/** Refund a payment, in full or in part. */
|
|
43
|
+
async refund(paymentId, opts = {}) {
|
|
44
|
+
return this.request('POST', `/v1/payments/${encodeURIComponent(paymentId)}/refund`, opts);
|
|
45
|
+
}
|
|
46
|
+
/** Page through historical payments for the current environment. */
|
|
47
|
+
async list(opts = {}) {
|
|
48
|
+
const qs = new URLSearchParams();
|
|
49
|
+
if (opts.limit)
|
|
50
|
+
qs.set('limit', String(opts.limit));
|
|
51
|
+
if (opts.cursor)
|
|
52
|
+
qs.set('cursor', opts.cursor);
|
|
53
|
+
const path = `/v1/payments/list${qs.size ? `?${qs.toString()}` : ''}`;
|
|
54
|
+
return this.request('GET', path);
|
|
55
|
+
}
|
|
56
|
+
async request(method, path, body) {
|
|
57
|
+
const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
58
|
+
method,
|
|
59
|
+
headers: {
|
|
60
|
+
...(body !== undefined ? { 'content-type': 'application/json' } : {}),
|
|
61
|
+
cookie: this.cookieHeader,
|
|
62
|
+
},
|
|
63
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
64
|
+
});
|
|
65
|
+
const text = await res.text();
|
|
66
|
+
let parsed = null;
|
|
67
|
+
if (text) {
|
|
68
|
+
try {
|
|
69
|
+
parsed = JSON.parse(text);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Non-JSON error body — propagate raw text below.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
const msg = parsed?.message ?? parsed?.error ?? text ?? `HTTP ${res.status}`;
|
|
77
|
+
throw new Error(`Authfyio payments error (${res.status}): ${msg}`);
|
|
78
|
+
}
|
|
79
|
+
return parsed;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type SessionClaims = {
|
|
2
|
+
sid: string;
|
|
3
|
+
sub: string;
|
|
4
|
+
env: string;
|
|
5
|
+
org?: string;
|
|
6
|
+
org_role?: string;
|
|
7
|
+
iat?: number;
|
|
8
|
+
exp?: number;
|
|
9
|
+
iss?: string;
|
|
10
|
+
aud?: string | string[];
|
|
11
|
+
};
|
|
12
|
+
export declare const DEFAULT_JWKS_URL = "https://api.authfyio.com/.well-known/jwks.json";
|
|
13
|
+
export type VerifySessionJwtOptions = {
|
|
14
|
+
/**
|
|
15
|
+
* JWKS endpoint to verify the JWT against. Defaults to
|
|
16
|
+
* `https://api.authfyio.com/.well-known/jwks.json` (the hosted SaaS).
|
|
17
|
+
* Override only when self-hosting.
|
|
18
|
+
*/
|
|
19
|
+
jwksUrl?: string;
|
|
20
|
+
issuer?: string;
|
|
21
|
+
audience?: string;
|
|
22
|
+
clockToleranceSeconds?: number;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Verifies `__session` JWT using the instance JWKS endpoint.
|
|
26
|
+
* Intended for backend apps integrating Authfyio.
|
|
27
|
+
*/
|
|
28
|
+
export declare function verifySessionJwt(token: string, opts?: VerifySessionJwtOptions): Promise<SessionClaims>;
|
|
29
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CACzB,CAAC;AAEF,eAAO,MAAM,gBAAgB,mDAAmD,CAAC;AAEjF,MAAM,MAAM,uBAAuB,GAAG;IACpC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE,uBAA4B,GAAG,OAAO,CAAC,aAAa,CAAC,CAQhH"}
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
2
|
+
export const DEFAULT_JWKS_URL = 'https://api.authfyio.com/.well-known/jwks.json';
|
|
3
|
+
/**
|
|
4
|
+
* Verifies `__session` JWT using the instance JWKS endpoint.
|
|
5
|
+
* Intended for backend apps integrating Authfyio.
|
|
6
|
+
*/
|
|
7
|
+
export async function verifySessionJwt(token, opts = {}) {
|
|
8
|
+
const jwks = createRemoteJWKSet(new URL(opts.jwksUrl ?? DEFAULT_JWKS_URL));
|
|
9
|
+
const { payload } = await jwtVerify(token, jwks, {
|
|
10
|
+
issuer: opts.issuer,
|
|
11
|
+
audience: opts.audience,
|
|
12
|
+
clockTolerance: opts.clockToleranceSeconds,
|
|
13
|
+
});
|
|
14
|
+
return payload;
|
|
15
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kolay webhook delivery signs each outbound request with:
|
|
3
|
+
* signature = base64url(HMAC_SHA256(secret, `${timestamp}.${rawBody}`))
|
|
4
|
+
*
|
|
5
|
+
* and sends it alongside:
|
|
6
|
+
* x-af-webhook-signature: <sig>
|
|
7
|
+
* x-af-webhook-timestamp: <unix seconds>
|
|
8
|
+
*
|
|
9
|
+
* This helper recomputes the signature and compares in constant time. It
|
|
10
|
+
* also rejects timestamps outside a configurable tolerance window so an
|
|
11
|
+
* attacker can't replay old payloads even if they captured one valid
|
|
12
|
+
* (body, signature) pair.
|
|
13
|
+
*/
|
|
14
|
+
export type VerifyWebhookSignatureOptions = {
|
|
15
|
+
secret: string;
|
|
16
|
+
body: string;
|
|
17
|
+
signature: string;
|
|
18
|
+
timestamp: string | number;
|
|
19
|
+
/** Max age (seconds) the timestamp may have before we reject. Default 300. */
|
|
20
|
+
toleranceSeconds?: number;
|
|
21
|
+
/** Override "now" (unix seconds) for deterministic tests. */
|
|
22
|
+
nowSeconds?: number;
|
|
23
|
+
};
|
|
24
|
+
export type VerifyWebhookSignatureResult = {
|
|
25
|
+
valid: true;
|
|
26
|
+
} | {
|
|
27
|
+
valid: false;
|
|
28
|
+
reason: 'timestamp_out_of_range' | 'invalid_signature' | 'malformed_timestamp';
|
|
29
|
+
};
|
|
30
|
+
export declare function verifyWebhookSignature(opts: VerifyWebhookSignatureOptions): VerifyWebhookSignatureResult;
|
|
31
|
+
/**
|
|
32
|
+
* Convenience wrapper — reads the signature/timestamp headers from a plain
|
|
33
|
+
* object (express `req.headers`, Next's request headers, etc.) and verifies
|
|
34
|
+
* against the provided raw body. Throws an error when invalid so the caller
|
|
35
|
+
* can map it to a 401/400 response directly.
|
|
36
|
+
*/
|
|
37
|
+
export declare function assertWebhookSignature(req: {
|
|
38
|
+
headers: Record<string, string | string[] | undefined>;
|
|
39
|
+
rawBody: string;
|
|
40
|
+
secret: string;
|
|
41
|
+
toleranceSeconds?: number;
|
|
42
|
+
}): void;
|
|
43
|
+
//# sourceMappingURL=webhooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AAEH,MAAM,MAAM,6BAA6B,GAAG;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,8EAA8E;IAC9E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6DAA6D;IAC7D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GACpC;IAAE,KAAK,EAAE,IAAI,CAAA;CAAE,GACf;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,wBAAwB,GAAG,mBAAmB,GAAG,qBAAqB,CAAA;CAAE,CAAC;AAErG,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,6BAA6B,GAClC,4BAA4B,CAsB9B;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE;IAC1C,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,GAAG,IAAI,CAYP"}
|
package/dist/webhooks.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
export function verifyWebhookSignature(opts) {
|
|
3
|
+
const tsNum = typeof opts.timestamp === 'number' ? opts.timestamp : Number(opts.timestamp);
|
|
4
|
+
if (!Number.isFinite(tsNum))
|
|
5
|
+
return { valid: false, reason: 'malformed_timestamp' };
|
|
6
|
+
const tolerance = opts.toleranceSeconds ?? 300;
|
|
7
|
+
const now = opts.nowSeconds ?? Math.floor(Date.now() / 1000);
|
|
8
|
+
if (Math.abs(now - tsNum) > tolerance) {
|
|
9
|
+
return { valid: false, reason: 'timestamp_out_of_range' };
|
|
10
|
+
}
|
|
11
|
+
const expected = createHmac('sha256', opts.secret)
|
|
12
|
+
.update(`${tsNum}.${opts.body}`)
|
|
13
|
+
.digest();
|
|
14
|
+
let provided;
|
|
15
|
+
try {
|
|
16
|
+
provided = Buffer.from(opts.signature, 'base64url');
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return { valid: false, reason: 'invalid_signature' };
|
|
20
|
+
}
|
|
21
|
+
if (provided.length !== expected.length)
|
|
22
|
+
return { valid: false, reason: 'invalid_signature' };
|
|
23
|
+
const ok = timingSafeEqual(expected, provided);
|
|
24
|
+
return ok ? { valid: true } : { valid: false, reason: 'invalid_signature' };
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Convenience wrapper — reads the signature/timestamp headers from a plain
|
|
28
|
+
* object (express `req.headers`, Next's request headers, etc.) and verifies
|
|
29
|
+
* against the provided raw body. Throws an error when invalid so the caller
|
|
30
|
+
* can map it to a 401/400 response directly.
|
|
31
|
+
*/
|
|
32
|
+
export function assertWebhookSignature(req) {
|
|
33
|
+
const sig = pickHeader(req.headers, 'x-af-webhook-signature');
|
|
34
|
+
const ts = pickHeader(req.headers, 'x-af-webhook-timestamp');
|
|
35
|
+
if (!sig || !ts)
|
|
36
|
+
throw new Error('missing_webhook_signature_headers');
|
|
37
|
+
const result = verifyWebhookSignature({
|
|
38
|
+
secret: req.secret,
|
|
39
|
+
body: req.rawBody,
|
|
40
|
+
signature: sig,
|
|
41
|
+
timestamp: ts,
|
|
42
|
+
toleranceSeconds: req.toleranceSeconds,
|
|
43
|
+
});
|
|
44
|
+
if (!result.valid)
|
|
45
|
+
throw new Error(`invalid_webhook_signature:${result.reason}`);
|
|
46
|
+
}
|
|
47
|
+
function pickHeader(headers, key) {
|
|
48
|
+
const v = headers[key] ?? headers[key.toLowerCase()];
|
|
49
|
+
if (Array.isArray(v))
|
|
50
|
+
return v[0] ?? null;
|
|
51
|
+
return v ?? null;
|
|
52
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "authfyio-backend",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Framework-agnostic server-side SDK for Authfyio: session JWT verify, cookie helpers, webhook signature verification, and Stripe payments client.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.build.json",
|
|
21
|
+
"typecheck": "tsc -p tsconfig.build.json --noEmit",
|
|
22
|
+
"test": "vitest run --config ../../vitest.config.ts --dir src",
|
|
23
|
+
"test:watch": "vitest --config ../../vitest.config.ts --dir src",
|
|
24
|
+
"prepublishOnly": "npm run build"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"jose": "^5.9.6"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"authfyio",
|
|
31
|
+
"auth",
|
|
32
|
+
"authentication",
|
|
33
|
+
"jwt",
|
|
34
|
+
"webhooks",
|
|
35
|
+
"sdk"
|
|
36
|
+
],
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://authfyio.com/docs",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/authfyio/authfyio.git"
|
|
44
|
+
}
|
|
45
|
+
}
|