@theholocron/holocron-plugin-clerk 2.0.0-alpha.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 +48 -0
- package/dist/index.d.mts +92 -0
- package/dist/index.mjs +254 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Newton Koumantzelis
|
|
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,48 @@
|
|
|
1
|
+
# `@theholocron/holocron-plugin-clerk`
|
|
2
|
+
|
|
3
|
+
Clerk plugin for [Holocron](../cli). Implements the `auth` capability
|
|
4
|
+
against [Clerk's Backend REST API](https://clerk.com/docs/reference/backend-api).
|
|
5
|
+
|
|
6
|
+
## Auth
|
|
7
|
+
|
|
8
|
+
Token resolution order:
|
|
9
|
+
|
|
10
|
+
1. `--token <KEY>` flag on the holocron invocation
|
|
11
|
+
2. `HOLOCRON_CLERK_SECRET_KEY` env var
|
|
12
|
+
3. `CLERK_SECRET_KEY` env var (Clerk's own default; what their docs reference)
|
|
13
|
+
|
|
14
|
+
> **Why not the `clerk` CLI?** Rando's adapter shells out to `npx clerk@latest
|
|
15
|
+
> api …`, but the `clerk` CLI just wraps the same REST API holocron talks to.
|
|
16
|
+
> Direct REST drops a system-binary dependency and matches the uniform auth/REST
|
|
17
|
+
> pattern across the other holocron plugins.
|
|
18
|
+
|
|
19
|
+
## Config
|
|
20
|
+
|
|
21
|
+
```jsonc
|
|
22
|
+
{
|
|
23
|
+
"providers": {
|
|
24
|
+
"auth": "clerk"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
No plugin-level options today. Per-instance scoping (Development vs.
|
|
30
|
+
Production) is driven by which secret key the env var holds — `sk_test_*`
|
|
31
|
+
for Development, `sk_live_*` for Production.
|
|
32
|
+
|
|
33
|
+
## What's implemented
|
|
34
|
+
|
|
35
|
+
| Method | What it does |
|
|
36
|
+
| ------------------------- | ----------------------------------------------------------------------------- |
|
|
37
|
+
| `describe` | Declares `CLERK_PUBLISHABLE_KEY` + `CLERK_SECRET_KEY` as runtime envs |
|
|
38
|
+
| `whoami` | `GET /users/count` reachability probe (returns user count) |
|
|
39
|
+
| `ensureWebhookApp` | `POST /webhooks/svix` — idempotent; already-exists → `{alreadyExists:true}` |
|
|
40
|
+
| `getWebhookDashboardUrl` | `POST /webhooks/svix_url` — deep-link to Svix dashboard |
|
|
41
|
+
| `createUser` | `POST /users` — seeds users (test fixtures, admin bootstrap) |
|
|
42
|
+
|
|
43
|
+
## Status
|
|
44
|
+
|
|
45
|
+
**v0.0.0 — first port.** Capability matches Rando's
|
|
46
|
+
`adapters/clerk-cli.ts` surface plus a holocron-native `describe()`,
|
|
47
|
+
all via direct REST against `api.clerk.com/v1`. No `clerk` CLI binary
|
|
48
|
+
required on the operator's machine.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Auth, AuthDescription, AuthEvent, AuthIdentity, AuthUser, CreateAuthUserInput, ParseWebhookInput, WebhookDashboardInfo } from "@theholocron/cli";
|
|
2
|
+
|
|
3
|
+
//#region src/auth.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Token resolution for the Clerk plugin.
|
|
6
|
+
*
|
|
7
|
+
* Resolution order:
|
|
8
|
+
* 1. explicit `cliToken` argument (from `--token` flag)
|
|
9
|
+
* 2. HOLOCRON_CLERK_SECRET_KEY env var (preferred — explicit intent)
|
|
10
|
+
* 3. CLERK_SECRET_KEY env var (the default Clerk's docs reference)
|
|
11
|
+
*
|
|
12
|
+
* The key (sk_test_* / sk_live_*) determines which Clerk instance —
|
|
13
|
+
* Development or Production — every call hits.
|
|
14
|
+
*/
|
|
15
|
+
declare class AuthError extends Error {
|
|
16
|
+
name: string;
|
|
17
|
+
}
|
|
18
|
+
interface ResolveTokenInput {
|
|
19
|
+
/** From `--token` CLI flag. */
|
|
20
|
+
cliToken?: string;
|
|
21
|
+
/** Env vars; passed in for testability. Defaults to `process.env`. */
|
|
22
|
+
env?: NodeJS.ProcessEnv;
|
|
23
|
+
}
|
|
24
|
+
declare function resolveToken(input?: ResolveTokenInput): string;
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/rest.d.ts
|
|
27
|
+
/**
|
|
28
|
+
* Thin REST wrapper around api.clerk.com/v1.
|
|
29
|
+
*
|
|
30
|
+
* Same pattern as the github/vercel/neon REST clients — bearer auth,
|
|
31
|
+
* JSON-only bodies, transport-failure wrapping with `status: 0` so the
|
|
32
|
+
* orchestrator's soft-skip path sees a clear "Clerk GET /path failed"
|
|
33
|
+
* message instead of a generic `TypeError: fetch failed`.
|
|
34
|
+
*/
|
|
35
|
+
interface RestClientOptions {
|
|
36
|
+
token: string;
|
|
37
|
+
fetch?: typeof fetch;
|
|
38
|
+
baseUrl?: string;
|
|
39
|
+
}
|
|
40
|
+
interface RequestOptions {
|
|
41
|
+
method?: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
|
42
|
+
body?: unknown;
|
|
43
|
+
query?: Record<string, string>;
|
|
44
|
+
}
|
|
45
|
+
declare class ClerkRestClient {
|
|
46
|
+
private readonly token;
|
|
47
|
+
private readonly fetchImpl;
|
|
48
|
+
readonly baseUrl: string;
|
|
49
|
+
constructor(opts: RestClientOptions);
|
|
50
|
+
request<T>(path: string, opts?: RequestOptions): Promise<T>;
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/capabilities/auth.d.ts
|
|
54
|
+
type ClerkAuthOptions = Record<string, never>;
|
|
55
|
+
declare class ClerkAuth implements Auth {
|
|
56
|
+
private readonly rest;
|
|
57
|
+
readonly key: "auth";
|
|
58
|
+
readonly providerName = "clerk";
|
|
59
|
+
constructor(rest: ClerkRestClient, _opts?: ClerkAuthOptions);
|
|
60
|
+
describe(): Promise<AuthDescription>;
|
|
61
|
+
whoami(): Promise<AuthIdentity>;
|
|
62
|
+
ensureWebhookApp(): Promise<{
|
|
63
|
+
alreadyExists: boolean;
|
|
64
|
+
}>;
|
|
65
|
+
getWebhookDashboardUrl(): Promise<WebhookDashboardInfo>;
|
|
66
|
+
createUser(input: CreateAuthUserInput): Promise<AuthUser>;
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/parse-webhook.d.ts
|
|
70
|
+
declare function parseWebhook(input: ParseWebhookInput): Promise<AuthEvent>;
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/index.d.ts
|
|
73
|
+
interface ClerkPluginOptions extends ResolveTokenInput {
|
|
74
|
+
/** Override base URL for tests. */
|
|
75
|
+
baseUrl?: string;
|
|
76
|
+
/** Override `fetch` for tests. */
|
|
77
|
+
fetch?: typeof fetch;
|
|
78
|
+
}
|
|
79
|
+
interface PluginContext {
|
|
80
|
+
options: ClerkPluginOptions;
|
|
81
|
+
rest: ClerkRestClient;
|
|
82
|
+
}
|
|
83
|
+
declare function createContext(options?: ClerkPluginOptions): PluginContext;
|
|
84
|
+
declare function auth(ctx: PluginContext): Auth;
|
|
85
|
+
declare function createPlugin(options?: ClerkPluginOptions): {
|
|
86
|
+
name: string;
|
|
87
|
+
capabilities: {
|
|
88
|
+
auth: () => Auth;
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
//#endregion
|
|
92
|
+
export { AuthError, ClerkAuth, ClerkPluginOptions, ClerkRestClient, PluginContext, ResolveTokenInput, auth, createContext, createPlugin, parseWebhook, resolveToken };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { ProviderApiError, WebhookVerificationError } from "@theholocron/cli";
|
|
2
|
+
//#region src/auth.ts
|
|
3
|
+
/**
|
|
4
|
+
* Token resolution for the Clerk plugin.
|
|
5
|
+
*
|
|
6
|
+
* Resolution order:
|
|
7
|
+
* 1. explicit `cliToken` argument (from `--token` flag)
|
|
8
|
+
* 2. HOLOCRON_CLERK_SECRET_KEY env var (preferred — explicit intent)
|
|
9
|
+
* 3. CLERK_SECRET_KEY env var (the default Clerk's docs reference)
|
|
10
|
+
*
|
|
11
|
+
* The key (sk_test_* / sk_live_*) determines which Clerk instance —
|
|
12
|
+
* Development or Production — every call hits.
|
|
13
|
+
*/
|
|
14
|
+
var AuthError = class extends Error {
|
|
15
|
+
name = "AuthError";
|
|
16
|
+
};
|
|
17
|
+
function resolveToken(input = {}) {
|
|
18
|
+
const env = input.env ?? process.env;
|
|
19
|
+
const token = input.cliToken || env.HOLOCRON_CLERK_SECRET_KEY || env.CLERK_SECRET_KEY;
|
|
20
|
+
if (!token) throw new AuthError("no Clerk secret key found. Pass --token <KEY>, or set HOLOCRON_CLERK_SECRET_KEY / CLERK_SECRET_KEY.");
|
|
21
|
+
return token;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/capabilities/auth.ts
|
|
25
|
+
/**
|
|
26
|
+
* `auth` capability for Clerk.
|
|
27
|
+
*
|
|
28
|
+
* Ported from rando-id/rando.id `adapters/clerk-cli.ts`, swapped from
|
|
29
|
+
* shell-out (`npx clerk@latest api ...`) to direct REST against
|
|
30
|
+
* api.clerk.com/v1.
|
|
31
|
+
*
|
|
32
|
+
* Surface mirrors the Rando shape:
|
|
33
|
+
*
|
|
34
|
+
* - `whoami` — `GET /users/count`. Cheap reachability probe; the user
|
|
35
|
+
* count doubles as a sanity signal for the operator.
|
|
36
|
+
* - `ensureWebhookApp` — `POST /webhooks/svix`. Idempotent — if a Svix
|
|
37
|
+
* app already exists, the API returns an error containing "already";
|
|
38
|
+
* we suppress it and return `{ alreadyExists: true }`.
|
|
39
|
+
* - `getWebhookDashboardUrl` — `POST /webhooks/svix_url`. Returns a
|
|
40
|
+
* one-time admin login URL to Svix's dashboard so the operator can
|
|
41
|
+
* finish endpoint config there.
|
|
42
|
+
* - `createUser` — `POST /users`. Used for seeding test users into
|
|
43
|
+
* staging without touching the dashboard.
|
|
44
|
+
* - `describe` — declares the runtime env keys a Clerk-using app needs.
|
|
45
|
+
*
|
|
46
|
+
* The configured secret key (`sk_test_*` vs `sk_live_*`) decides which
|
|
47
|
+
* Clerk instance (Development vs Production) every call hits — there's
|
|
48
|
+
* no per-call instance switch.
|
|
49
|
+
*/
|
|
50
|
+
var ClerkAuth = class {
|
|
51
|
+
rest;
|
|
52
|
+
key = "auth";
|
|
53
|
+
providerName = "clerk";
|
|
54
|
+
constructor(rest, _opts = {}) {
|
|
55
|
+
this.rest = rest;
|
|
56
|
+
}
|
|
57
|
+
async describe() {
|
|
58
|
+
return {
|
|
59
|
+
provider: "clerk",
|
|
60
|
+
envKeys: ["CLERK_PUBLISHABLE_KEY", "CLERK_SECRET_KEY"]
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async whoami() {
|
|
64
|
+
return {
|
|
65
|
+
provider: "clerk",
|
|
66
|
+
details: { userCount: (await this.rest.request("/users/count")).total_count }
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async ensureWebhookApp() {
|
|
70
|
+
try {
|
|
71
|
+
await this.rest.request("/webhooks/svix", { method: "POST" });
|
|
72
|
+
return { alreadyExists: false };
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (err instanceof ProviderApiError && isAlreadyExistsError(err)) return { alreadyExists: true };
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async getWebhookDashboardUrl() {
|
|
79
|
+
const body = await this.rest.request("/webhooks/svix_url", { method: "POST" });
|
|
80
|
+
const url = body.url ?? body.svix_url;
|
|
81
|
+
if (!url) throw new ProviderApiError("Clerk POST /webhooks/svix_url returned 200 but no `url` field", 500, void 0);
|
|
82
|
+
return { url };
|
|
83
|
+
}
|
|
84
|
+
async createUser(input) {
|
|
85
|
+
const body = await this.rest.request("/users", {
|
|
86
|
+
method: "POST",
|
|
87
|
+
body: {
|
|
88
|
+
email_address: [input.email],
|
|
89
|
+
password: input.password,
|
|
90
|
+
...input.firstName ? { first_name: input.firstName } : {},
|
|
91
|
+
...input.lastName ? { last_name: input.lastName } : {}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
const email = body.email_addresses[0]?.email_address ?? input.email;
|
|
95
|
+
return {
|
|
96
|
+
id: body.id,
|
|
97
|
+
email
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Clerk returns a 4xx with `errors[0].code` of `you_already_have_a_svix_app`
|
|
103
|
+
* (or text containing "already exists") when the Svix app already exists.
|
|
104
|
+
* Treat that as a successful no-op so `ensureWebhookApp` is idempotent.
|
|
105
|
+
*/
|
|
106
|
+
function isAlreadyExistsError(err) {
|
|
107
|
+
if (typeof err.details === "string") {
|
|
108
|
+
const lower = err.details.toLowerCase();
|
|
109
|
+
return lower.includes("already") || lower.includes("exists");
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/rest.ts
|
|
115
|
+
/**
|
|
116
|
+
* Thin REST wrapper around api.clerk.com/v1.
|
|
117
|
+
*
|
|
118
|
+
* Same pattern as the github/vercel/neon REST clients — bearer auth,
|
|
119
|
+
* JSON-only bodies, transport-failure wrapping with `status: 0` so the
|
|
120
|
+
* orchestrator's soft-skip path sees a clear "Clerk GET /path failed"
|
|
121
|
+
* message instead of a generic `TypeError: fetch failed`.
|
|
122
|
+
*/
|
|
123
|
+
var ClerkRestClient = class {
|
|
124
|
+
token;
|
|
125
|
+
fetchImpl;
|
|
126
|
+
baseUrl;
|
|
127
|
+
constructor(opts) {
|
|
128
|
+
this.token = opts.token;
|
|
129
|
+
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
130
|
+
this.baseUrl = (opts.baseUrl ?? "https://api.clerk.com/v1").replace(/\/+$/, "");
|
|
131
|
+
}
|
|
132
|
+
async request(path, opts = {}) {
|
|
133
|
+
const url = new URL(`${this.baseUrl}${path.startsWith("/") ? path : "/" + path}`);
|
|
134
|
+
for (const [k, v] of Object.entries(opts.query ?? {})) url.searchParams.set(k, v);
|
|
135
|
+
const fullUrl = url.toString();
|
|
136
|
+
const headers = {
|
|
137
|
+
authorization: `Bearer ${this.token}`,
|
|
138
|
+
accept: "application/json"
|
|
139
|
+
};
|
|
140
|
+
const init = {
|
|
141
|
+
method: opts.method ?? "GET",
|
|
142
|
+
headers
|
|
143
|
+
};
|
|
144
|
+
if (opts.body !== void 0) {
|
|
145
|
+
headers["content-type"] = "application/json";
|
|
146
|
+
init.body = JSON.stringify(opts.body);
|
|
147
|
+
}
|
|
148
|
+
let res;
|
|
149
|
+
try {
|
|
150
|
+
res = await this.fetchImpl(fullUrl, init);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
const detail = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
153
|
+
throw new ProviderApiError(`Clerk ${init.method} ${path} failed: ${detail}`, 0, void 0);
|
|
154
|
+
}
|
|
155
|
+
if (!res.ok) {
|
|
156
|
+
const body = await res.text().catch(() => "");
|
|
157
|
+
throw new ProviderApiError(`Clerk ${init.method} ${path} → ${res.status}`, res.status, body);
|
|
158
|
+
}
|
|
159
|
+
if (res.status === 204) return void 0;
|
|
160
|
+
const text = await res.text();
|
|
161
|
+
if (!text) return void 0;
|
|
162
|
+
return JSON.parse(text);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region src/parse-webhook.ts
|
|
167
|
+
/**
|
|
168
|
+
* Translates a Clerk webhook delivery into the normalized `AuthEvent`
|
|
169
|
+
* shape defined in `@theholocron/cli`. The first concrete example of
|
|
170
|
+
* the cross-provider sync pattern (see `AuthEvent` JSDoc in core).
|
|
171
|
+
*
|
|
172
|
+
* Status: signature SHIPPED, body STUBBED. Real Svix signature
|
|
173
|
+
* verification (HMAC-SHA256 over `svix-id.svix-timestamp.body` with
|
|
174
|
+
* the `whsec_…` signing secret, base64-decoded) lands in a follow-up
|
|
175
|
+
* (tracked at #80). For now, parseWebhook trusts the request and
|
|
176
|
+
* focuses on shape translation — usable in environments where
|
|
177
|
+
* signature verification happens upstream (e.g., Vercel middleware,
|
|
178
|
+
* an API gateway), and an explicit error in production-grade
|
|
179
|
+
* deployments until the verification body lands.
|
|
180
|
+
*
|
|
181
|
+
* Reference event shapes:
|
|
182
|
+
* https://clerk.com/docs/integrations/webhooks/overview
|
|
183
|
+
* https://clerk.com/docs/reference/backend-api/tag/Webhooks
|
|
184
|
+
*/
|
|
185
|
+
const CLERK_TO_NORMALIZED = {
|
|
186
|
+
"user.created": "user.created",
|
|
187
|
+
"user.updated": "user.updated",
|
|
188
|
+
"user.deleted": "user.deleted"
|
|
189
|
+
};
|
|
190
|
+
async function parseWebhook(input) {
|
|
191
|
+
await verifySignature(input);
|
|
192
|
+
const bodyStr = typeof input.body === "string" ? input.body : input.body.toString("utf8");
|
|
193
|
+
let payload;
|
|
194
|
+
try {
|
|
195
|
+
payload = JSON.parse(bodyStr);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
throw new WebhookVerificationError(`Clerk webhook body is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
198
|
+
}
|
|
199
|
+
const normalizedType = CLERK_TO_NORMALIZED[payload.type];
|
|
200
|
+
if (!normalizedType) throw new WebhookVerificationError(`Clerk webhook event type "${payload.type}" has no normalized mapping yet`);
|
|
201
|
+
const primaryEmail = payload.data.email_addresses?.find((e) => e.id === payload.data.primary_email_address_id)?.email_address ?? payload.data.email_addresses?.[0]?.email_address;
|
|
202
|
+
const occurredAt = payload.created_at ? new Date(payload.created_at).toISOString() : (/* @__PURE__ */ new Date(0)).toISOString();
|
|
203
|
+
return {
|
|
204
|
+
type: normalizedType,
|
|
205
|
+
user: {
|
|
206
|
+
id: payload.data.id,
|
|
207
|
+
email: primaryEmail ?? "",
|
|
208
|
+
...payload.data.first_name !== void 0 ? { firstName: payload.data.first_name } : {},
|
|
209
|
+
...payload.data.last_name !== void 0 ? { lastName: payload.data.last_name } : {},
|
|
210
|
+
raw: payload.data
|
|
211
|
+
},
|
|
212
|
+
occurredAt
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Stub for Svix signature verification. The real implementation
|
|
217
|
+
* lands at #80 and will:
|
|
218
|
+
*
|
|
219
|
+
* 1. Read svix-id, svix-timestamp, svix-signature headers
|
|
220
|
+
* 2. Verify svix-timestamp is within the replay window (±5 min)
|
|
221
|
+
* 3. Compute HMAC-SHA256(<signing_secret>, `${id}.${timestamp}.${body}`)
|
|
222
|
+
* 4. Compare (constant-time) against the base64 sig in svix-signature
|
|
223
|
+
*
|
|
224
|
+
* For now: throw WebhookVerificationError when the signing secret is
|
|
225
|
+
* missing, so consumers see the contract; let the call through
|
|
226
|
+
* otherwise (signature-validation TODO).
|
|
227
|
+
*/
|
|
228
|
+
async function verifySignature(input) {
|
|
229
|
+
if (!input.signingSecret) throw new WebhookVerificationError("Clerk webhook signingSecret is required (use the Svix whsec_… value from the Clerk dashboard)");
|
|
230
|
+
return Promise.resolve();
|
|
231
|
+
}
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/index.ts
|
|
234
|
+
function createContext(options = {}) {
|
|
235
|
+
const restOpts = { token: resolveToken(options) };
|
|
236
|
+
if (options.baseUrl !== void 0) restOpts.baseUrl = options.baseUrl;
|
|
237
|
+
if (options.fetch !== void 0) restOpts.fetch = options.fetch;
|
|
238
|
+
return {
|
|
239
|
+
options,
|
|
240
|
+
rest: new ClerkRestClient(restOpts)
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function auth(ctx) {
|
|
244
|
+
return new ClerkAuth(ctx.rest);
|
|
245
|
+
}
|
|
246
|
+
function createPlugin(options = {}) {
|
|
247
|
+
const ctx = createContext(options);
|
|
248
|
+
return {
|
|
249
|
+
name: "@theholocron/holocron-plugin-clerk",
|
|
250
|
+
capabilities: { auth: () => auth(ctx) }
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
//#endregion
|
|
254
|
+
export { AuthError, ClerkAuth, ClerkRestClient, auth, createContext, createPlugin, parseWebhook, resolveToken };
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@theholocron/holocron-plugin-clerk",
|
|
3
|
+
"version": "2.0.0-alpha.0",
|
|
4
|
+
"description": "Holocron plugin for Clerk. Implements the auth capability against Clerk's Backend REST API.",
|
|
5
|
+
"homepage": "https://github.com/theholocron/holocron/tree/main/packages/holocron-plugin-clerk#readme",
|
|
6
|
+
"bugs": "https://github.com/theholocron/holocron/issues",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/theholocron/holocron.git",
|
|
10
|
+
"directory": "packages/holocron-plugin-clerk"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "Newton Koumantzelis",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "./dist/index.mjs",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.mts",
|
|
19
|
+
"import": "./dist/index.mjs",
|
|
20
|
+
"default": "./dist/index.mjs"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@theholocron/cli": "2.0.0-alpha.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@theholocron/tsconfig": "^4.1.0",
|
|
28
|
+
"@tsconfig/node-lts": "^24.0.0",
|
|
29
|
+
"@vitest/coverage-v8": "^3.2.6",
|
|
30
|
+
"eslint": "^9.36.0",
|
|
31
|
+
"globals": "^16.5.0",
|
|
32
|
+
"typescript": "^5.9.3",
|
|
33
|
+
"vitest": "^3.2.6",
|
|
34
|
+
"tsdown": "^0.22.3",
|
|
35
|
+
"@theholocron/cli": "2.0.0-alpha.0"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
"README.md"
|
|
43
|
+
],
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsdown",
|
|
46
|
+
"lint": "eslint .",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"test": "vitest run",
|
|
49
|
+
"test:watch": "vitest",
|
|
50
|
+
"test:coverage": "vitest run --coverage"
|
|
51
|
+
},
|
|
52
|
+
"types": "./dist/index.d.mts"
|
|
53
|
+
}
|