@shelbyb/daywalker-sdk 0.3.0 → 0.4.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.
Files changed (54) hide show
  1. package/README.md +31 -23
  2. package/dist/analytics/index.d.ts +16 -2
  3. package/dist/analytics/index.js +79 -14
  4. package/dist/cli.js +133 -31
  5. package/dist/client.d.ts +29 -2
  6. package/dist/client.js +314 -62
  7. package/dist/config.d.ts +8 -2
  8. package/dist/config.js +14 -0
  9. package/dist/crypto/encoding.d.ts +5 -0
  10. package/dist/crypto/encoding.js +14 -0
  11. package/dist/crypto/index.d.ts +3 -3
  12. package/dist/crypto/index.js +2 -2
  13. package/dist/crypto/keys.d.ts +3 -3
  14. package/dist/crypto/keys.js +28 -7
  15. package/dist/crypto/signatures.d.ts +42 -12
  16. package/dist/crypto/signatures.js +236 -79
  17. package/dist/express.d.ts +1 -0
  18. package/dist/express.js +8 -6
  19. package/dist/flags/client.d.ts +32 -4
  20. package/dist/flags/client.js +74 -12
  21. package/dist/flags/common/events.d.ts +9 -0
  22. package/dist/flags/common/events.js +51 -6
  23. package/dist/flags/common/index.d.ts +3 -3
  24. package/dist/flags/common/index.js +3 -3
  25. package/dist/flags/common/sse.d.ts +24 -3
  26. package/dist/flags/common/sse.js +85 -15
  27. package/dist/flags/common/version.d.ts +2 -0
  28. package/dist/flags/common/version.js +2 -0
  29. package/dist/flags/engine/index.d.ts +1 -1
  30. package/dist/flags/engine/index.js +1 -1
  31. package/dist/flags/engine/operators.d.ts +20 -3
  32. package/dist/flags/engine/operators.js +38 -32
  33. package/dist/flags/node.d.ts +26 -14
  34. package/dist/flags/node.js +51 -7
  35. package/dist/flags/react.d.ts +6 -4
  36. package/dist/flags/react.js +47 -17
  37. package/dist/index.d.ts +7 -6
  38. package/dist/index.js +4 -4
  39. package/dist/middleware.d.ts +18 -0
  40. package/dist/middleware.js +28 -7
  41. package/dist/next.d.ts +52 -8
  42. package/dist/next.js +151 -37
  43. package/dist/openfeature/server.d.ts +3 -0
  44. package/dist/openfeature/server.js +14 -2
  45. package/dist/openfeature/shared.d.ts +1 -1
  46. package/dist/openfeature/shared.js +22 -2
  47. package/dist/openfeature/web.js +9 -3
  48. package/dist/types.d.ts +56 -4
  49. package/dist/types.js +6 -0
  50. package/docs/analytics.md +1 -1
  51. package/docs/flags-node.md +1 -1
  52. package/docs/flags.md +2 -1
  53. package/package.json +32 -14
  54. package/assets/logo-512.png +0 -0
@@ -8,6 +8,11 @@ export declare function bytesToBase64(bytes: Uint8Array): string;
8
8
  export declare function base64ToBytes(b64: string): Uint8Array;
9
9
  export declare function bytesToBase64Url(bytes: Uint8Array): string;
10
10
  export declare function base64UrlToBytes(b64url: string): Uint8Array;
11
+ /**
12
+ * Decodes unpadded base64url (RFC 7515 §2) strictly: only `[A-Za-z0-9_-]` and only the canonical encoding
13
+ * of the bytes, so a token or signature has exactly one accepted spelling. Throws otherwise.
14
+ */
15
+ export declare function base64UrlToBytesStrict(b64url: string): Uint8Array;
11
16
  export declare function bytesToHex(bytes: Uint8Array): string;
12
17
  export declare function hexToBytes(hex: string): Uint8Array;
13
18
  /**
@@ -47,6 +47,20 @@ export function base64UrlToBytes(b64url) {
47
47
  }
48
48
  return base64ToBytes(b64);
49
49
  }
50
+ /**
51
+ * Decodes unpadded base64url (RFC 7515 §2) strictly: only `[A-Za-z0-9_-]` and only the canonical encoding
52
+ * of the bytes, so a token or signature has exactly one accepted spelling. Throws otherwise.
53
+ */
54
+ export function base64UrlToBytesStrict(b64url) {
55
+ if (!/^[A-Za-z0-9_-]*$/.test(b64url) || b64url.length % 4 === 1) {
56
+ throw new Error("Invalid base64url encoding");
57
+ }
58
+ const bytes = base64UrlToBytes(b64url);
59
+ if (bytesToBase64Url(bytes) !== b64url) {
60
+ throw new Error("Non-canonical base64url encoding");
61
+ }
62
+ return bytes;
63
+ }
50
64
  export function bytesToHex(bytes) {
51
65
  let hex = "";
52
66
  for (let i = 0; i < bytes.length; i++) {
@@ -1,5 +1,5 @@
1
- export { stringToUtf8, utf8ToString, bytesToBase64, base64ToBytes, bytesToBase64Url, base64UrlToBytes, bytesToHex, hexToBytes, timingSafeEqual, timingSafeEqualString, maskSecret, redactSecrets, } from "./encoding.js";
1
+ export { stringToUtf8, utf8ToString, bytesToBase64, base64ToBytes, bytesToBase64Url, base64UrlToBytes, base64UrlToBytesStrict, bytesToHex, hexToBytes, timingSafeEqual, timingSafeEqualString, maskSecret, redactSecrets, } from "./encoding.js";
2
2
  export { DEFAULT_MAX_BODY_SIZE_BYTES, computeContentDigest, sha256Base64, sha256Base64Url, } from "./digest.js";
3
3
  export { calculateJwkThumbprint, generateSigningKey, loadSigningKey, loadPublicKey, } from "./keys.js";
4
- export { SIGNATURE_VERSION, SIGNATURE_LABEL, SIGNATURE_ALG, MAX_CLOCK_SKEW_SECONDS, formatSignatureParams, createSignatureBase, signRequest, verifyRequestSignature, verifyJwtLease, registerPublicKey, } from "./signatures.js";
5
- export type { SignedHeaders, SignRequestParams, VerifySignatureParams, VerifySignatureResult, } from "./signatures.js";
4
+ export { SIGNATURE_VERSION, SIGNATURE_LABEL, SIGNATURE_ALG, MAX_CLOCK_SKEW_SECONDS, REPLAY_CACHE_MAX_ENTRIES, SignatureReplayCache, formatSignatureParams, createSignatureBase, signRequest, verifyRequestSignature, verifyJwtLease, registerPublicKey, } from "./signatures.js";
5
+ export type { SignedHeaders, SignRequestParams, VerifySignatureParams, VerifySignatureResult, ReplayStore, } from "./signatures.js";
@@ -1,4 +1,4 @@
1
- export { stringToUtf8, utf8ToString, bytesToBase64, base64ToBytes, bytesToBase64Url, base64UrlToBytes, bytesToHex, hexToBytes, timingSafeEqual, timingSafeEqualString, maskSecret, redactSecrets, } from "./encoding.js";
1
+ export { stringToUtf8, utf8ToString, bytesToBase64, base64ToBytes, bytesToBase64Url, base64UrlToBytes, base64UrlToBytesStrict, bytesToHex, hexToBytes, timingSafeEqual, timingSafeEqualString, maskSecret, redactSecrets, } from "./encoding.js";
2
2
  export { DEFAULT_MAX_BODY_SIZE_BYTES, computeContentDigest, sha256Base64, sha256Base64Url, } from "./digest.js";
3
3
  export { calculateJwkThumbprint, generateSigningKey, loadSigningKey, loadPublicKey, } from "./keys.js";
4
- export { SIGNATURE_VERSION, SIGNATURE_LABEL, SIGNATURE_ALG, MAX_CLOCK_SKEW_SECONDS, formatSignatureParams, createSignatureBase, signRequest, verifyRequestSignature, verifyJwtLease, registerPublicKey, } from "./signatures.js";
4
+ export { SIGNATURE_VERSION, SIGNATURE_LABEL, SIGNATURE_ALG, MAX_CLOCK_SKEW_SECONDS, REPLAY_CACHE_MAX_ENTRIES, SignatureReplayCache, formatSignatureParams, createSignatureBase, signRequest, verifyRequestSignature, verifyJwtLease, registerPublicKey, } from "./signatures.js";
@@ -1,4 +1,4 @@
1
- import type { PublicJwk, SigningKey } from "../types.js";
1
+ import type { PublicJwk, PublicJwks, SigningKey } from "../types.js";
2
2
  /**
3
3
  * Calculate the RFC 7638 JWK thumbprint for an Ed25519 public key.
4
4
  */
@@ -17,9 +17,9 @@ export declare function generateSigningKey(): Promise<SigningKey>;
17
17
  */
18
18
  export declare function loadSigningKey(raw: string | SigningKey): Promise<SigningKey>;
19
19
  /**
20
- * Load an Ed25519 public key from JWK, PEM, Base64 SPKI, or Base64url raw public key.
20
+ * Load an Ed25519 public key from JWK, JWKS (selected by `kid`), PEM, Base64 SPKI, or Base64url raw public key.
21
21
  */
22
- export declare function loadPublicKey(raw: string | PublicJwk | CryptoKey): Promise<{
22
+ export declare function loadPublicKey(raw: string | PublicJwk | PublicJwks | CryptoKey, kid?: string): Promise<{
23
23
  publicKey: CryptoKey;
24
24
  publicJwk: PublicJwk;
25
25
  }>;
@@ -131,9 +131,27 @@ export async function loadSigningKey(raw) {
131
131
  };
132
132
  }
133
133
  /**
134
- * Load an Ed25519 public key from JWK, PEM, Base64 SPKI, or Base64url raw public key.
134
+ * Picks the Ed25519 key with this `kid` from a JWKS, or its only Ed25519 key (the signature check still
135
+ * has to pass against it). Several keys and no matching `kid` is an error rather than a guess.
135
136
  */
136
- export async function loadPublicKey(raw) {
137
+ function selectJwk(jwks, kid) {
138
+ const keys = (Array.isArray(jwks.keys) ? jwks.keys : []).filter((k) => !!k && typeof k === "object" && k.kty === "OKP" && k.crv === "Ed25519");
139
+ const byKid = kid ? keys.find((k) => k.kid === kid) : undefined;
140
+ if (byKid)
141
+ return byKid;
142
+ if (keys.length === 1)
143
+ return keys[0];
144
+ if (keys.length === 0)
145
+ throw new Error("JWKS contains no Ed25519 keys");
146
+ throw new Error(kid ? `No key in JWKS matches kid "${kid}"` : "JWKS has several keys and the signature names no kid");
147
+ }
148
+ /**
149
+ * Load an Ed25519 public key from JWK, JWKS (selected by `kid`), PEM, Base64 SPKI, or Base64url raw public key.
150
+ */
151
+ export async function loadPublicKey(raw, kid) {
152
+ if (typeof raw === "object" && raw && "keys" in raw) {
153
+ return loadPublicKey(selectJwk(raw, kid));
154
+ }
137
155
  if (typeof raw === "object" && "type" in raw && raw.type === "public") {
138
156
  const exported = (await globalThis.crypto.subtle.exportKey("jwk", raw));
139
157
  const kid = exported.kid || (await calculateJwkThumbprint({ crv: "Ed25519", kty: "OKP", x: exported.x }));
@@ -170,16 +188,19 @@ export async function loadPublicKey(raw) {
170
188
  const trimmed = raw.trim();
171
189
  // Try parsing as JSON JWK or JWKS
172
190
  if (trimmed.startsWith("{")) {
191
+ let parsed;
173
192
  try {
174
- const parsed = JSON.parse(trimmed);
175
- const jwkCandidate = parsed.keys ? parsed.keys[0] : parsed;
176
- if (jwkCandidate && jwkCandidate.kty === "OKP" && jwkCandidate.crv === "Ed25519") {
177
- return loadPublicKey(jwkCandidate);
178
- }
193
+ parsed = JSON.parse(trimmed);
179
194
  }
180
195
  catch {
181
196
  // Continue
182
197
  }
198
+ if (parsed && typeof parsed === "object") {
199
+ const jwkCandidate = parsed.keys ? selectJwk(parsed, kid) : parsed;
200
+ if (jwkCandidate.kty === "OKP" && jwkCandidate.crv === "Ed25519") {
201
+ return loadPublicKey(jwkCandidate);
202
+ }
203
+ }
183
204
  }
184
205
  // Handle PEM / Base64 / Base64url
185
206
  let cleaned = trimmed
@@ -1,38 +1,57 @@
1
- import type { PublicJwk, SignedHeaders, SignRequestParams, VerifySignatureParams, VerifySignatureResult } from "../types.js";
1
+ import type { PublicJwk, PublicJwks, ReplayStore, SignedHeaders, SignRequestParams, VerifySignatureParams, VerifySignatureResult } from "../types.js";
2
2
  export declare const SIGNATURE_VERSION = "rfc9421";
3
3
  export declare const SIGNATURE_LABEL = "sig1";
4
4
  export declare const SIGNATURE_ALG = "ed25519";
5
5
  export declare const MAX_CLOCK_SKEW_SECONDS = 300;
6
6
  export declare const SAFE_IDENTIFIER_PATTERN: RegExp;
7
7
  /**
8
- * Normalizes HTTP request path per RFC 3986 to prevent path traversal & signature confusion.
8
+ * Splits a request target into its RFC 9421 "@path" value (§2.2.6; dot segments resolved and repeated
9
+ * slashes collapsed per RFC 3986, to prevent path traversal & signature confusion) and its "@query" value
10
+ * (§2.2.7; the query with its leading "?", or "?" alone when there is none). Fragments are dropped.
11
+ */
12
+ export declare function splitRequestTarget(rawTarget: string): {
13
+ path: string;
14
+ query: string;
15
+ };
16
+ /**
17
+ * Normalizes an HTTP request path per RFC 3986 into the "@path" component. Path only: the query string is
18
+ * a separate "@query" component (see `splitRequestTarget`).
9
19
  */
10
20
  export declare function normalizePath(rawPath: string): string;
11
21
  /**
12
22
  * Validates that an identifier (keyId, serviceSlug, etc.) contains only safe alphanumeric/hyphen characters.
13
23
  */
14
24
  export declare function assertSafeIdentifier(name: string, value: string): void;
15
- /** Upper bound on remembered signatures; the oldest entries are evicted past this. */
16
- export declare const REPLAY_CACHE_MAX_ENTRIES = 10000;
25
+ /**
26
+ * Upper bound on remembered signatures. Unexpired entries are never evicted: once the cache is full of them,
27
+ * new signatures are rejected (fail closed) until entries expire. Entries live 2 x maxClockSkewSeconds
28
+ * (600s by default), so the default sustains ~160 verified requests/s per process.
29
+ */
30
+ export declare const REPLAY_CACHE_MAX_ENTRIES = 100000;
17
31
  /**
18
32
  * In-memory replay cache, keyed on keyid + decoded signature bytes (so re-encodings of the
19
- * same signature collide). Insertion-ordered, so eviction drops the oldest entries first.
33
+ * same signature collide). Per process only: multi-instance deployments need a shared `ReplayStore`.
20
34
  */
21
- export declare class SignatureReplayCache {
35
+ export declare class SignatureReplayCache implements ReplayStore {
22
36
  private readonly maxEntries;
23
37
  private seen;
24
38
  constructor(maxEntries?: number);
25
39
  get size(): number;
26
40
  /** True when the key was recorded and has not expired yet (a replay). */
27
41
  has(key: string, now?: number): boolean;
28
- record(key: string, ttlMs: number, now?: number): void;
42
+ /**
43
+ * Records `key` until `now + ttlMs`. Returns false, recording nothing, when the key is already present
44
+ * or the cache is full of unexpired entries (a still-valid signature is never forgotten early).
45
+ */
46
+ record(key: string, ttlMs: number, now?: number): boolean;
29
47
  clear(): void;
30
48
  }
31
49
  export declare const globalReplayCache: SignatureReplayCache;
32
- export type { SignedHeaders, SignRequestParams, VerifySignatureParams, VerifySignatureResult, };
50
+ export type { ReplayStore, SignedHeaders, SignRequestParams, VerifySignatureParams, VerifySignatureResult, };
33
51
  /**
34
52
  * Format the RFC 9421 Signature-Input parameters string.
35
53
  * Example: `("@method" "@path" "@authority" "content-digest");created=1791561600;keyid="...";alg="ed25519";nonce="..."`
54
+ * Every value is validated, so none can close its quoted string and inject parameters.
36
55
  */
37
56
  export declare function formatSignatureParams(params: {
38
57
  components?: string[];
@@ -42,7 +61,10 @@ export declare function formatSignatureParams(params: {
42
61
  nonce?: string;
43
62
  }): string;
44
63
  /**
45
- * Construct canonical RFC 9421 signature base string.
64
+ * Construct canonical RFC 9421 signature base string from the covered components, in order. They are
65
+ * `components` when given, else the list declared in `signatureParams`. "@path" and "@query" come from
66
+ * `path` (which may carry a query string); "content-digest" from `contentDigest`; any other header field
67
+ * from `additionalComponents`. Throws when a covered component has no value.
46
68
  */
47
69
  export declare function createSignatureBase(params: {
48
70
  method: string;
@@ -51,13 +73,19 @@ export declare function createSignatureBase(params: {
51
73
  contentDigest?: string | null;
52
74
  signatureParams: string;
53
75
  additionalComponents?: Record<string, string>;
76
+ components?: string[];
54
77
  }): string;
55
78
  /**
56
79
  * Sign an outgoing HTTP request according to RFC 9421 + RFC 9530.
80
+ * Covers "@method", "@path", "@query" (only when `path` has a query string), "@authority" and
81
+ * "content-digest" (whenever there is a body, and always for POST / PUT / PATCH).
57
82
  */
58
83
  export declare function signRequest(params: SignRequestParams): Promise<SignedHeaders>;
59
84
  /**
60
- * Verify an incoming RFC 9421 HTTP Message Signature.
85
+ * Verify an incoming RFC 9421 HTTP Message Signature. The signature base is built from the components the
86
+ * signature declares, which must include "@method", "@path" and "@authority", plus "@query" when the request
87
+ * has a query string and "content-digest" when it has a body or is a POST / PUT / PATCH.
88
+ * Replay protection uses `replayStore` (default: per-process memory; use a shared store across instances).
61
89
  */
62
90
  export declare function verifyRequestSignature(params: VerifySignatureParams): Promise<VerifySignatureResult>;
63
91
  /** Clock skew tolerated on JWT lease `exp` / `nbf`. */
@@ -65,11 +93,13 @@ export declare const JWT_CLOCK_SKEW_SECONDS = 60;
65
93
  /**
66
94
  * Zero-egress offline verification for asymmetric JWT token leases.
67
95
  * Requires a numeric `exp` and an audience (`aud` or `serviceSlug`) equal to `expectedServiceSlug`;
68
- * a verifier without an expected slug rejects every lease. `iss` is checked when `expectedIssuer` is set.
96
+ * a verifier without an expected slug rejects every lease. `iss` is checked when `expectedIssuer` is set
97
+ * (Daywalker documents no fixed issuer, so there is no default). Segments must be canonical unpadded
98
+ * base64url, a `crit` header is rejected, and a JWKS `publicKey` is searched by the header's `kid`.
69
99
  */
70
100
  export declare function verifyJwtLease(params: {
71
101
  token: string;
72
- publicKey: string | PublicJwk | CryptoKey;
102
+ publicKey: string | PublicJwk | PublicJwks | CryptoKey;
73
103
  expectedServiceSlug?: string;
74
104
  expectedIssuer?: string;
75
105
  currentTime?: number;