@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.
- package/README.md +157 -10
- package/dist/auth/client/httpClient.d.ts +79 -1
- package/dist/auth/client/httpClient.js +185 -16
- package/dist/auth/client/httpClient.js.map +1 -1
- package/dist/auth/client/index.d.ts +4 -4
- package/dist/auth/client/index.js +3 -3
- package/dist/auth/client/index.js.map +1 -1
- package/dist/auth/client/useAuthSession.d.ts +61 -13
- package/dist/auth/client/useAuthSession.js +185 -42
- package/dist/auth/client/useAuthSession.js.map +1 -1
- package/dist/auth/errors.d.ts +30 -7
- package/dist/auth/errors.js +38 -8
- package/dist/auth/errors.js.map +1 -1
- package/dist/auth/guards.d.ts +12 -0
- package/dist/auth/guards.js +24 -6
- package/dist/auth/guards.js.map +1 -1
- package/dist/auth/index.d.ts +4 -4
- package/dist/auth/index.js +2 -2
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/oidc-client.js +12 -2
- package/dist/auth/oidc-client.js.map +1 -1
- package/dist/auth/plugin.d.ts +6 -0
- package/dist/auth/plugin.js +532 -304
- package/dist/auth/plugin.js.map +1 -1
- package/dist/auth/shared-types.d.ts +28 -1
- package/dist/auth/shared-types.js +35 -0
- package/dist/auth/shared-types.js.map +1 -1
- package/dist/auth/silentAuthPage.d.ts +20 -0
- package/dist/auth/silentAuthPage.js +75 -0
- package/dist/auth/silentAuthPage.js.map +1 -0
- package/dist/auth/testing/fake-idp.d.ts +35 -0
- package/dist/auth/testing/fake-idp.js +86 -2
- package/dist/auth/testing/fake-idp.js.map +1 -1
- package/dist/auth/testing/index.d.ts +3 -1
- package/dist/auth/testing/index.js +3 -1
- package/dist/auth/testing/index.js.map +1 -1
- package/dist/auth/testing/memory-store.d.ts +44 -0
- package/dist/auth/testing/memory-store.js +120 -0
- package/dist/auth/testing/memory-store.js.map +1 -0
- package/dist/auth/tokens.d.ts +24 -0
- package/dist/auth/tokens.js +31 -1
- package/dist/auth/tokens.js.map +1 -1
- package/dist/auth/types.d.ts +231 -96
- package/dist/auth/unavailable.d.ts +10 -0
- package/dist/auth/unavailable.js +11 -0
- package/dist/auth/unavailable.js.map +1 -0
- package/dist/bigquery/errors.d.ts +21 -3
- package/dist/bigquery/errors.js +66 -12
- package/dist/bigquery/errors.js.map +1 -1
- package/dist/bigquery/index.d.ts +5 -3
- package/dist/bigquery/index.js +4 -2
- package/dist/bigquery/index.js.map +1 -1
- package/dist/bigquery/query.d.ts +9 -2
- package/dist/bigquery/query.js +38 -16
- package/dist/bigquery/query.js.map +1 -1
- package/dist/bigquery/types.d.ts +45 -1
- package/dist/flags/plugin.js +4 -20
- package/dist/flags/plugin.js.map +1 -1
- package/dist/postgres/advisoryLock.d.ts +58 -0
- package/dist/postgres/advisoryLock.js +71 -0
- package/dist/postgres/advisoryLock.js.map +1 -0
- package/dist/postgres/index.d.ts +6 -0
- package/dist/postgres/index.js +7 -0
- package/dist/postgres/index.js.map +1 -0
- package/dist/profile/plugin.js +3 -20
- package/dist/profile/plugin.js.map +1 -1
- package/package.json +8 -2
- package/scripts/pending-release-count.mjs +0 -37
- package/scripts/release.sh +0 -126
- 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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"}
|
package/dist/auth/tokens.d.ts
CHANGED
|
@@ -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;
|
package/dist/auth/tokens.js
CHANGED
|
@@ -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
|
}
|
package/dist/auth/tokens.js.map
CHANGED
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/auth/tokens.ts"],"names":[],"mappings":"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"}
|