@andco/sdk 0.0.2 → 0.0.4

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 (83) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +96 -54
  3. package/dist/auth.d.ts +8 -3
  4. package/dist/auth.d.ts.map +1 -1
  5. package/dist/auth.js +32 -23
  6. package/dist/browser/controller.d.ts +55 -1
  7. package/dist/browser/controller.d.ts.map +1 -1
  8. package/dist/browser/controller.js +166 -9
  9. package/dist/browser/frame.d.ts +4 -3
  10. package/dist/browser/frame.d.ts.map +1 -1
  11. package/dist/browser/frame.js +12 -20
  12. package/dist/browser/index.d.ts +18 -6
  13. package/dist/browser/index.d.ts.map +1 -1
  14. package/dist/browser/index.js +64 -29
  15. package/dist/browser/popup.d.ts +6 -1
  16. package/dist/browser/popup.d.ts.map +1 -1
  17. package/dist/browser/popup.js +37 -8
  18. package/dist/cli/index.d.ts +4 -2
  19. package/dist/cli/index.d.ts.map +1 -1
  20. package/dist/cli/index.js +10 -3
  21. package/dist/cli/server.d.ts +5 -0
  22. package/dist/cli/server.d.ts.map +1 -1
  23. package/dist/cli/server.js +5 -0
  24. package/dist/client.d.ts +66 -19
  25. package/dist/client.d.ts.map +1 -1
  26. package/dist/client.js +125 -74
  27. package/dist/config.d.ts +2 -1
  28. package/dist/config.d.ts.map +1 -1
  29. package/dist/config.js +1 -0
  30. package/dist/credentials.d.ts +58 -4
  31. package/dist/credentials.d.ts.map +1 -1
  32. package/dist/credentials.js +0 -0
  33. package/dist/errors.d.ts +23 -21
  34. package/dist/errors.d.ts.map +1 -1
  35. package/dist/errors.js +18 -20
  36. package/dist/globals.d.ts +25 -0
  37. package/dist/globals.d.ts.map +1 -0
  38. package/dist/globals.js +15 -0
  39. package/dist/grants-api.d.ts +34 -0
  40. package/dist/grants-api.d.ts.map +1 -0
  41. package/dist/grants-api.js +48 -0
  42. package/dist/grants.d.ts +16 -0
  43. package/dist/grants.d.ts.map +1 -0
  44. package/dist/grants.js +13 -0
  45. package/dist/index.d.ts +12 -6
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +8 -2
  48. package/dist/inflight.d.ts +31 -0
  49. package/dist/inflight.d.ts.map +1 -0
  50. package/dist/inflight.js +26 -0
  51. package/dist/intents.d.ts +386 -40
  52. package/dist/intents.d.ts.map +1 -1
  53. package/dist/intents.js +712 -53
  54. package/dist/oauth.d.ts +55 -6
  55. package/dist/oauth.d.ts.map +1 -1
  56. package/dist/oauth.js +86 -57
  57. package/dist/presenter.d.ts +59 -11
  58. package/dist/presenter.d.ts.map +1 -1
  59. package/dist/presenter.js +40 -1
  60. package/dist/resource.d.ts +109 -0
  61. package/dist/resource.d.ts.map +1 -0
  62. package/dist/resource.js +151 -0
  63. package/dist/rest.d.ts +23 -4
  64. package/dist/rest.d.ts.map +1 -1
  65. package/dist/rest.js +46 -5
  66. package/dist/server/index.d.ts +3 -0
  67. package/dist/server/index.d.ts.map +1 -1
  68. package/dist/server/index.js +1 -0
  69. package/dist/server-metadata.generated.d.ts.map +1 -1
  70. package/dist/server-metadata.generated.js +8 -4
  71. package/dist/service.d.ts +109 -0
  72. package/dist/service.d.ts.map +1 -0
  73. package/dist/service.js +241 -0
  74. package/dist/session-store.d.ts +12 -4
  75. package/dist/session-store.d.ts.map +1 -1
  76. package/dist/session-store.js +64 -14
  77. package/dist/storage.d.ts +16 -23
  78. package/dist/storage.d.ts.map +1 -1
  79. package/dist/storage.js +27 -25
  80. package/dist/tokens.d.ts +46 -0
  81. package/dist/tokens.d.ts.map +1 -0
  82. package/dist/tokens.js +148 -0
  83. package/package.json +13 -3
package/dist/tokens.js ADDED
@@ -0,0 +1,148 @@
1
+ import { decodeJwtPayload } from "@andco/protocol";
2
+ import { AndcoError, Result } from "./errors.js";
3
+ import { andcoServerMetadata } from "./server-metadata.generated.js";
4
+ const ALGORITHMS = {
5
+ RS256: {
6
+ import: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
7
+ verify: { name: "RSASSA-PKCS1-v1_5" },
8
+ },
9
+ ES256: {
10
+ import: { name: "ECDSA", namedCurve: "P-256" },
11
+ verify: { name: "ECDSA", hash: "SHA-256" },
12
+ },
13
+ };
14
+ /** Minimum time between two JWKS downloads triggered by an unknown `kid`, so a bad token cannot hammer the server. */
15
+ const JWKS_REFRESH_COOLDOWN_MS = 10_000;
16
+ /**
17
+ * Offline verification of Andco access tokens with the Authorization Server's JWKS.
18
+ *
19
+ * The JWKS URL comes from the baked Server Description, re-rooted on the configured issuer, and
20
+ * keys are cached by `kid`. An unknown `kid` triggers one (rate limited) refetch, which is how a key
21
+ * rotation is picked up without a restart. A valid signature proves who issued the token, not that
22
+ * the Grant behind it is still alive: use `grants.get` for that.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const { data: claims, error } = await andco.tokens.verify(bearer, { audience: "https://api.casa-norte.example" });
27
+ * if (error) return new Response(null, { status: 401 });
28
+ * ```
29
+ */
30
+ export class AndcoTokens {
31
+ #config;
32
+ #globals;
33
+ #keys = new Map();
34
+ #fetchedAt = 0;
35
+ #loading = null;
36
+ constructor(options) {
37
+ this.#config = options.config;
38
+ this.#globals = options.globals;
39
+ }
40
+ /** The `iss` the Authorization Server signs: the configured base URL without a trailing slash. */
41
+ get issuer() {
42
+ const base = this.#config.endpoints.auth;
43
+ return `${base.origin}${base.pathname.replace(/\/+$/, "")}`;
44
+ }
45
+ /** Checks signature, `exp`, `iss` and, when given, `aud`. Never throws. */
46
+ async verify(token, options = {}) {
47
+ const fail = (code, message, cause) => {
48
+ console.debug("[tokens.verify] rejected %s: %s", code, message);
49
+ return Result.fail(new AndcoError(code, { message, cause }));
50
+ };
51
+ try {
52
+ const parts = token.split(".");
53
+ const header = parts.length === 3 ? parseHeader(parts[0]) : null;
54
+ if (!header)
55
+ return fail("invalid_token", "token is not a JWT");
56
+ const algorithm = header.alg;
57
+ if (typeof header.alg !== "string" || !Object.hasOwn(ALGORITHMS, header.alg)) {
58
+ return fail("invalid_token", `unsupported signing algorithm ${String(header.alg)}`);
59
+ }
60
+ const signing = await this.#keyFor(typeof header.kid === "string" ? header.kid : undefined, algorithm);
61
+ if (!signing)
62
+ return fail("invalid_token", "no JWKS key matches the token kid");
63
+ const valid = await globalThis.crypto.subtle.verify(ALGORITHMS[algorithm].verify, signing.key, bytes(parts[2]), new TextEncoder().encode(`${parts[0]}.${parts[1]}`));
64
+ if (!valid)
65
+ return fail("invalid_token", "signature does not verify");
66
+ const claims = decodeJwtPayload(token);
67
+ if (!claims)
68
+ return fail("invalid_token", "payload is not a JSON object");
69
+ const tolerance = options.clockToleranceSeconds ?? 30;
70
+ const now = Math.floor(Date.now() / 1000);
71
+ if (typeof claims["exp"] !== "number")
72
+ return fail("invalid_token", "exp claim is missing");
73
+ if (claims["exp"] + tolerance <= now)
74
+ return fail("invalid_token", "token is expired");
75
+ if (typeof claims["nbf"] === "number" && claims["nbf"] - tolerance > now) {
76
+ return fail("invalid_token", "token is not yet valid");
77
+ }
78
+ if (claims["iss"] !== this.issuer)
79
+ return fail("invalid_token", "issuer does not match the configured one");
80
+ if (options.audience !== undefined) {
81
+ const aud = claims["aud"];
82
+ const audiences = typeof aud === "string" ? [aud] : Array.isArray(aud) ? aud : [];
83
+ if (!audiences.includes(options.audience))
84
+ return fail("invalid_token", "audience does not match");
85
+ }
86
+ console.debug("[tokens.verify] accepted sub=%s", claims["sub"]);
87
+ return Result.ok(claims);
88
+ }
89
+ catch (cause) {
90
+ return fail("invalid_token", "verification failed", cause);
91
+ }
92
+ }
93
+ async #keyFor(kid, algorithm) {
94
+ const lookup = () => (kid === undefined ? this.#onlyKey() : this.#keys.get(kid));
95
+ const cached = lookup();
96
+ if (cached)
97
+ return cached;
98
+ if (Date.now() - this.#fetchedAt < JWKS_REFRESH_COOLDOWN_MS && this.#fetchedAt > 0)
99
+ return null;
100
+ this.#loading ??= this.#refresh(algorithm).finally(() => {
101
+ this.#loading = null;
102
+ });
103
+ await this.#loading;
104
+ return lookup() ?? null;
105
+ }
106
+ #onlyKey() {
107
+ return this.#keys.size === 1 ? [...this.#keys.values()][0] : undefined;
108
+ }
109
+ async #refresh(algorithm) {
110
+ this.#fetchedAt = Date.now();
111
+ const uri = andcoServerMetadata(this.issuer).jwks_uri;
112
+ console.debug("[tokens.refresh] fetching JWKS %s", uri);
113
+ const response = await this.#globals.fetch(uri, { headers: { Accept: "application/json" } });
114
+ if (!response.ok)
115
+ throw new AndcoError("jwks_unavailable", { message: `JWKS answered ${response.status}` });
116
+ const body = (await response.json());
117
+ const next = new Map();
118
+ for (const jwk of body.keys ?? []) {
119
+ if (!jwk.kid || (jwk.use && jwk.use !== "sig"))
120
+ continue;
121
+ const alg = (jwk.alg ?? (jwk.kty === "EC" ? "ES256" : "RS256"));
122
+ if (!Object.hasOwn(ALGORITHMS, alg))
123
+ continue;
124
+ const key = await globalThis.crypto.subtle.importKey("jwk", jwk, ALGORITHMS[alg].import, false, ["verify"]);
125
+ next.set(jwk.kid, { key, algorithm: ALGORITHMS[alg].import });
126
+ }
127
+ this.#keys.clear();
128
+ for (const [kid, key] of next)
129
+ this.#keys.set(kid, key);
130
+ console.debug("[tokens.refresh] cached kids %o for %s", [...next.keys()], algorithm);
131
+ }
132
+ }
133
+ function bytes(segment) {
134
+ const base64 = segment
135
+ .replace(/-/g, "+")
136
+ .replace(/_/g, "/")
137
+ .padEnd(Math.ceil(segment.length / 4) * 4, "=");
138
+ return Uint8Array.from(atob(base64), (char) => char.charCodeAt(0));
139
+ }
140
+ function parseHeader(segment) {
141
+ try {
142
+ const header = JSON.parse(new TextDecoder().decode(bytes(segment)));
143
+ return header && typeof header === "object" && !Array.isArray(header) ? header : null;
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andco/sdk",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -33,8 +33,8 @@
33
33
  "dist"
34
34
  ],
35
35
  "dependencies": {
36
- "@andco/openapi-fetch": "0.0.1",
37
- "@andco/protocol": "0.0.1",
36
+ "@andco/openapi-fetch": "0.0.4",
37
+ "@andco/protocol": "0.0.4",
38
38
  "openid-client": "^6.8.8"
39
39
  },
40
40
  "devDependencies": {
@@ -43,6 +43,16 @@
43
43
  },
44
44
  "license": "Apache-2.0",
45
45
  "description": "Andco SDK: OAuth authorization, sessions and API access for browser and server.",
46
+ "author": "Patricio Lopez Juri <patricio.lopez@haulmer.com>",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/haulmer/andco-sdk-js.git",
50
+ "directory": "packages/sdk"
51
+ },
52
+ "homepage": "https://github.com/haulmer/andco-sdk-js#readme",
53
+ "bugs": {
54
+ "url": "https://github.com/haulmer/andco-sdk-js/issues"
55
+ },
46
56
  "scripts": {
47
57
  "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
48
58
  "build": "pnpm run clean && tsc",