@theholocron/holocron-plugin-clerk 2.0.0-alpha.5 → 2.0.0-alpha.50
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 +7 -3
- package/dist/index.d.mts +7 -58
- package/dist/index.mjs +35 -107
- package/package.json +16 -10
package/README.md
CHANGED
|
@@ -5,8 +5,10 @@ against [Clerk's Backend REST API](https://clerk.com/docs/reference/backend-api)
|
|
|
5
5
|
|
|
6
6
|
## Install
|
|
7
7
|
|
|
8
|
+
<!-- prettier-ignore -->
|
|
8
9
|
```bash
|
|
9
10
|
pnpm add -D @theholocron/holocron-plugin-clerk@alpha
|
|
11
|
+
|
|
10
12
|
```
|
|
11
13
|
|
|
12
14
|
## Auth
|
|
@@ -24,12 +26,14 @@ api …`, but the `clerk` CLI just wraps the same REST API holocron talks to.
|
|
|
24
26
|
|
|
25
27
|
## Config
|
|
26
28
|
|
|
29
|
+
<!-- prettier-ignore -->
|
|
27
30
|
```jsonc
|
|
28
31
|
{
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
"providers": {
|
|
33
|
+
"auth": "clerk",
|
|
34
|
+
},
|
|
32
35
|
}
|
|
36
|
+
|
|
33
37
|
```
|
|
34
38
|
|
|
35
39
|
No plugin-level options today. Per-instance scoping (Development vs.
|
package/dist/index.d.mts
CHANGED
|
@@ -1,67 +1,16 @@
|
|
|
1
|
-
import { Auth, AuthDescription, AuthEvent, AuthIdentity, AuthUser, CreateAuthUserInput, ParseWebhookInput, WebhookDashboardInfo } from "@theholocron/cli";
|
|
1
|
+
import { Auth, AuthDescription, AuthError, AuthEvent, AuthIdentity, AuthUser, CreateAuthUserInput, ParseWebhookInput, ResolveTokenInput, WebhookDashboardInfo } from "@theholocron/cli";
|
|
2
|
+
import { ClerkClient, createClerkClient } from "@theholocron/clerk-client";
|
|
2
3
|
|
|
3
4
|
//#region src/auth.d.ts
|
|
4
|
-
|
|
5
|
-
* Token resolution for the Clerk plugin.
|
|
6
|
-
*
|
|
7
|
-
* Resolution order (matches the standard 4-step precedence set by
|
|
8
|
-
* `.notes/tech-auth-bootstrap.spec.md`):
|
|
9
|
-
* 1. explicit `cliToken` argument (from `--token` flag)
|
|
10
|
-
* 2. HOLOCRON_CLERK_SECRET_KEY env var (preferred — explicit intent)
|
|
11
|
-
* 3. CLERK_SECRET_KEY env var (the default Clerk's docs reference)
|
|
12
|
-
* 4. keyring (com.theholocron.cli / "clerk")
|
|
13
|
-
* 5. AuthError naming all four options + the bootstrap hint
|
|
14
|
-
*
|
|
15
|
-
* The key (sk_test_* / sk_live_*) determines which Clerk instance —
|
|
16
|
-
* Development or Production — every call hits.
|
|
17
|
-
*/
|
|
18
|
-
declare class AuthError extends Error {
|
|
19
|
-
name: string;
|
|
20
|
-
}
|
|
21
|
-
interface ResolveTokenInput {
|
|
22
|
-
/** From `--token` CLI flag. */
|
|
23
|
-
cliToken?: string;
|
|
24
|
-
/** Env vars; passed in for testability. Defaults to `process.env`. */
|
|
25
|
-
env?: NodeJS.ProcessEnv;
|
|
26
|
-
/** Keyring lookup fn; passed in for testability. Defaults to `getToken(provider)`. */
|
|
27
|
-
keyring?: (provider: string) => string | null;
|
|
28
|
-
}
|
|
29
|
-
declare function resolveToken(input?: ResolveTokenInput): string;
|
|
30
|
-
//#endregion
|
|
31
|
-
//#region src/rest.d.ts
|
|
32
|
-
/**
|
|
33
|
-
* Thin REST wrapper around api.clerk.com/v1.
|
|
34
|
-
*
|
|
35
|
-
* Same pattern as the github/vercel/neon REST clients — bearer auth,
|
|
36
|
-
* JSON-only bodies, transport-failure wrapping with `status: 0` so the
|
|
37
|
-
* orchestrator's soft-skip path sees a clear "Clerk GET /path failed"
|
|
38
|
-
* message instead of a generic `TypeError: fetch failed`.
|
|
39
|
-
*/
|
|
40
|
-
interface RestClientOptions {
|
|
41
|
-
token: string;
|
|
42
|
-
fetch?: typeof fetch;
|
|
43
|
-
baseUrl?: string;
|
|
44
|
-
}
|
|
45
|
-
interface RequestOptions {
|
|
46
|
-
method?: "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
|
|
47
|
-
body?: unknown;
|
|
48
|
-
query?: Record<string, string>;
|
|
49
|
-
}
|
|
50
|
-
declare class ClerkRestClient {
|
|
51
|
-
private readonly token;
|
|
52
|
-
private readonly fetchImpl;
|
|
53
|
-
readonly baseUrl: string;
|
|
54
|
-
constructor(opts: RestClientOptions);
|
|
55
|
-
request<T>(path: string, opts?: RequestOptions): Promise<T>;
|
|
56
|
-
}
|
|
5
|
+
declare const resolveToken: (input?: import("@theholocron/http-client").ResolveTokenInput) => string;
|
|
57
6
|
//#endregion
|
|
58
7
|
//#region src/capabilities/auth.d.ts
|
|
59
8
|
type ClerkAuthOptions = Record<string, never>;
|
|
60
9
|
declare class ClerkAuth implements Auth {
|
|
61
|
-
private readonly
|
|
10
|
+
private readonly client;
|
|
62
11
|
readonly key: "auth";
|
|
63
12
|
readonly providerName = "clerk";
|
|
64
|
-
constructor(
|
|
13
|
+
constructor(client: ClerkClient, _opts?: ClerkAuthOptions);
|
|
65
14
|
describe(): Promise<AuthDescription>;
|
|
66
15
|
whoami(): Promise<AuthIdentity>;
|
|
67
16
|
ensureWebhookApp(): Promise<{
|
|
@@ -109,7 +58,7 @@ interface ClerkPluginOptions extends ResolveTokenInput {
|
|
|
109
58
|
}
|
|
110
59
|
interface PluginContext {
|
|
111
60
|
options: ClerkPluginOptions;
|
|
112
|
-
|
|
61
|
+
client: ClerkClient;
|
|
113
62
|
}
|
|
114
63
|
declare function createContext(options?: ClerkPluginOptions): PluginContext;
|
|
115
64
|
declare function auth(ctx: PluginContext): Auth;
|
|
@@ -127,4 +76,4 @@ declare function createPlugin(options?: ClerkPluginOptions): {
|
|
|
127
76
|
*/
|
|
128
77
|
declare const AUTH_HINT: string;
|
|
129
78
|
//#endregion
|
|
130
|
-
export { AUTH_HINT, AuthError, ClerkAuth, ClerkPluginOptions,
|
|
79
|
+
export { AUTH_HINT, AuthError, ClerkAuth, ClerkPluginOptions, PluginContext, type ResolveTokenInput, type VerifyTokenFailure, type VerifyTokenResult, type VerifyTokenSuccess, auth, createClerkClient, createContext, createPlugin, parseWebhook, resolveToken, verifyToken };
|
package/dist/index.mjs
CHANGED
|
@@ -1,29 +1,12 @@
|
|
|
1
|
-
import { ProviderApiError, WebhookVerificationError,
|
|
1
|
+
import { AuthError, ProviderApiError, WebhookVerificationError, createResolveToken } from "@theholocron/cli";
|
|
2
|
+
import { createClerkClient } from "@theholocron/clerk-client";
|
|
2
3
|
//#region src/auth.ts
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
* 4. keyring (com.theholocron.cli / "clerk")
|
|
12
|
-
* 5. AuthError naming all four options + the bootstrap hint
|
|
13
|
-
*
|
|
14
|
-
* The key (sk_test_* / sk_live_*) determines which Clerk instance —
|
|
15
|
-
* Development or Production — every call hits.
|
|
16
|
-
*/
|
|
17
|
-
var AuthError = class extends Error {
|
|
18
|
-
name = "AuthError";
|
|
19
|
-
};
|
|
20
|
-
function resolveToken(input = {}) {
|
|
21
|
-
const env = input.env ?? process.env;
|
|
22
|
-
const keyring = input.keyring ?? getToken;
|
|
23
|
-
const token = input.cliToken || env["HOLOCRON_CLERK_SECRET_KEY"] || env["CLERK_SECRET_KEY"] || keyring("clerk");
|
|
24
|
-
if (!token) throw new AuthError("no Clerk secret key found. Pass --token <KEY>, set HOLOCRON_CLERK_SECRET_KEY / CLERK_SECRET_KEY, or run: holocron auth set clerk <KEY>");
|
|
25
|
-
return token;
|
|
26
|
-
}
|
|
4
|
+
const resolveToken = createResolveToken({
|
|
5
|
+
envName: "HOLOCRON_CLERK_SECRET_KEY",
|
|
6
|
+
vendorEnvName: "CLERK_SECRET_KEY",
|
|
7
|
+
keyringService: "clerk",
|
|
8
|
+
errorMessage: "no Clerk secret key found. Pass --token <KEY>, set HOLOCRON_CLERK_SECRET_KEY / CLERK_SECRET_KEY, or run: holocron auth set clerk <KEY>"
|
|
9
|
+
});
|
|
27
10
|
//#endregion
|
|
28
11
|
//#region src/capabilities/auth.ts
|
|
29
12
|
/**
|
|
@@ -52,11 +35,11 @@ function resolveToken(input = {}) {
|
|
|
52
35
|
* no per-call instance switch.
|
|
53
36
|
*/
|
|
54
37
|
var ClerkAuth = class {
|
|
55
|
-
|
|
38
|
+
client;
|
|
56
39
|
key = "auth";
|
|
57
40
|
providerName = "clerk";
|
|
58
|
-
constructor(
|
|
59
|
-
this.
|
|
41
|
+
constructor(client, _opts = {}) {
|
|
42
|
+
this.client = client;
|
|
60
43
|
}
|
|
61
44
|
async describe() {
|
|
62
45
|
return {
|
|
@@ -67,12 +50,12 @@ var ClerkAuth = class {
|
|
|
67
50
|
async whoami() {
|
|
68
51
|
return {
|
|
69
52
|
provider: "clerk",
|
|
70
|
-
details: { userCount: (await this.
|
|
53
|
+
details: { userCount: (await this.client.users.count()).total_count }
|
|
71
54
|
};
|
|
72
55
|
}
|
|
73
56
|
async ensureWebhookApp() {
|
|
74
57
|
try {
|
|
75
|
-
await this.
|
|
58
|
+
await this.client.webhooks.ensureSvixApp();
|
|
76
59
|
return { alreadyExists: false };
|
|
77
60
|
} catch (err) {
|
|
78
61
|
if (err instanceof ProviderApiError && isAlreadyExistsError(err)) return { alreadyExists: true };
|
|
@@ -80,24 +63,21 @@ var ClerkAuth = class {
|
|
|
80
63
|
}
|
|
81
64
|
}
|
|
82
65
|
async getWebhookDashboardUrl() {
|
|
83
|
-
const body = await this.
|
|
66
|
+
const body = await this.client.webhooks.getSvixUrl();
|
|
84
67
|
const url = body.url ?? body.svix_url;
|
|
85
68
|
if (!url) throw new ProviderApiError("Clerk POST /webhooks/svix_url returned 200 but no `url` field", 500, void 0);
|
|
86
69
|
return { url };
|
|
87
70
|
}
|
|
88
71
|
async createUser(input) {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
...input.firstName ? { first_name: input.firstName } : {},
|
|
95
|
-
...input.lastName ? { last_name: input.lastName } : {}
|
|
96
|
-
}
|
|
72
|
+
const user = await this.client.users.create({
|
|
73
|
+
email_address: [input.email],
|
|
74
|
+
password: input.password,
|
|
75
|
+
...input.firstName ? { first_name: input.firstName } : {},
|
|
76
|
+
...input.lastName ? { last_name: input.lastName } : {}
|
|
97
77
|
});
|
|
98
|
-
const email =
|
|
78
|
+
const email = user.email_addresses[0]?.email_address ?? input.email;
|
|
99
79
|
return {
|
|
100
|
-
id:
|
|
80
|
+
id: user.id,
|
|
101
81
|
email
|
|
102
82
|
};
|
|
103
83
|
}
|
|
@@ -115,60 +95,6 @@ function isAlreadyExistsError(err) {
|
|
|
115
95
|
return false;
|
|
116
96
|
}
|
|
117
97
|
//#endregion
|
|
118
|
-
//#region src/rest.ts
|
|
119
|
-
/**
|
|
120
|
-
* Thin REST wrapper around api.clerk.com/v1.
|
|
121
|
-
*
|
|
122
|
-
* Same pattern as the github/vercel/neon REST clients — bearer auth,
|
|
123
|
-
* JSON-only bodies, transport-failure wrapping with `status: 0` so the
|
|
124
|
-
* orchestrator's soft-skip path sees a clear "Clerk GET /path failed"
|
|
125
|
-
* message instead of a generic `TypeError: fetch failed`.
|
|
126
|
-
*/
|
|
127
|
-
var ClerkRestClient = class {
|
|
128
|
-
token;
|
|
129
|
-
fetchImpl;
|
|
130
|
-
baseUrl;
|
|
131
|
-
constructor(opts) {
|
|
132
|
-
this.token = opts.token;
|
|
133
|
-
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
134
|
-
let url = opts.baseUrl ?? "https://api.clerk.com/v1";
|
|
135
|
-
while (url.endsWith("/")) url = url.slice(0, -1);
|
|
136
|
-
this.baseUrl = url;
|
|
137
|
-
}
|
|
138
|
-
async request(path, opts = {}) {
|
|
139
|
-
const url = new URL(`${this.baseUrl}${path.startsWith("/") ? path : "/" + path}`);
|
|
140
|
-
for (const [k, v] of Object.entries(opts.query ?? {})) url.searchParams.set(k, v);
|
|
141
|
-
const fullUrl = url.toString();
|
|
142
|
-
const headers = {
|
|
143
|
-
authorization: `Bearer ${this.token}`,
|
|
144
|
-
accept: "application/json"
|
|
145
|
-
};
|
|
146
|
-
const init = {
|
|
147
|
-
method: opts.method ?? "GET",
|
|
148
|
-
headers
|
|
149
|
-
};
|
|
150
|
-
if (opts.body !== void 0) {
|
|
151
|
-
headers["content-type"] = "application/json";
|
|
152
|
-
init.body = JSON.stringify(opts.body);
|
|
153
|
-
}
|
|
154
|
-
let res;
|
|
155
|
-
try {
|
|
156
|
-
res = await this.fetchImpl(fullUrl, init);
|
|
157
|
-
} catch (err) {
|
|
158
|
-
const detail = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
159
|
-
throw new ProviderApiError(`Clerk ${init.method} ${path} failed: ${detail}`, 0, void 0);
|
|
160
|
-
}
|
|
161
|
-
if (!res.ok) {
|
|
162
|
-
const body = await res.text().catch(() => "");
|
|
163
|
-
throw new ProviderApiError(`Clerk ${init.method} ${path} → ${res.status}`, res.status, body);
|
|
164
|
-
}
|
|
165
|
-
if (res.status === 204) return void 0;
|
|
166
|
-
const text = await res.text();
|
|
167
|
-
if (!text) return void 0;
|
|
168
|
-
return JSON.parse(text);
|
|
169
|
-
}
|
|
170
|
-
};
|
|
171
|
-
//#endregion
|
|
172
98
|
//#region src/parse-webhook.ts
|
|
173
99
|
/**
|
|
174
100
|
* Translates a Clerk webhook delivery into the normalized `AuthEvent`
|
|
@@ -248,15 +174,16 @@ async function verifySignature(input) {
|
|
|
248
174
|
* canonical "is this secret key valid?" endpoint.
|
|
249
175
|
*/
|
|
250
176
|
async function verifyToken(token, opts = {}) {
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
177
|
+
const client = createClerkClient({
|
|
178
|
+
token,
|
|
179
|
+
baseUrl: opts.baseUrl,
|
|
180
|
+
fetch: opts.fetch
|
|
181
|
+
});
|
|
255
182
|
try {
|
|
256
|
-
const
|
|
183
|
+
const inst = await client.instance.get();
|
|
257
184
|
return {
|
|
258
185
|
ok: true,
|
|
259
|
-
subject: `${
|
|
186
|
+
subject: `${inst?.environment_type ?? "unknown"} instance ${inst?.id ?? "unknown"}`
|
|
260
187
|
};
|
|
261
188
|
} catch (err) {
|
|
262
189
|
return {
|
|
@@ -268,16 +195,17 @@ async function verifyToken(token, opts = {}) {
|
|
|
268
195
|
//#endregion
|
|
269
196
|
//#region src/index.ts
|
|
270
197
|
function createContext(options = {}) {
|
|
271
|
-
const restOpts = { token: resolveToken(options) };
|
|
272
|
-
if (options.baseUrl !== void 0) restOpts.baseUrl = options.baseUrl;
|
|
273
|
-
if (options.fetch !== void 0) restOpts.fetch = options.fetch;
|
|
274
198
|
return {
|
|
275
199
|
options,
|
|
276
|
-
|
|
200
|
+
client: createClerkClient({
|
|
201
|
+
token: resolveToken(options),
|
|
202
|
+
baseUrl: options.baseUrl,
|
|
203
|
+
fetch: options.fetch
|
|
204
|
+
})
|
|
277
205
|
};
|
|
278
206
|
}
|
|
279
207
|
function auth(ctx) {
|
|
280
|
-
return new ClerkAuth(ctx.
|
|
208
|
+
return new ClerkAuth(ctx.client);
|
|
281
209
|
}
|
|
282
210
|
function createPlugin(options = {}) {
|
|
283
211
|
const ctx = createContext(options);
|
|
@@ -294,4 +222,4 @@ function createPlugin(options = {}) {
|
|
|
294
222
|
*/
|
|
295
223
|
const AUTH_HINT = "grab your SECRET key (sk_test_* / sk_live_*) at https://dashboard.clerk.com → API Keys, then run: holocron auth set clerk <KEY>. Do NOT use the publishable key.";
|
|
296
224
|
//#endregion
|
|
297
|
-
export { AUTH_HINT, AuthError, ClerkAuth,
|
|
225
|
+
export { AUTH_HINT, AuthError, ClerkAuth, auth, createClerkClient, createContext, createPlugin, parseWebhook, resolveToken, verifyToken };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theholocron/holocron-plugin-clerk",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.50",
|
|
4
4
|
"description": "Holocron plugin for Clerk. Implements the auth capability against Clerk's Backend REST API.",
|
|
5
5
|
"homepage": "https://github.com/theholocron/holocron/tree/main/packages/holocron-plugin-clerk#readme",
|
|
6
6
|
"bugs": "https://github.com/theholocron/holocron/issues",
|
|
@@ -21,19 +21,25 @@
|
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
|
-
"@theholocron/
|
|
24
|
+
"@theholocron/clerk-client": "^0.7.0",
|
|
25
|
+
"@theholocron/cli": "2.0.0-alpha.50"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
|
-
"@theholocron/
|
|
28
|
-
"@
|
|
29
|
-
"@
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"vitest": "^
|
|
28
|
+
"@theholocron/clerk-client": "^0.7.0",
|
|
29
|
+
"@types/node": "^26",
|
|
30
|
+
"@theholocron/eslint-config": "^5.2.0",
|
|
31
|
+
"@theholocron/tsconfig": "^6.0.0",
|
|
32
|
+
"@theholocron/tsdown-config": "^5.1.2",
|
|
33
|
+
"@theholocron/vitest-config": "^5.2.0",
|
|
34
|
+
"@vitest/eslint-plugin": "^1.6.23",
|
|
35
|
+
"eslint": "^10.7.0",
|
|
36
|
+
"eslint-plugin-n": "^18.2.2",
|
|
37
|
+
"globals": "^17.7.0",
|
|
34
38
|
"tsdown": "^0.22.3",
|
|
35
39
|
"tsx": "^4.22.4",
|
|
36
|
-
"
|
|
40
|
+
"typescript": "^5.9.3",
|
|
41
|
+
"vitest": "^4.1.10",
|
|
42
|
+
"@theholocron/cli": "2.0.0-alpha.50"
|
|
37
43
|
},
|
|
38
44
|
"publishConfig": {
|
|
39
45
|
"access": "public"
|