@flytedesk/app-kit 3.2.2 → 4.0.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 (70) hide show
  1. package/README.md +157 -10
  2. package/dist/auth/client/httpClient.d.ts +79 -1
  3. package/dist/auth/client/httpClient.js +185 -16
  4. package/dist/auth/client/httpClient.js.map +1 -1
  5. package/dist/auth/client/index.d.ts +4 -4
  6. package/dist/auth/client/index.js +3 -3
  7. package/dist/auth/client/index.js.map +1 -1
  8. package/dist/auth/client/useAuthSession.d.ts +61 -13
  9. package/dist/auth/client/useAuthSession.js +185 -42
  10. package/dist/auth/client/useAuthSession.js.map +1 -1
  11. package/dist/auth/errors.d.ts +30 -7
  12. package/dist/auth/errors.js +38 -8
  13. package/dist/auth/errors.js.map +1 -1
  14. package/dist/auth/guards.d.ts +12 -0
  15. package/dist/auth/guards.js +24 -6
  16. package/dist/auth/guards.js.map +1 -1
  17. package/dist/auth/index.d.ts +4 -4
  18. package/dist/auth/index.js +2 -2
  19. package/dist/auth/index.js.map +1 -1
  20. package/dist/auth/oidc-client.js +12 -2
  21. package/dist/auth/oidc-client.js.map +1 -1
  22. package/dist/auth/plugin.d.ts +6 -0
  23. package/dist/auth/plugin.js +532 -304
  24. package/dist/auth/plugin.js.map +1 -1
  25. package/dist/auth/shared-types.d.ts +28 -1
  26. package/dist/auth/shared-types.js +35 -0
  27. package/dist/auth/shared-types.js.map +1 -1
  28. package/dist/auth/silentAuthPage.d.ts +20 -0
  29. package/dist/auth/silentAuthPage.js +75 -0
  30. package/dist/auth/silentAuthPage.js.map +1 -0
  31. package/dist/auth/testing/fake-idp.d.ts +35 -0
  32. package/dist/auth/testing/fake-idp.js +86 -2
  33. package/dist/auth/testing/fake-idp.js.map +1 -1
  34. package/dist/auth/testing/index.d.ts +3 -1
  35. package/dist/auth/testing/index.js +3 -1
  36. package/dist/auth/testing/index.js.map +1 -1
  37. package/dist/auth/testing/memory-store.d.ts +44 -0
  38. package/dist/auth/testing/memory-store.js +120 -0
  39. package/dist/auth/testing/memory-store.js.map +1 -0
  40. package/dist/auth/tokens.d.ts +24 -0
  41. package/dist/auth/tokens.js +31 -1
  42. package/dist/auth/tokens.js.map +1 -1
  43. package/dist/auth/types.d.ts +231 -96
  44. package/dist/auth/unavailable.d.ts +10 -0
  45. package/dist/auth/unavailable.js +11 -0
  46. package/dist/auth/unavailable.js.map +1 -0
  47. package/dist/bigquery/errors.d.ts +21 -3
  48. package/dist/bigquery/errors.js +66 -12
  49. package/dist/bigquery/errors.js.map +1 -1
  50. package/dist/bigquery/index.d.ts +5 -3
  51. package/dist/bigquery/index.js +4 -2
  52. package/dist/bigquery/index.js.map +1 -1
  53. package/dist/bigquery/query.d.ts +9 -2
  54. package/dist/bigquery/query.js +38 -16
  55. package/dist/bigquery/query.js.map +1 -1
  56. package/dist/bigquery/types.d.ts +45 -1
  57. package/dist/flags/plugin.js +4 -20
  58. package/dist/flags/plugin.js.map +1 -1
  59. package/dist/postgres/advisoryLock.d.ts +58 -0
  60. package/dist/postgres/advisoryLock.js +71 -0
  61. package/dist/postgres/advisoryLock.js.map +1 -0
  62. package/dist/postgres/index.d.ts +6 -0
  63. package/dist/postgres/index.js +7 -0
  64. package/dist/postgres/index.js.map +1 -0
  65. package/dist/profile/plugin.js +3 -20
  66. package/dist/profile/plugin.js.map +1 -1
  67. package/package.json +8 -2
  68. package/scripts/pending-release-count.mjs +0 -37
  69. package/scripts/release.sh +0 -126
  70. package/scripts/release.test.ts +0 -205
@@ -0,0 +1,44 @@
1
+ import type { AuthStore, CreateIdpSessionInput, CreateRefreshTokenInput, IdpSessionLockedStore, IdpSessionRow, RefreshTokenRow, RevokeFamilyReason, UpdateIdpSessionInput } from "../types.js";
2
+ /**
3
+ * An in-memory `AuthStore` that honestly implements every guarantee the contract asks of
4
+ * a real one (AK-23) — for tests of `@flytedesk/app-kit/auth` itself and of any
5
+ * consumer's routes. Not for production: nothing survives a restart and nothing is
6
+ * shared across processes.
7
+ *
8
+ * Atomicity comes from JavaScript's run-to-completion: `rotateRefreshToken` and
9
+ * `revokeFamily` each read, decide and write with no `await` in between, so two
10
+ * same-instant callers can never interleave inside either — which is also what
11
+ * serializes a rotation against a revocation of the same family here (a Postgres store
12
+ * needs a per-family lock for that; see `AuthStore.revokeFamily`). `withIdpSessionLock` is a per-session promise queue; the store
13
+ * it hands `fn` is this store itself, since there is no connection pool to starve.
14
+ */
15
+ export declare class MemoryAuthStore implements AuthStore {
16
+ private readonly refreshTokens;
17
+ private readonly idpSessions;
18
+ private readonly lockTails;
19
+ /** Every `revokeFamily` call, in order — lets a test assert WHY a family was revoked
20
+ * (or that none was) without reaching into the plugin. */
21
+ readonly revokeFamilyCalls: Array<{
22
+ familyId: string;
23
+ reason: RevokeFamilyReason;
24
+ revokedAt: Date;
25
+ }>;
26
+ createRefreshToken(row: CreateRefreshTokenInput): Promise<void>;
27
+ findRefreshTokenByHash(hash: string): Promise<RefreshTokenRow | null>;
28
+ rotateRefreshToken(presentedId: string, successor: CreateRefreshTokenInput, rotatedAt: Date): Promise<boolean>;
29
+ revokeFamily(familyId: string, reason: RevokeFamilyReason, revokedAt: Date): Promise<void>;
30
+ createIdpSession(row: CreateIdpSessionInput): Promise<{
31
+ id: string;
32
+ }>;
33
+ findIdpSession(id: string): Promise<IdpSessionRow | null>;
34
+ updateIdpSession(id: string, patch: UpdateIdpSessionInput): Promise<void>;
35
+ deleteIdpSession(id: string): Promise<void>;
36
+ withIdpSessionLock<T>(sessionId: string, fn: (locked: IdpSessionLockedStore) => Promise<T>): Promise<T>;
37
+ /** Sessions whose lock is currently held or queued for. */
38
+ heldLockCount(): number;
39
+ /** Ids of every IdP session currently stored, oldest first. */
40
+ idpSessionIds(): string[];
41
+ /** Ids of every refresh token whose `replacesId` is `id` — its rotation successors. */
42
+ successorsOf(id: string): string[];
43
+ private insert;
44
+ }
@@ -0,0 +1,120 @@
1
+ import { randomUUID } from "node:crypto";
2
+ /**
3
+ * An in-memory `AuthStore` that honestly implements every guarantee the contract asks of
4
+ * a real one (AK-23) — for tests of `@flytedesk/app-kit/auth` itself and of any
5
+ * consumer's routes. Not for production: nothing survives a restart and nothing is
6
+ * shared across processes.
7
+ *
8
+ * Atomicity comes from JavaScript's run-to-completion: `rotateRefreshToken` and
9
+ * `revokeFamily` each read, decide and write with no `await` in between, so two
10
+ * same-instant callers can never interleave inside either — which is also what
11
+ * serializes a rotation against a revocation of the same family here (a Postgres store
12
+ * needs a per-family lock for that; see `AuthStore.revokeFamily`). `withIdpSessionLock` is a per-session promise queue; the store
13
+ * it hands `fn` is this store itself, since there is no connection pool to starve.
14
+ */
15
+ export class MemoryAuthStore {
16
+ refreshTokens = new Map();
17
+ idpSessions = new Map();
18
+ lockTails = new Map();
19
+ /** Every `revokeFamily` call, in order — lets a test assert WHY a family was revoked
20
+ * (or that none was) without reaching into the plugin. */
21
+ revokeFamilyCalls = [];
22
+ async createRefreshToken(row) {
23
+ this.insert(row, null);
24
+ }
25
+ async findRefreshTokenByHash(hash) {
26
+ for (const row of this.refreshTokens.values()) {
27
+ if (row.tokenHash === hash)
28
+ return toRow(row);
29
+ }
30
+ return null;
31
+ }
32
+ async rotateRefreshToken(presentedId, successor, rotatedAt) {
33
+ const presented = this.refreshTokens.get(presentedId);
34
+ if (!presented || presented.revocation)
35
+ return false;
36
+ presented.revocation = { at: rotatedAt, reason: "rotated" };
37
+ this.insert(successor, presentedId);
38
+ return true;
39
+ }
40
+ async revokeFamily(familyId, reason, revokedAt) {
41
+ this.revokeFamilyCalls.push({ familyId, reason, revokedAt });
42
+ const revocation = { at: revokedAt, reason: reason.type };
43
+ for (const row of this.refreshTokens.values()) {
44
+ if (row.familyId === familyId && !row.revocation)
45
+ row.revocation = { ...revocation };
46
+ }
47
+ }
48
+ async createIdpSession(row) {
49
+ const id = randomUUID();
50
+ this.idpSessions.set(id, { id, ...row });
51
+ return { id };
52
+ }
53
+ async findIdpSession(id) {
54
+ const row = this.idpSessions.get(id);
55
+ return row ? { ...row } : null;
56
+ }
57
+ async updateIdpSession(id, patch) {
58
+ const row = this.idpSessions.get(id);
59
+ if (row)
60
+ Object.assign(row, patch);
61
+ }
62
+ async deleteIdpSession(id) {
63
+ this.idpSessions.delete(id);
64
+ }
65
+ async withIdpSessionLock(sessionId, fn) {
66
+ const prior = this.lockTails.get(sessionId) ?? Promise.resolve();
67
+ // Chained after the prior holder whether it resolved or rejected, so one caller's
68
+ // failure can never wedge the queue for its siblings.
69
+ const run = () => fn(this);
70
+ const result = prior.then(run, run);
71
+ const tail = result.then(() => undefined, () => undefined);
72
+ this.lockTails.set(sessionId, tail);
73
+ // Released: forget the session unless another caller has queued behind this one, so
74
+ // a long-lived store does not keep one settled promise per session it ever locked.
75
+ void tail.then(() => {
76
+ if (this.lockTails.get(sessionId) === tail)
77
+ this.lockTails.delete(sessionId);
78
+ });
79
+ return result;
80
+ }
81
+ /** Sessions whose lock is currently held or queued for. */
82
+ heldLockCount() {
83
+ return this.lockTails.size;
84
+ }
85
+ /** Ids of every IdP session currently stored, oldest first. */
86
+ idpSessionIds() {
87
+ return [...this.idpSessions.keys()];
88
+ }
89
+ /** Ids of every refresh token whose `replacesId` is `id` — its rotation successors. */
90
+ successorsOf(id) {
91
+ return [...this.refreshTokens.values()].filter((row) => row.replacesId === id).map((row) => row.id);
92
+ }
93
+ insert(row, replacesId) {
94
+ this.refreshTokens.set(row.id, {
95
+ id: row.id,
96
+ familyId: row.familyId,
97
+ userId: row.userId,
98
+ idpSessionId: row.idpSessionId,
99
+ expiresAt: row.expiresAt,
100
+ ip: row.ip,
101
+ userAgent: row.userAgent,
102
+ tokenHash: row.tokenHash,
103
+ revocation: null,
104
+ replacesId,
105
+ });
106
+ }
107
+ }
108
+ function toRow(stored) {
109
+ return {
110
+ id: stored.id,
111
+ familyId: stored.familyId,
112
+ userId: stored.userId,
113
+ idpSessionId: stored.idpSessionId,
114
+ expiresAt: stored.expiresAt,
115
+ ip: stored.ip,
116
+ userAgent: stored.userAgent,
117
+ revocation: stored.revocation ? { ...stored.revocation } : null,
118
+ };
119
+ }
120
+ //# sourceMappingURL=memory-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-store.js","sourceRoot":"","sources":["../../../src/auth/testing/memory-store.ts"],"names":[],"mappings":"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"}
@@ -21,4 +21,28 @@ export declare function generateRefreshToken(): {
21
21
  token: string;
22
22
  hash: string;
23
23
  };
24
+ /** The minimum `accessTokenSecret` length the plugin accepts, in bytes (UTF-8): 256 bits,
25
+ * the HS256 key size and the HKDF input the successor key is derived from. */
26
+ export declare const MIN_ACCESS_TOKEN_SECRET_BYTES = 32;
27
+ /** The HMAC key `successorRefreshToken` uses, derived once from `accessTokenSecret` with
28
+ * HKDF-SHA256 (RFC 5869). */
29
+ export declare function refreshSuccessorKey(accessTokenSecret: string): Buffer;
30
+ /**
31
+ * The refresh token that rotating the presented row issues (AK-23):
32
+ * `HMAC-SHA256(key, presentedId ‖ 0x00 ‖ presentedToken)`, with `key` from
33
+ * `refreshSuccessorKey`. Deterministic, so `POST {routePrefix}/refresh` can hand an
34
+ * in-grace second presentation of a just-rotated token the SAME successor its first
35
+ * presentation issued — a browser whose refresh response was lost (timed out, network
36
+ * drop) still holds the old cookie, and nothing else could give it the successor back.
37
+ *
38
+ * `presentedId` is the row id the plugin generated (`randomUUID()`) and only the store
39
+ * holds — it is never in a cookie. So the successor is NOT computable from a raw token
40
+ * plus the secret: an attacker holding an old raw token who later learns the secret
41
+ * still cannot compute the family's live token offline, and rotating the secret (plus
42
+ * revoking every family — see the README) evicts them.
43
+ */
44
+ export declare function successorRefreshToken(key: Buffer, presentedId: string, presentedToken: string): {
45
+ token: string;
46
+ hash: string;
47
+ };
24
48
  export declare function hashRefreshToken(token: string): string;
@@ -1,4 +1,4 @@
1
- import { randomBytes, createHash } from "node:crypto";
1
+ import { randomBytes, createHash, createHmac, hkdfSync } from "node:crypto";
2
2
  import { SignJWT, jwtVerify } from "jose";
3
3
  /**
4
4
  * Bearer JWT this service issues to itself. Carries only `sub`/`sid` — never the
@@ -29,6 +29,36 @@ export function generateRefreshToken() {
29
29
  const token = randomBytes(48).toString("base64url");
30
30
  return { token, hash: hashRefreshToken(token) };
31
31
  }
32
+ /** HKDF `info` for the successor key: domain separation from every other use of
33
+ * `accessTokenSecret` (it also signs access tokens), versioned so the derivation can
34
+ * change without ever colliding with this one. */
35
+ const SUCCESSOR_KEY_INFO = "@flytedesk/app-kit/auth refresh-successor v1";
36
+ /** The minimum `accessTokenSecret` length the plugin accepts, in bytes (UTF-8): 256 bits,
37
+ * the HS256 key size and the HKDF input the successor key is derived from. */
38
+ export const MIN_ACCESS_TOKEN_SECRET_BYTES = 32;
39
+ /** The HMAC key `successorRefreshToken` uses, derived once from `accessTokenSecret` with
40
+ * HKDF-SHA256 (RFC 5869). */
41
+ export function refreshSuccessorKey(accessTokenSecret) {
42
+ return Buffer.from(hkdfSync("sha256", accessTokenSecret, Buffer.alloc(0), SUCCESSOR_KEY_INFO, 32));
43
+ }
44
+ /**
45
+ * The refresh token that rotating the presented row issues (AK-23):
46
+ * `HMAC-SHA256(key, presentedId ‖ 0x00 ‖ presentedToken)`, with `key` from
47
+ * `refreshSuccessorKey`. Deterministic, so `POST {routePrefix}/refresh` can hand an
48
+ * in-grace second presentation of a just-rotated token the SAME successor its first
49
+ * presentation issued — a browser whose refresh response was lost (timed out, network
50
+ * drop) still holds the old cookie, and nothing else could give it the successor back.
51
+ *
52
+ * `presentedId` is the row id the plugin generated (`randomUUID()`) and only the store
53
+ * holds — it is never in a cookie. So the successor is NOT computable from a raw token
54
+ * plus the secret: an attacker holding an old raw token who later learns the secret
55
+ * still cannot compute the family's live token offline, and rotating the secret (plus
56
+ * revoking every family — see the README) evicts them.
57
+ */
58
+ export function successorRefreshToken(key, presentedId, presentedToken) {
59
+ const token = createHmac("sha256", key).update(presentedId).update("\0").update(presentedToken).digest("base64url");
60
+ return { token, hash: hashRefreshToken(token) };
61
+ }
32
62
  export function hashRefreshToken(token) {
33
63
  return createHash("sha256").update(token).digest("hex");
34
64
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/auth/tokens.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAU1C;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAc,EACd,UAAkB,EAClB,MAAc,EACd,SAAiB;IAEjB,MAAM,GAAG,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC7C,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,UAAU,GAAG,IAAI,CAAC,CAAC;IAC3D,MAAM,KAAK,GAAG,MAAM,IAAI,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC;SAC7D,kBAAkB,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;SACpC,WAAW,EAAE;SACb,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,CAAC;SACzD,IAAI,CAAC,GAAG,CAAC,CAAC;IACb,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;AAC9B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,MAAc,EAAE,KAAa;IACnE,MAAM,GAAG,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC7C,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,UAAU,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC3E,IAAI,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ,IAAI,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ,EAAE,CAAC;QACvE,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;IACzE,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC;AAChD,CAAC;AAED;qEACqE;AACrE,MAAM,UAAU,oBAAoB;IAClC,MAAM,KAAK,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IACpD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC;AAClD,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC1D,CAAC"}
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/auth/tokens.ts"],"names":[],"mappings":"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"}