rastack 0.0.52 → 0.0.54

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.
@@ -0,0 +1,281 @@
1
+ /**
2
+ * The Cognito auth core (`rastack/auth`): PKCE (S256, against the RFC 7636 test
3
+ * vector), Hosted UI URL builders, the `/oauth2/token` request body, JWT
4
+ * decoding, session shaping + expiry, redirect parsing, and the server-less
5
+ * local session.
6
+ */
7
+
8
+ import {
9
+ authorizeUrl,
10
+ base64UrlDecode,
11
+ base64UrlDecodeToBytes,
12
+ base64UrlEncode,
13
+ codeChallengeFromVerifier,
14
+ createPkcePair,
15
+ decodeJwt,
16
+ hostedUiOrigin,
17
+ isSessionValid,
18
+ issuerUrl,
19
+ localSession,
20
+ logoutUrl,
21
+ parseRedirect,
22
+ randomCodeVerifier,
23
+ sessionFromTokens,
24
+ tokenEndpoint,
25
+ tokenRequestBody,
26
+ userFromClaims,
27
+ type CognitoAuthConfig,
28
+ type CognitoTokenResponse,
29
+ } from "../src/auth/cognito";
30
+ import { cognitoToRastackAuth } from "../auth/provider";
31
+
32
+ const CONFIG: CognitoAuthConfig = {
33
+ domain: "myapp.auth.us-east-1.amazoncognito.com",
34
+ clientId: "abc123clientid",
35
+ redirectUri: "https://app.example.com/callback",
36
+ region: "us-east-1",
37
+ userPoolId: "us-east-1_Pool",
38
+ };
39
+
40
+ /** Build a signed-shaped JWT (unsigned) carrying the given payload. */
41
+ function makeJwt(payload: Record<string, unknown>): string {
42
+ const seg = (v: unknown) =>
43
+ base64UrlEncode(new TextEncoder().encode(JSON.stringify(v)));
44
+ return `${seg({ alg: "RS256", typ: "JWT" })}.${seg(payload)}.sig`;
45
+ }
46
+
47
+ describe("base64url", () => {
48
+ it("round-trips bytes and strips padding", () => {
49
+ const bytes = new Uint8Array([0, 1, 2, 250, 251, 252, 253]);
50
+ const encoded = base64UrlEncode(bytes);
51
+ expect(encoded).not.toMatch(/[+/=]/);
52
+ expect([...base64UrlDecodeToBytes(encoded)]).toEqual([...bytes]);
53
+ });
54
+
55
+ it("decodes a UTF-8 payload", () => {
56
+ const encoded = base64UrlEncode(new TextEncoder().encode('{"é":"✓"}'));
57
+ expect(base64UrlDecode(encoded)).toBe('{"é":"✓"}');
58
+ });
59
+ });
60
+
61
+ describe("PKCE", () => {
62
+ it("derives the S256 challenge from the RFC 7636 test vector", async () => {
63
+ const verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
64
+ const challenge = await codeChallengeFromVerifier(verifier);
65
+ expect(challenge).toBe("E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM");
66
+ });
67
+
68
+ it("generates a url-safe verifier of the expected length", () => {
69
+ const verifier = randomCodeVerifier(new Uint8Array(32).fill(7));
70
+ expect(verifier).not.toMatch(/[+/=]/);
71
+ expect(verifier.length).toBeGreaterThanOrEqual(43);
72
+ });
73
+
74
+ it("creates a matching verifier/challenge pair", async () => {
75
+ const bytes = new Uint8Array(32).map((_, i) => i);
76
+ const { verifier, challenge } = await createPkcePair(bytes);
77
+ expect(verifier).toBe(randomCodeVerifier(bytes));
78
+ expect(challenge).toBe(await codeChallengeFromVerifier(verifier));
79
+ });
80
+ });
81
+
82
+ describe("Hosted UI URLs", () => {
83
+ it("builds the authorize URL with PKCE + default scopes", () => {
84
+ const url = new URL(
85
+ authorizeUrl(CONFIG, { state: "st8", codeChallenge: "chal" }),
86
+ );
87
+ expect(url.origin + url.pathname).toBe(
88
+ "https://myapp.auth.us-east-1.amazoncognito.com/oauth2/authorize",
89
+ );
90
+ expect(url.searchParams.get("response_type")).toBe("code");
91
+ expect(url.searchParams.get("client_id")).toBe("abc123clientid");
92
+ expect(url.searchParams.get("redirect_uri")).toBe(
93
+ "https://app.example.com/callback",
94
+ );
95
+ expect(url.searchParams.get("scope")).toBe("openid email profile");
96
+ expect(url.searchParams.get("code_challenge")).toBe("chal");
97
+ expect(url.searchParams.get("code_challenge_method")).toBe("S256");
98
+ expect(url.searchParams.get("state")).toBe("st8");
99
+ });
100
+
101
+ it("honours custom scopes and strips a scheme from the domain", () => {
102
+ const url = new URL(
103
+ authorizeUrl(
104
+ { ...CONFIG, domain: "https://auth.example.com/", scopes: ["openid"] },
105
+ { state: "s", codeChallenge: "c" },
106
+ ),
107
+ );
108
+ expect(url.origin).toBe("https://auth.example.com");
109
+ expect(url.searchParams.get("scope")).toBe("openid");
110
+ });
111
+
112
+ it("builds the logout URL, defaulting logout_uri to redirect_uri", () => {
113
+ const url = new URL(logoutUrl(CONFIG));
114
+ expect(url.pathname).toBe("/logout");
115
+ expect(url.searchParams.get("logout_uri")).toBe(
116
+ "https://app.example.com/callback",
117
+ );
118
+ });
119
+
120
+ it("derives the origin, token endpoint and issuer", () => {
121
+ expect(hostedUiOrigin(CONFIG)).toBe(
122
+ "https://myapp.auth.us-east-1.amazoncognito.com",
123
+ );
124
+ expect(tokenEndpoint(CONFIG)).toBe(
125
+ "https://myapp.auth.us-east-1.amazoncognito.com/oauth2/token",
126
+ );
127
+ expect(issuerUrl(CONFIG)).toBe(
128
+ "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Pool",
129
+ );
130
+ });
131
+ });
132
+
133
+ describe("token request body", () => {
134
+ it("encodes an authorization_code grant with the verifier", () => {
135
+ const body = new URLSearchParams(
136
+ tokenRequestBody(CONFIG, {
137
+ grantType: "authorization_code",
138
+ code: "the-code",
139
+ codeVerifier: "the-verifier",
140
+ }),
141
+ );
142
+ expect(body.get("grant_type")).toBe("authorization_code");
143
+ expect(body.get("client_id")).toBe("abc123clientid");
144
+ expect(body.get("code")).toBe("the-code");
145
+ expect(body.get("redirect_uri")).toBe("https://app.example.com/callback");
146
+ expect(body.get("code_verifier")).toBe("the-verifier");
147
+ });
148
+
149
+ it("encodes a refresh_token grant", () => {
150
+ const body = new URLSearchParams(
151
+ tokenRequestBody(CONFIG, {
152
+ grantType: "refresh_token",
153
+ refreshToken: "rt",
154
+ }),
155
+ );
156
+ expect(body.get("grant_type")).toBe("refresh_token");
157
+ expect(body.get("refresh_token")).toBe("rt");
158
+ expect(body.has("code")).toBe(false);
159
+ });
160
+ });
161
+
162
+ describe("JWT + claims", () => {
163
+ it("decodes a JWT payload without verifying", () => {
164
+ const token = makeJwt({ sub: "user-1", email: "a@b.co" });
165
+ expect(decodeJwt(token)).toMatchObject({ sub: "user-1", email: "a@b.co" });
166
+ });
167
+
168
+ it("throws on a malformed token", () => {
169
+ expect(() => decodeJwt("not-a-jwt")).toThrow(/Malformed JWT/);
170
+ });
171
+
172
+ it("projects cognito:groups into the user (empty when absent)", () => {
173
+ expect(
174
+ userFromClaims({ sub: "s", "cognito:groups": ["admin", "ops"] }),
175
+ ).toEqual({ sub: "s", email: undefined, username: "s", groups: ["admin", "ops"] });
176
+ expect(userFromClaims({ sub: "s", email: "e@x.co" }).groups).toEqual([]);
177
+ });
178
+ });
179
+
180
+ describe("sessions", () => {
181
+ const now = 1_000_000_000_000;
182
+
183
+ it("builds a session from tokens, using the token's own exp", () => {
184
+ const idToken = makeJwt({
185
+ sub: "u9",
186
+ exp: Math.floor(now / 1000) + 3600,
187
+ "cognito:groups": ["pilots"],
188
+ email: "pilot@sky.co",
189
+ });
190
+ const tokens: CognitoTokenResponse = {
191
+ id_token: idToken,
192
+ access_token: makeJwt({ sub: "u9" }),
193
+ refresh_token: "rt",
194
+ expires_in: 3600,
195
+ };
196
+ const session = sessionFromTokens(tokens, now);
197
+ expect(session.user).toMatchObject({ sub: "u9", groups: ["pilots"] });
198
+ expect(session.expiresAt).toBe((Math.floor(now / 1000) + 3600) * 1000);
199
+ expect(session.refreshToken).toBe("rt");
200
+ });
201
+
202
+ it("validates against expiry with skew", () => {
203
+ const session = sessionFromTokens(
204
+ {
205
+ id_token: makeJwt({ sub: "u", exp: Math.floor(now / 1000) + 3600 }),
206
+ access_token: "a",
207
+ },
208
+ now,
209
+ );
210
+ expect(isSessionValid(session, now)).toBe(true);
211
+ expect(isSessionValid(session, session.expiresAt - 10_000, 30_000)).toBe(false);
212
+ expect(isSessionValid(null, now)).toBe(false);
213
+ });
214
+ });
215
+
216
+ describe("redirect parsing", () => {
217
+ it("parses code + state from a search string or full URL", () => {
218
+ expect(parseRedirect("?code=abc&state=xyz")).toMatchObject({
219
+ code: "abc",
220
+ state: "xyz",
221
+ });
222
+ expect(
223
+ parseRedirect("https://app.example.com/callback?code=c1&state=s1"),
224
+ ).toMatchObject({ code: "c1", state: "s1" });
225
+ });
226
+
227
+ it("surfaces an OAuth error", () => {
228
+ const r = parseRedirect("?error=access_denied&error_description=nope");
229
+ expect(r.error).toBe("access_denied");
230
+ expect(r.errorDescription).toBe("nope");
231
+ expect(r.code).toBeUndefined();
232
+ });
233
+ });
234
+
235
+ describe("local (server-less) session", () => {
236
+ const now = 2_000_000_000_000;
237
+
238
+ it("mints a decodable, valid session with groups", () => {
239
+ const session = localSession(
240
+ { sub: "ops-lead", email: "lead@sky.co", groups: ["admins"] },
241
+ now,
242
+ );
243
+ expect(session.user).toMatchObject({
244
+ sub: "ops-lead",
245
+ email: "lead@sky.co",
246
+ groups: ["admins"],
247
+ });
248
+ expect(isSessionValid(session, now)).toBe(true);
249
+ // The minted token is a real-shaped JWT the same decoder reads back.
250
+ expect(decodeJwt(session.idToken)).toMatchObject({ sub: "ops-lead" });
251
+ });
252
+
253
+ it("defaults the subject and carries no groups", () => {
254
+ const session = localSession({}, now);
255
+ expect(session.user.sub).toBe("local-dev");
256
+ expect(session.user.groups).toEqual([]);
257
+ });
258
+ });
259
+
260
+ describe("bridge to the authorization half (RastackAuth)", () => {
261
+ const now = 2_000_000_000_000;
262
+
263
+ it("maps a signed-in session's claims into a RastackAuth", () => {
264
+ const session = localSession({ sub: "u1", groups: ["admins"] }, now);
265
+ const auth = cognitoToRastackAuth({
266
+ isAuthenticated: true,
267
+ claims: session.claims,
268
+ });
269
+ expect(auth).toEqual({
270
+ authenticated: true,
271
+ sub: "u1",
272
+ groups: ["admins"],
273
+ });
274
+ });
275
+
276
+ it("maps a signed-out state to an anonymous identity", () => {
277
+ expect(cognitoToRastackAuth({ isAuthenticated: false, claims: null })).toEqual(
278
+ { authenticated: false, groups: [] },
279
+ );
280
+ });
281
+ });
@@ -65,8 +65,8 @@ export interface InitOutput {
65
65
  readonly rust_zstd_wasm_shim_memmove: (a: number, b: number, c: number) => number;
66
66
  readonly rust_zstd_wasm_shim_memset: (a: number, b: number, c: number) => number;
67
67
  readonly rust_zstd_wasm_shim_qsort: (a: number, b: number, c: number, d: number) => void;
68
- readonly wasm_bindgen_1a7aa9db5392c486___convert__closures_____invoke___wasm_bindgen_1a7aa9db5392c486___JsValue__core_7d5f0a2ba6a62c33___result__Result_____wasm_bindgen_1a7aa9db5392c486___JsError___true_: (a: number, b: number, c: any) => [number, number];
69
- readonly wasm_bindgen_1a7aa9db5392c486___convert__closures_____invoke___js_sys_191ff9fb2bd47800___Function_fn_wasm_bindgen_1a7aa9db5392c486___JsValue_____wasm_bindgen_1a7aa9db5392c486___sys__Undefined___js_sys_191ff9fb2bd47800___Function_fn_wasm_bindgen_1a7aa9db5392c486___JsValue_____wasm_bindgen_1a7aa9db5392c486___sys__Undefined_______true_: (a: number, b: number, c: any, d: any) => void;
68
+ readonly wasm_bindgen_ac0f9960a408a037___convert__closures_____invoke___wasm_bindgen_ac0f9960a408a037___JsValue__core_9b3796e30d99ddb7___result__Result_____wasm_bindgen_ac0f9960a408a037___JsError___true_: (a: number, b: number, c: any) => [number, number];
69
+ readonly wasm_bindgen_ac0f9960a408a037___convert__closures_____invoke___js_sys_10c47e7ecd0a94ab___Function_fn_wasm_bindgen_ac0f9960a408a037___JsValue_____wasm_bindgen_ac0f9960a408a037___sys__Undefined___js_sys_10c47e7ecd0a94ab___Function_fn_wasm_bindgen_ac0f9960a408a037___JsValue_____wasm_bindgen_ac0f9960a408a037___sys__Undefined_______true_: (a: number, b: number, c: any, d: any) => void;
70
70
  readonly __wbindgen_malloc: (a: number, b: number) => number;
71
71
  readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
72
72
  readonly __wbindgen_exn_store: (a: number) => void;
@@ -303,7 +303,7 @@ function __wbg_get_imports() {
303
303
  const a = state0.a;
304
304
  state0.a = 0;
305
305
  try {
306
- return wasm_bindgen_1a7aa9db5392c486___convert__closures_____invoke___js_sys_191ff9fb2bd47800___Function_fn_wasm_bindgen_1a7aa9db5392c486___JsValue_____wasm_bindgen_1a7aa9db5392c486___sys__Undefined___js_sys_191ff9fb2bd47800___Function_fn_wasm_bindgen_1a7aa9db5392c486___JsValue_____wasm_bindgen_1a7aa9db5392c486___sys__Undefined_______true_(a, state0.b, arg0, arg1);
306
+ return wasm_bindgen_ac0f9960a408a037___convert__closures_____invoke___js_sys_10c47e7ecd0a94ab___Function_fn_wasm_bindgen_ac0f9960a408a037___JsValue_____wasm_bindgen_ac0f9960a408a037___sys__Undefined___js_sys_10c47e7ecd0a94ab___Function_fn_wasm_bindgen_ac0f9960a408a037___JsValue_____wasm_bindgen_ac0f9960a408a037___sys__Undefined_______true_(a, state0.b, arg0, arg1);
307
307
  } finally {
308
308
  state0.a = a;
309
309
  }
@@ -376,7 +376,7 @@ function __wbg_get_imports() {
376
376
  },
377
377
  __wbindgen_cast_0000000000000001: function(arg0, arg1) {
378
378
  // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 83, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
379
- const ret = makeMutClosure(arg0, arg1, wasm_bindgen_1a7aa9db5392c486___convert__closures_____invoke___wasm_bindgen_1a7aa9db5392c486___JsValue__core_7d5f0a2ba6a62c33___result__Result_____wasm_bindgen_1a7aa9db5392c486___JsError___true_);
379
+ const ret = makeMutClosure(arg0, arg1, wasm_bindgen_ac0f9960a408a037___convert__closures_____invoke___wasm_bindgen_ac0f9960a408a037___JsValue__core_9b3796e30d99ddb7___result__Result_____wasm_bindgen_ac0f9960a408a037___JsError___true_);
380
380
  return ret;
381
381
  },
382
382
  __wbindgen_cast_0000000000000002: function(arg0) {
@@ -415,15 +415,15 @@ function __wbg_get_imports() {
415
415
  };
416
416
  }
417
417
 
418
- function wasm_bindgen_1a7aa9db5392c486___convert__closures_____invoke___wasm_bindgen_1a7aa9db5392c486___JsValue__core_7d5f0a2ba6a62c33___result__Result_____wasm_bindgen_1a7aa9db5392c486___JsError___true_(arg0, arg1, arg2) {
419
- const ret = wasm.wasm_bindgen_1a7aa9db5392c486___convert__closures_____invoke___wasm_bindgen_1a7aa9db5392c486___JsValue__core_7d5f0a2ba6a62c33___result__Result_____wasm_bindgen_1a7aa9db5392c486___JsError___true_(arg0, arg1, arg2);
418
+ function wasm_bindgen_ac0f9960a408a037___convert__closures_____invoke___wasm_bindgen_ac0f9960a408a037___JsValue__core_9b3796e30d99ddb7___result__Result_____wasm_bindgen_ac0f9960a408a037___JsError___true_(arg0, arg1, arg2) {
419
+ const ret = wasm.wasm_bindgen_ac0f9960a408a037___convert__closures_____invoke___wasm_bindgen_ac0f9960a408a037___JsValue__core_9b3796e30d99ddb7___result__Result_____wasm_bindgen_ac0f9960a408a037___JsError___true_(arg0, arg1, arg2);
420
420
  if (ret[1]) {
421
421
  throw takeFromExternrefTable0(ret[0]);
422
422
  }
423
423
  }
424
424
 
425
- function wasm_bindgen_1a7aa9db5392c486___convert__closures_____invoke___js_sys_191ff9fb2bd47800___Function_fn_wasm_bindgen_1a7aa9db5392c486___JsValue_____wasm_bindgen_1a7aa9db5392c486___sys__Undefined___js_sys_191ff9fb2bd47800___Function_fn_wasm_bindgen_1a7aa9db5392c486___JsValue_____wasm_bindgen_1a7aa9db5392c486___sys__Undefined_______true_(arg0, arg1, arg2, arg3) {
426
- wasm.wasm_bindgen_1a7aa9db5392c486___convert__closures_____invoke___js_sys_191ff9fb2bd47800___Function_fn_wasm_bindgen_1a7aa9db5392c486___JsValue_____wasm_bindgen_1a7aa9db5392c486___sys__Undefined___js_sys_191ff9fb2bd47800___Function_fn_wasm_bindgen_1a7aa9db5392c486___JsValue_____wasm_bindgen_1a7aa9db5392c486___sys__Undefined_______true_(arg0, arg1, arg2, arg3);
425
+ function wasm_bindgen_ac0f9960a408a037___convert__closures_____invoke___js_sys_10c47e7ecd0a94ab___Function_fn_wasm_bindgen_ac0f9960a408a037___JsValue_____wasm_bindgen_ac0f9960a408a037___sys__Undefined___js_sys_10c47e7ecd0a94ab___Function_fn_wasm_bindgen_ac0f9960a408a037___JsValue_____wasm_bindgen_ac0f9960a408a037___sys__Undefined_______true_(arg0, arg1, arg2, arg3) {
426
+ wasm.wasm_bindgen_ac0f9960a408a037___convert__closures_____invoke___js_sys_10c47e7ecd0a94ab___Function_fn_wasm_bindgen_ac0f9960a408a037___JsValue_____wasm_bindgen_ac0f9960a408a037___sys__Undefined___js_sys_10c47e7ecd0a94ab___Function_fn_wasm_bindgen_ac0f9960a408a037___JsValue_____wasm_bindgen_ac0f9960a408a037___sys__Undefined_______true_(arg0, arg1, arg2, arg3);
427
427
  }
428
428
 
429
429
  const RastackApiFinalization = (typeof FinalizationRegistry === 'undefined')
Binary file
@@ -16,8 +16,8 @@ export const rust_zstd_wasm_shim_memcpy: (a: number, b: number, c: number) => nu
16
16
  export const rust_zstd_wasm_shim_memmove: (a: number, b: number, c: number) => number;
17
17
  export const rust_zstd_wasm_shim_memset: (a: number, b: number, c: number) => number;
18
18
  export const rust_zstd_wasm_shim_qsort: (a: number, b: number, c: number, d: number) => void;
19
- export const wasm_bindgen_1a7aa9db5392c486___convert__closures_____invoke___wasm_bindgen_1a7aa9db5392c486___JsValue__core_7d5f0a2ba6a62c33___result__Result_____wasm_bindgen_1a7aa9db5392c486___JsError___true_: (a: number, b: number, c: any) => [number, number];
20
- export const wasm_bindgen_1a7aa9db5392c486___convert__closures_____invoke___js_sys_191ff9fb2bd47800___Function_fn_wasm_bindgen_1a7aa9db5392c486___JsValue_____wasm_bindgen_1a7aa9db5392c486___sys__Undefined___js_sys_191ff9fb2bd47800___Function_fn_wasm_bindgen_1a7aa9db5392c486___JsValue_____wasm_bindgen_1a7aa9db5392c486___sys__Undefined_______true_: (a: number, b: number, c: any, d: any) => void;
19
+ export const wasm_bindgen_ac0f9960a408a037___convert__closures_____invoke___wasm_bindgen_ac0f9960a408a037___JsValue__core_9b3796e30d99ddb7___result__Result_____wasm_bindgen_ac0f9960a408a037___JsError___true_: (a: number, b: number, c: any) => [number, number];
20
+ export const wasm_bindgen_ac0f9960a408a037___convert__closures_____invoke___js_sys_10c47e7ecd0a94ab___Function_fn_wasm_bindgen_ac0f9960a408a037___JsValue_____wasm_bindgen_ac0f9960a408a037___sys__Undefined___js_sys_10c47e7ecd0a94ab___Function_fn_wasm_bindgen_ac0f9960a408a037___JsValue_____wasm_bindgen_ac0f9960a408a037___sys__Undefined_______true_: (a: number, b: number, c: any, d: any) => void;
21
21
  export const __wbindgen_malloc: (a: number, b: number) => number;
22
22
  export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
23
23
  export const __wbindgen_exn_store: (a: number) => void;