@sneekin/sdk 1.1.0 → 1.1.2

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.
@@ -1,3 +1,29 @@
1
+ // src/core/derive-keys.ts
2
+ import { hkdfSync } from "crypto";
3
+ var KEY_LABELS = {
4
+ qrSigningKey: "sneek-qr-v1",
5
+ webhookSigningKey: "sneek-webhook-v1",
6
+ proofKey: "sneek-proof-v1"
7
+ };
8
+ var API_KEY_PATTERN = /^sneek_[A-Za-z0-9_-]{43}$/;
9
+ function isValidApiKey(apiKey) {
10
+ return API_KEY_PATTERN.test(apiKey);
11
+ }
12
+ function deriveKeys(apiKey) {
13
+ if (typeof apiKey !== "string" || !isValidApiKey(apiKey)) {
14
+ throw new TypeError("apiKey must use the sneek_ format with a 43-character secret");
15
+ }
16
+ const inputKeyMaterial = Buffer.from(apiKey, "utf8");
17
+ const derive = (label) => Buffer.from(
18
+ hkdfSync("sha256", inputKeyMaterial, Buffer.alloc(0), label, 32)
19
+ );
20
+ return {
21
+ qrSigningKey: derive(KEY_LABELS.qrSigningKey),
22
+ webhookSigningKey: derive(KEY_LABELS.webhookSigningKey),
23
+ proofKey: derive(KEY_LABELS.proofKey)
24
+ };
25
+ }
26
+
1
27
  // src/core/client.ts
2
28
  var SneekApiError = class extends Error {
3
29
  constructor(status, type, title, detail) {
@@ -22,6 +48,7 @@ var SneekClient = class {
22
48
  constructor(options) {
23
49
  if (!options.apiUrl) throw new Error("apiUrl is required");
24
50
  if (!options.apiKey) throw new Error("apiKey is required");
51
+ if (!isValidApiKey(options.apiKey)) throw new Error("apiKey must use the sneek_ format with a 43-character secret");
25
52
  this.apiUrl = options.apiUrl.replace(/\/+$/, "");
26
53
  this.apiKey = options.apiKey;
27
54
  this.fetchImpl = options.fetch ?? fetch;
@@ -93,31 +120,9 @@ function asProblem(value) {
93
120
  };
94
121
  }
95
122
 
96
- // src/core/derive-keys.ts
97
- import { hkdfSync } from "crypto";
98
- var KEY_LABELS = {
99
- qrSigningKey: "sneek-qr-v1",
100
- webhookSigningKey: "sneek-webhook-v1",
101
- proofKey: "sneek-proof-v1"
102
- };
103
- function deriveKeys(apiKey) {
104
- if (typeof apiKey !== "string" || apiKey.length === 0) {
105
- throw new TypeError("apiKey must be a nonempty string");
106
- }
107
- const inputKeyMaterial = Buffer.from(apiKey, "utf8");
108
- const derive = (label) => Buffer.from(
109
- hkdfSync("sha256", inputKeyMaterial, Buffer.alloc(0), label, 32)
110
- );
111
- return {
112
- qrSigningKey: derive(KEY_LABELS.qrSigningKey),
113
- webhookSigningKey: derive(KEY_LABELS.webhookSigningKey),
114
- proofKey: derive(KEY_LABELS.proofKey)
115
- };
116
- }
117
-
118
123
  // src/verify/handler.ts
119
124
  import { createHash, createHmac, randomBytes, timingSafeEqual } from "crypto";
120
- import { createRemoteJWKSet, jwtVerify } from "jose";
125
+ import { createLocalJWKSet, createRemoteJWKSet, jwtVerify } from "jose";
121
126
 
122
127
  // src/verify/auth-schema.ts
123
128
  import { z } from "zod";
@@ -305,8 +310,16 @@ var SneekVerifyHandler = class {
305
310
  const partner = await this.client.request("GET", "/api/verify/qr/partner-config");
306
311
  let payload;
307
312
  try {
308
- const jwks = this.jwksByIssuer.get(partner.issuer) ?? createRemoteJWKSet(new URL("/oauth/jwks", partner.issuer));
309
- this.jwksByIssuer.set(partner.issuer, jwks);
313
+ let jwks = this.jwksByIssuer.get(partner.issuer);
314
+ if (!jwks) {
315
+ try {
316
+ const localJwksData = await this.client.request("GET", "/oauth/jwks");
317
+ jwks = createLocalJWKSet(localJwksData);
318
+ } catch {
319
+ jwks = createRemoteJWKSet(new URL("/oauth/jwks", partner.issuer));
320
+ }
321
+ this.jwksByIssuer.set(partner.issuer, jwks);
322
+ }
310
323
  ({ payload } = await jwtVerify(result.idToken, jwks, {
311
324
  issuer: partner.issuer,
312
325
  audience: partner.clientId
@@ -390,11 +403,11 @@ var SneekVerifyHandler = class {
390
403
  const partner = await this.partnerConfig();
391
404
  if (!partner.origins.includes(origin)) return jsonResponse({ error: "Origin is not registered for QR verification" }, 403);
392
405
  const nonce = randomBytes(32).toString("hex");
393
- const ref = createHash("sha256").update(nonce).digest("hex");
406
+ const ref = createHash("sha256").update(Buffer.from(nonce, "hex")).digest("hex");
394
407
  const issuedAt = Math.floor(Date.now() / 1e3);
395
408
  const expiresAt = (issuedAt + partner.qrTtlSeconds) * 1e3;
396
409
  const bindingProof = randomBytes(32).toString("base64url");
397
- const signature = createHmac("sha256", this.keys.qrSigningKey).update(["sneek-qr-v1", partner.applicationId, nonce, origin, String(issuedAt)].join("\n")).digest().subarray(0, 16).toString("base64url");
410
+ const signature = createHmac("sha256", this.keys.qrSigningKey).update(["sneek-qr-v1", partner.applicationId, partner.kid, nonce, origin, String(issuedAt)].join("\n")).digest().subarray(0, 16).toString("base64url");
398
411
  await this.approvalStore.createQrSession({
399
412
  ref,
400
413
  applicationId: partner.applicationId,
@@ -402,7 +415,7 @@ var SneekVerifyHandler = class {
402
415
  bindingProofHash: this.hash(bindingProof),
403
416
  expiresAt
404
417
  });
405
- return jsonResponse({ ref, nonce, issuedAt, expiresAt, signature, bindingProof }, 200);
418
+ return jsonResponse({ ref, nonce, issuedAt, expiresAt, kid: partner.kid, signature, bindingProof }, 200);
406
419
  }
407
420
  async qrStatus(ref, bindingProof, origin) {
408
421
  const session = await this.qrSession(ref, bindingProof, origin);
@@ -437,5 +450,6 @@ export {
437
450
  SneekApiError,
438
451
  SneekClient,
439
452
  SneekVerifyHandler,
440
- deriveKeys
453
+ deriveKeys,
454
+ isValidApiKey
441
455
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sneekin/sdk",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -66,4 +66,4 @@
66
66
  "engines": {
67
67
  "node": ">=20"
68
68
  }
69
- }
69
+ }