@shipfox/api-auth 9.1.0 → 9.3.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +41 -0
- package/README.md +44 -2
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +26 -0
- package/dist/config.js.map +1 -1
- package/dist/core/admin-role-model.d.ts +5 -0
- package/dist/core/admin-role-model.d.ts.map +1 -0
- package/dist/core/admin-role-model.js +18 -0
- package/dist/core/admin-role-model.js.map +1 -0
- package/dist/core/admin-role.d.ts +13 -0
- package/dist/core/admin-role.d.ts.map +1 -0
- package/dist/core/admin-role.js +21 -0
- package/dist/core/admin-role.js.map +1 -0
- package/dist/core/administration.d.ts +27 -0
- package/dist/core/administration.d.ts.map +1 -0
- package/dist/core/administration.js +122 -0
- package/dist/core/administration.js.map +1 -0
- package/dist/core/auth.d.ts +9 -0
- package/dist/core/auth.d.ts.map +1 -1
- package/dist/core/auth.js +68 -20
- package/dist/core/auth.js.map +1 -1
- package/dist/core/entities/admin-grant.d.ts +10 -0
- package/dist/core/entities/admin-grant.d.ts.map +1 -0
- package/dist/core/entities/admin-grant.js +3 -0
- package/dist/core/entities/admin-grant.js.map +1 -0
- package/dist/core/errors.d.ts +25 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +49 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/ports.d.ts +13 -0
- package/dist/core/ports.d.ts.map +1 -0
- package/dist/core/ports.js +3 -0
- package/dist/core/ports.js.map +1 -0
- package/dist/core/signup-policy.d.ts +10 -0
- package/dist/core/signup-policy.d.ts.map +1 -0
- package/dist/core/signup-policy.js +39 -0
- package/dist/core/signup-policy.js.map +1 -0
- package/dist/db/admin-grants.d.ts +33 -0
- package/dist/db/admin-grants.d.ts.map +1 -0
- package/dist/db/admin-grants.js +193 -0
- package/dist/db/admin-grants.js.map +1 -0
- package/dist/db/db.d.ts +474 -0
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +4 -0
- package/dist/db/db.js.map +1 -1
- package/dist/db/schema/admin-command-results.d.ts +142 -0
- package/dist/db/schema/admin-command-results.d.ts.map +1 -0
- package/dist/db/schema/admin-command-results.js +21 -0
- package/dist/db/schema/admin-command-results.js.map +1 -0
- package/dist/db/schema/admin-grants.d.ts +115 -0
- package/dist/db/schema/admin-grants.d.ts.map +1 -0
- package/dist/db/schema/admin-grants.js +42 -0
- package/dist/db/schema/admin-grants.js.map +1 -0
- package/dist/index.d.ts +12 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -4
- package/dist/index.js.map +1 -1
- package/dist/metrics/instance.d.ts +1 -1
- package/dist/metrics/instance.d.ts.map +1 -1
- package/dist/metrics/instance.js.map +1 -1
- package/dist/presentation/dto/user.d.ts +2 -1
- package/dist/presentation/dto/user.d.ts.map +1 -1
- package/dist/presentation/dto/user.js +2 -1
- package/dist/presentation/dto/user.js.map +1 -1
- package/dist/presentation/inter-module.d.ts.map +1 -1
- package/dist/presentation/inter-module.js +26 -2
- package/dist/presentation/inter-module.js.map +1 -1
- package/dist/presentation/routes/administration.d.ts +3 -0
- package/dist/presentation/routes/administration.d.ts.map +1 -0
- package/dist/presentation/routes/administration.js +186 -0
- package/dist/presentation/routes/administration.js.map +1 -0
- package/dist/presentation/routes/index.d.ts +2 -1
- package/dist/presentation/routes/index.d.ts.map +1 -1
- package/dist/presentation/routes/index.js +2 -2
- package/dist/presentation/routes/index.js.map +1 -1
- package/dist/presentation/routes/rate-limit.d.ts +1 -0
- package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
- package/dist/presentation/routes/rate-limit.js +20 -1
- package/dist/presentation/routes/rate-limit.js.map +1 -1
- package/dist/presentation/routes/registration/signup.d.ts +2 -1
- package/dist/presentation/routes/registration/signup.d.ts.map +1 -1
- package/dist/presentation/routes/registration/signup.js +14 -3
- package/dist/presentation/routes/registration/signup.js.map +1 -1
- package/dist/presentation/routes/session/me.d.ts.map +1 -1
- package/dist/presentation/routes/session/me.js +5 -1
- package/dist/presentation/routes/session/me.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0001_brainy_shriek.sql +14 -0
- package/drizzle/0002_burly_whizzer.sql +12 -0
- package/drizzle/meta/0001_snapshot.json +702 -0
- package/drizzle/meta/0002_snapshot.json +791 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +10 -9
- package/src/config.test.ts +39 -0
- package/src/config.ts +34 -0
- package/src/core/admin-role-model.ts +25 -0
- package/src/core/admin-role.test.ts +39 -0
- package/src/core/admin-role.ts +25 -0
- package/src/core/administration.ts +172 -0
- package/src/core/auth-default-signup-policy.test.ts +65 -0
- package/src/core/auth.test.ts +115 -0
- package/src/core/auth.ts +77 -21
- package/src/core/entities/admin-grant.ts +10 -0
- package/src/core/errors.ts +59 -0
- package/src/core/jwt.test.ts +1 -0
- package/src/core/ports.ts +7 -0
- package/src/core/signup-policy.test.ts +78 -0
- package/src/core/signup-policy.ts +42 -0
- package/src/db/admin-grants.test.ts +57 -0
- package/src/db/admin-grants.ts +340 -0
- package/src/db/db.ts +4 -0
- package/src/db/schema/admin-command-results.ts +41 -0
- package/src/db/schema/admin-grants.ts +49 -0
- package/src/index.test.ts +58 -1
- package/src/index.ts +47 -6
- package/src/metrics/instance.ts +1 -1
- package/src/presentation/auth/refresh-cookie.test.ts +4 -0
- package/src/presentation/dto/user.ts +7 -2
- package/src/presentation/e2eRoutes/index.test.ts +1 -0
- package/src/presentation/inter-module.test.ts +47 -0
- package/src/presentation/inter-module.ts +22 -1
- package/src/presentation/routes/administration.test.ts +253 -0
- package/src/presentation/routes/administration.ts +195 -0
- package/src/presentation/routes/index.test.ts +1 -0
- package/src/presentation/routes/index.ts +3 -1
- package/src/presentation/routes/rate-limit.ts +23 -2
- package/src/presentation/routes/registration/signup.test.ts +26 -0
- package/src/presentation/routes/registration/signup.ts +22 -2
- package/src/presentation/routes/session/login.test.ts +1 -0
- package/src/presentation/routes/session/me.test.ts +18 -2
- package/src/presentation/routes/session/me.ts +5 -1
- package/test/globalSetup.ts +6 -2
- package/test/routes.ts +7 -1
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { config } from '#config.js';
|
|
2
|
+
export const DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE = 'This Shipfox deployment does not accept new accounts right now.';
|
|
3
|
+
/**
|
|
4
|
+
* Builds the default signup policy from the Auth module's environment settings.
|
|
5
|
+
*
|
|
6
|
+
* The allowlist is resolved once at startup. Environment values are normalized
|
|
7
|
+
* before matching, but the configured denial message is returned unchanged.
|
|
8
|
+
*/ export function createEnvironmentSignupPolicy() {
|
|
9
|
+
const allowedEmails = parseAllowlist(config.AUTH_SIGNUP_ALLOWED_EMAILS);
|
|
10
|
+
const allowedDomains = parseAllowlist(config.AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS);
|
|
11
|
+
return {
|
|
12
|
+
isSignupAllowed ({ email }) {
|
|
13
|
+
if (!config.AUTH_SIGNUP_GATE_ENABLED) return Promise.resolve({
|
|
14
|
+
allowed: true
|
|
15
|
+
});
|
|
16
|
+
const normalizedEmail = normalize(email);
|
|
17
|
+
if (allowedEmails.has(normalizedEmail)) return Promise.resolve({
|
|
18
|
+
allowed: true
|
|
19
|
+
});
|
|
20
|
+
const atIndex = normalizedEmail.lastIndexOf('@');
|
|
21
|
+
const domain = atIndex === -1 ? '' : normalizedEmail.slice(atIndex + 1);
|
|
22
|
+
if (allowedDomains.has(domain)) return Promise.resolve({
|
|
23
|
+
allowed: true
|
|
24
|
+
});
|
|
25
|
+
return Promise.resolve({
|
|
26
|
+
allowed: false,
|
|
27
|
+
message: config.AUTH_SIGNUP_NOT_ALLOWED_MESSAGE ?? DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function parseAllowlist(value) {
|
|
33
|
+
return new Set(value.split(',').map(normalize).filter(Boolean));
|
|
34
|
+
}
|
|
35
|
+
function normalize(value) {
|
|
36
|
+
return value.trim().toLowerCase();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
//# sourceMappingURL=signup-policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/signup-policy.ts"],"sourcesContent":["import {config} from '#config.js';\nimport type {SignupPolicy} from './ports.js';\n\nexport const DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE =\n 'This Shipfox deployment does not accept new accounts right now.';\n\n/**\n * Builds the default signup policy from the Auth module's environment settings.\n *\n * The allowlist is resolved once at startup. Environment values are normalized\n * before matching, but the configured denial message is returned unchanged.\n */\nexport function createEnvironmentSignupPolicy(): SignupPolicy {\n const allowedEmails = parseAllowlist(config.AUTH_SIGNUP_ALLOWED_EMAILS);\n const allowedDomains = parseAllowlist(config.AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS);\n\n return {\n isSignupAllowed({email}) {\n if (!config.AUTH_SIGNUP_GATE_ENABLED) return Promise.resolve({allowed: true});\n\n const normalizedEmail = normalize(email);\n if (allowedEmails.has(normalizedEmail)) return Promise.resolve({allowed: true});\n\n const atIndex = normalizedEmail.lastIndexOf('@');\n const domain = atIndex === -1 ? '' : normalizedEmail.slice(atIndex + 1);\n if (allowedDomains.has(domain)) return Promise.resolve({allowed: true});\n\n return Promise.resolve({\n allowed: false,\n message: config.AUTH_SIGNUP_NOT_ALLOWED_MESSAGE ?? DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE,\n });\n },\n };\n}\n\nfunction parseAllowlist(value: string): ReadonlySet<string> {\n return new Set(value.split(',').map(normalize).filter(Boolean));\n}\n\nfunction normalize(value: string): string {\n return value.trim().toLowerCase();\n}\n"],"names":["config","DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE","createEnvironmentSignupPolicy","allowedEmails","parseAllowlist","AUTH_SIGNUP_ALLOWED_EMAILS","allowedDomains","AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS","isSignupAllowed","email","AUTH_SIGNUP_GATE_ENABLED","Promise","resolve","allowed","normalizedEmail","normalize","has","atIndex","lastIndexOf","domain","slice","message","AUTH_SIGNUP_NOT_ALLOWED_MESSAGE","value","Set","split","map","filter","Boolean","trim","toLowerCase"],"mappings":"AAAA,SAAQA,MAAM,QAAO,aAAa;AAGlC,OAAO,MAAMC,qCACX,kEAAkE;AAEpE;;;;;CAKC,GACD,OAAO,SAASC;IACd,MAAMC,gBAAgBC,eAAeJ,OAAOK,0BAA0B;IACtE,MAAMC,iBAAiBF,eAAeJ,OAAOO,iCAAiC;IAE9E,OAAO;QACLC,iBAAgB,EAACC,KAAK,EAAC;YACrB,IAAI,CAACT,OAAOU,wBAAwB,EAAE,OAAOC,QAAQC,OAAO,CAAC;gBAACC,SAAS;YAAI;YAE3E,MAAMC,kBAAkBC,UAAUN;YAClC,IAAIN,cAAca,GAAG,CAACF,kBAAkB,OAAOH,QAAQC,OAAO,CAAC;gBAACC,SAAS;YAAI;YAE7E,MAAMI,UAAUH,gBAAgBI,WAAW,CAAC;YAC5C,MAAMC,SAASF,YAAY,CAAC,IAAI,KAAKH,gBAAgBM,KAAK,CAACH,UAAU;YACrE,IAAIX,eAAeU,GAAG,CAACG,SAAS,OAAOR,QAAQC,OAAO,CAAC;gBAACC,SAAS;YAAI;YAErE,OAAOF,QAAQC,OAAO,CAAC;gBACrBC,SAAS;gBACTQ,SAASrB,OAAOsB,+BAA+B,IAAIrB;YACrD;QACF;IACF;AACF;AAEA,SAASG,eAAemB,KAAa;IACnC,OAAO,IAAIC,IAAID,MAAME,KAAK,CAAC,KAAKC,GAAG,CAACX,WAAWY,MAAM,CAACC;AACxD;AAEA,SAASb,UAAUQ,KAAa;IAC9B,OAAOA,MAAMM,IAAI,GAAGC,WAAW;AACjC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AdminRole } from '@shipfox/api-auth-dto';
|
|
2
|
+
import type { AdministrationActionEvent } from '@shipfox/api-common-dto';
|
|
3
|
+
import type { AdminGrant } from '#core/entities/admin-grant.js';
|
|
4
|
+
export interface CreateAdminGrantParams {
|
|
5
|
+
userId: string;
|
|
6
|
+
role: AdminRole;
|
|
7
|
+
}
|
|
8
|
+
export declare function createAdminGrant(params: CreateAdminGrantParams): Promise<AdminGrant>;
|
|
9
|
+
export declare function listAdminGrants(): Promise<AdminGrant[]>;
|
|
10
|
+
export declare function findCurrentAdminRole(params: {
|
|
11
|
+
userId: string;
|
|
12
|
+
}): Promise<AdminRole | null>;
|
|
13
|
+
export declare function revokeAdminGrant(params: {
|
|
14
|
+
grantId: string;
|
|
15
|
+
}): Promise<AdminGrant | undefined>;
|
|
16
|
+
interface AuditedAdminCommandParams {
|
|
17
|
+
actorId: string;
|
|
18
|
+
idempotencyKeyFingerprint: string;
|
|
19
|
+
requestFingerprint: string;
|
|
20
|
+
event: AdministrationActionEvent;
|
|
21
|
+
}
|
|
22
|
+
export declare function bootstrapFirstAdminOwner(params: AuditedAdminCommandParams & {
|
|
23
|
+
userId: string;
|
|
24
|
+
}): Promise<AdminGrant>;
|
|
25
|
+
export declare function grantAdminRoleWithAudit(params: AuditedAdminCommandParams & {
|
|
26
|
+
userId: string;
|
|
27
|
+
role: AdminRole;
|
|
28
|
+
}): Promise<AdminGrant>;
|
|
29
|
+
export declare function revokeAdminGrantWithAudit(params: AuditedAdminCommandParams & {
|
|
30
|
+
grantId: string;
|
|
31
|
+
}): Promise<AdminGrant>;
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=admin-grants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-grants.d.ts","sourceRoot":"","sources":["../../src/db/admin-grants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EACV,yBAAyB,EAE1B,MAAM,yBAAyB,CAAC;AAIjC,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,+BAA+B,CAAC;AAqB9D,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,SAAS,CAAC;CACjB;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,sBAAsB,GAAG,OAAO,CAAC,UAAU,CAAC,CAQ1F;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC,CAG7D;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAc9F;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAqCjG;AAED,UAAU,yBAAyB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,yBAAyB,EAAE,MAAM,CAAC;IAClC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,KAAK,EAAE,yBAAyB,CAAC;CAClC;AAoFD,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,yBAAyB,GAAG;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,GACnD,OAAO,CAAC,UAAU,CAAC,CA6CrB;AAED,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,yBAAyB,GAAG;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,GACpE,OAAO,CAAC,UAAU,CAAC,CA4CrB;AAED,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,yBAAyB,GAAG;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GACpD,OAAO,CAAC,UAAU,CAAC,CA+CrB"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { writeOutboxEvent } from '@shipfox/node-outbox';
|
|
2
|
+
import { and, asc, eq, isNull, sql } from 'drizzle-orm';
|
|
3
|
+
import { highestAdminRole } from '#core/admin-role-model.js';
|
|
4
|
+
import { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, LastAdminOwnerError, UserNotFoundError } from '#core/errors.js';
|
|
5
|
+
import { db } from './db.js';
|
|
6
|
+
import { adminCommandResults } from './schema/admin-command-results.js';
|
|
7
|
+
import { adminGrants, toAdminGrant } from './schema/admin-grants.js';
|
|
8
|
+
import { authOutbox } from './schema/outbox.js';
|
|
9
|
+
import { users } from './schema/users.js';
|
|
10
|
+
export async function createAdminGrant(params) {
|
|
11
|
+
const rows = await db().insert(adminGrants).values({
|
|
12
|
+
userId: params.userId,
|
|
13
|
+
role: params.role
|
|
14
|
+
}).returning();
|
|
15
|
+
const row = rows[0];
|
|
16
|
+
if (!row) throw new Error('Insert returned no rows');
|
|
17
|
+
return toAdminGrant(row);
|
|
18
|
+
}
|
|
19
|
+
export async function listAdminGrants() {
|
|
20
|
+
const rows = await db().select().from(adminGrants).orderBy(asc(adminGrants.createdAt));
|
|
21
|
+
return rows.map(toAdminGrant);
|
|
22
|
+
}
|
|
23
|
+
export async function findCurrentAdminRole(params) {
|
|
24
|
+
const rows = await db().select({
|
|
25
|
+
role: adminGrants.role
|
|
26
|
+
}).from(adminGrants).innerJoin(users, eq(adminGrants.userId, users.id)).where(and(eq(adminGrants.userId, params.userId), isNull(adminGrants.revokedAt), eq(users.status, 'active')));
|
|
27
|
+
return highestAdminRole(rows.map(({ role })=>role));
|
|
28
|
+
}
|
|
29
|
+
export async function revokeAdminGrant(params) {
|
|
30
|
+
return await db().transaction(async (tx)=>{
|
|
31
|
+
// All owner grant changes share one lock so concurrent revocations cannot
|
|
32
|
+
// both observe the same final owner and leave the instance ownerless.
|
|
33
|
+
await tx.execute(sql`select pg_advisory_xact_lock(hashtext('auth_admin_owner_grants'))`);
|
|
34
|
+
const rows = await tx.select().from(adminGrants).where(and(eq(adminGrants.id, params.grantId), isNull(adminGrants.revokedAt))).limit(1);
|
|
35
|
+
const grant = rows[0];
|
|
36
|
+
if (!grant) return undefined;
|
|
37
|
+
if (grant.role === 'admin-owner') {
|
|
38
|
+
const activeOwners = await tx.select({
|
|
39
|
+
id: adminGrants.id
|
|
40
|
+
}).from(adminGrants).innerJoin(users, eq(adminGrants.userId, users.id)).where(and(eq(adminGrants.role, 'admin-owner'), isNull(adminGrants.revokedAt), eq(users.status, 'active')));
|
|
41
|
+
if (activeOwners.length <= 1) throw new LastAdminOwnerError();
|
|
42
|
+
}
|
|
43
|
+
const updated = await tx.update(adminGrants).set({
|
|
44
|
+
revokedAt: new Date(),
|
|
45
|
+
updatedAt: new Date()
|
|
46
|
+
}).where(and(eq(adminGrants.id, grant.id), isNull(adminGrants.revokedAt))).returning();
|
|
47
|
+
const row = updated[0];
|
|
48
|
+
return row ? toAdminGrant(row) : undefined;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function toStoredAdminGrant(grant) {
|
|
52
|
+
return {
|
|
53
|
+
id: grant.id,
|
|
54
|
+
userId: grant.userId,
|
|
55
|
+
role: grant.role,
|
|
56
|
+
revokedAt: grant.revokedAt?.toISOString() ?? null,
|
|
57
|
+
createdAt: grant.createdAt.toISOString(),
|
|
58
|
+
updatedAt: grant.updatedAt.toISOString()
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function fromStoredAdminGrant(grant) {
|
|
62
|
+
return {
|
|
63
|
+
id: grant.id,
|
|
64
|
+
userId: grant.userId,
|
|
65
|
+
role: grant.role,
|
|
66
|
+
revokedAt: grant.revokedAt ? new Date(grant.revokedAt) : null,
|
|
67
|
+
createdAt: new Date(grant.createdAt),
|
|
68
|
+
updatedAt: new Date(grant.updatedAt)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
async function findCommandResult(tx, params) {
|
|
72
|
+
const rows = await tx.select().from(adminCommandResults).where(and(eq(adminCommandResults.actorId, params.actorId), eq(adminCommandResults.idempotencyKeyFingerprint, params.idempotencyKeyFingerprint))).limit(1);
|
|
73
|
+
const result = rows[0];
|
|
74
|
+
if (!result) return undefined;
|
|
75
|
+
if (result.command !== params.command || result.requestFingerprint !== params.requestFingerprint) {
|
|
76
|
+
throw new AdminIdempotencyKeyReuseError();
|
|
77
|
+
}
|
|
78
|
+
return fromStoredAdminGrant(result.result.grant);
|
|
79
|
+
}
|
|
80
|
+
async function storeCommandResult(tx, params, grant) {
|
|
81
|
+
await tx.insert(adminCommandResults).values({
|
|
82
|
+
actorId: params.actorId,
|
|
83
|
+
idempotencyKeyFingerprint: params.idempotencyKeyFingerprint,
|
|
84
|
+
command: params.event.command,
|
|
85
|
+
requestFingerprint: params.requestFingerprint,
|
|
86
|
+
result: {
|
|
87
|
+
grant: toStoredAdminGrant(grant)
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
async function writeAdminAction(tx, event) {
|
|
92
|
+
await writeOutboxEvent(tx, authOutbox, {
|
|
93
|
+
type: 'administration.action.performed',
|
|
94
|
+
payload: event
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
async function lockAdminCommand(tx, params) {
|
|
98
|
+
await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${`auth_admin_command:${params.actorId}:${params.idempotencyKeyFingerprint}`}))`);
|
|
99
|
+
}
|
|
100
|
+
export async function bootstrapFirstAdminOwner(params) {
|
|
101
|
+
return await db().transaction(async (tx)=>{
|
|
102
|
+
await lockAdminCommand(tx, params);
|
|
103
|
+
await tx.execute(sql`select pg_advisory_xact_lock(hashtext('auth_admin_owner_grants'))`);
|
|
104
|
+
const existing = await findCommandResult(tx, {
|
|
105
|
+
...params,
|
|
106
|
+
command: params.event.command
|
|
107
|
+
});
|
|
108
|
+
if (existing) return existing;
|
|
109
|
+
const activeOwners = await tx.select({
|
|
110
|
+
id: adminGrants.id
|
|
111
|
+
}).from(adminGrants).innerJoin(users, eq(adminGrants.userId, users.id)).where(and(eq(adminGrants.role, 'admin-owner'), isNull(adminGrants.revokedAt), eq(users.status, 'active'))).limit(1);
|
|
112
|
+
if (activeOwners.length > 0) throw new AdminBootstrapClosedError();
|
|
113
|
+
const userRows = await tx.select({
|
|
114
|
+
id: users.id,
|
|
115
|
+
status: users.status
|
|
116
|
+
}).from(users).where(eq(users.id, params.userId)).limit(1);
|
|
117
|
+
const user = userRows[0];
|
|
118
|
+
if (user?.status !== 'active') throw new UserNotFoundError(params.userId);
|
|
119
|
+
const rows = await tx.insert(adminGrants).values({
|
|
120
|
+
userId: params.userId,
|
|
121
|
+
role: 'admin-owner'
|
|
122
|
+
}).returning();
|
|
123
|
+
const row = rows[0];
|
|
124
|
+
if (!row) throw new Error('Bootstrap grant insert returned no rows');
|
|
125
|
+
const grant = toAdminGrant(row);
|
|
126
|
+
await writeAdminAction(tx, params.event);
|
|
127
|
+
await storeCommandResult(tx, params, grant);
|
|
128
|
+
return grant;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
export async function grantAdminRoleWithAudit(params) {
|
|
132
|
+
return await db().transaction(async (tx)=>{
|
|
133
|
+
await lockAdminCommand(tx, params);
|
|
134
|
+
await tx.execute(sql`select pg_advisory_xact_lock(hashtext('auth_admin_owner_grants'))`);
|
|
135
|
+
const existing = await findCommandResult(tx, {
|
|
136
|
+
...params,
|
|
137
|
+
command: params.event.command
|
|
138
|
+
});
|
|
139
|
+
if (existing) return existing;
|
|
140
|
+
const userRows = await tx.select({
|
|
141
|
+
id: users.id,
|
|
142
|
+
status: users.status
|
|
143
|
+
}).from(users).where(eq(users.id, params.userId)).limit(1);
|
|
144
|
+
const user = userRows[0];
|
|
145
|
+
if (user?.status !== 'active') throw new UserNotFoundError(params.userId);
|
|
146
|
+
const activeRows = await tx.select({
|
|
147
|
+
id: adminGrants.id
|
|
148
|
+
}).from(adminGrants).where(and(eq(adminGrants.userId, params.userId), eq(adminGrants.role, params.role), isNull(adminGrants.revokedAt))).limit(1);
|
|
149
|
+
if (activeRows.length > 0) throw new AdminGrantAlreadyExistsError();
|
|
150
|
+
const rows = await tx.insert(adminGrants).values({
|
|
151
|
+
userId: params.userId,
|
|
152
|
+
role: params.role
|
|
153
|
+
}).returning();
|
|
154
|
+
const row = rows[0];
|
|
155
|
+
if (!row) throw new Error('Administrator grant insert returned no rows');
|
|
156
|
+
const grant = toAdminGrant(row);
|
|
157
|
+
await writeAdminAction(tx, params.event);
|
|
158
|
+
await storeCommandResult(tx, params, grant);
|
|
159
|
+
return grant;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
export async function revokeAdminGrantWithAudit(params) {
|
|
163
|
+
return await db().transaction(async (tx)=>{
|
|
164
|
+
await lockAdminCommand(tx, params);
|
|
165
|
+
await tx.execute(sql`select pg_advisory_xact_lock(hashtext('auth_admin_owner_grants'))`);
|
|
166
|
+
const existing = await findCommandResult(tx, {
|
|
167
|
+
...params,
|
|
168
|
+
command: params.event.command
|
|
169
|
+
});
|
|
170
|
+
if (existing) return existing;
|
|
171
|
+
const rows = await tx.select().from(adminGrants).where(and(eq(adminGrants.id, params.grantId), isNull(adminGrants.revokedAt))).limit(1);
|
|
172
|
+
const grant = rows[0];
|
|
173
|
+
if (!grant) throw new AdminGrantNotFoundError();
|
|
174
|
+
if (grant.role === 'admin-owner') {
|
|
175
|
+
const activeOwners = await tx.select({
|
|
176
|
+
id: adminGrants.id
|
|
177
|
+
}).from(adminGrants).innerJoin(users, eq(adminGrants.userId, users.id)).where(and(eq(adminGrants.role, 'admin-owner'), isNull(adminGrants.revokedAt), eq(users.status, 'active')));
|
|
178
|
+
if (activeOwners.length <= 1) throw new LastAdminOwnerError();
|
|
179
|
+
}
|
|
180
|
+
const updated = await tx.update(adminGrants).set({
|
|
181
|
+
revokedAt: new Date(),
|
|
182
|
+
updatedAt: new Date()
|
|
183
|
+
}).where(and(eq(adminGrants.id, grant.id), isNull(adminGrants.revokedAt))).returning();
|
|
184
|
+
const updatedRow = updated[0];
|
|
185
|
+
if (!updatedRow) throw new AdminGrantNotFoundError();
|
|
186
|
+
const revoked = toAdminGrant(updatedRow);
|
|
187
|
+
await writeAdminAction(tx, params.event);
|
|
188
|
+
await storeCommandResult(tx, params, revoked);
|
|
189
|
+
return revoked;
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
//# sourceMappingURL=admin-grants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/db/admin-grants.ts"],"sourcesContent":["import type {AdminRole} from '@shipfox/api-auth-dto';\nimport type {\n AdministrationActionEvent,\n AdministrationActionEventMap,\n} from '@shipfox/api-common-dto';\nimport {writeOutboxEvent} from '@shipfox/node-outbox';\nimport {and, asc, eq, isNull, sql} from 'drizzle-orm';\nimport {highestAdminRole} from '#core/admin-role-model.js';\nimport type {AdminGrant} from '#core/entities/admin-grant.js';\nimport {\n AdminBootstrapClosedError,\n AdminGrantAlreadyExistsError,\n AdminGrantNotFoundError,\n AdminIdempotencyKeyReuseError,\n LastAdminOwnerError,\n UserNotFoundError,\n} from '#core/errors.js';\nimport {db} from './db.js';\nimport {\n type AdminCommandResultDb,\n adminCommandResults,\n type StoredAdminGrant,\n} from './schema/admin-command-results.js';\nimport {adminGrants, toAdminGrant} from './schema/admin-grants.js';\nimport {authOutbox} from './schema/outbox.js';\nimport {users} from './schema/users.js';\n\ntype Tx = Parameters<Parameters<ReturnType<typeof db>['transaction']>[0]>[0];\n\nexport interface CreateAdminGrantParams {\n userId: string;\n role: AdminRole;\n}\n\nexport async function createAdminGrant(params: CreateAdminGrantParams): Promise<AdminGrant> {\n const rows = await db()\n .insert(adminGrants)\n .values({userId: params.userId, role: params.role})\n .returning();\n const row = rows[0];\n if (!row) throw new Error('Insert returned no rows');\n return toAdminGrant(row);\n}\n\nexport async function listAdminGrants(): Promise<AdminGrant[]> {\n const rows = await db().select().from(adminGrants).orderBy(asc(adminGrants.createdAt));\n return rows.map(toAdminGrant);\n}\n\nexport async function findCurrentAdminRole(params: {userId: string}): Promise<AdminRole | null> {\n const rows = await db()\n .select({role: adminGrants.role})\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(\n and(\n eq(adminGrants.userId, params.userId),\n isNull(adminGrants.revokedAt),\n eq(users.status, 'active'),\n ),\n );\n\n return highestAdminRole(rows.map(({role}) => role));\n}\n\nexport async function revokeAdminGrant(params: {grantId: string}): Promise<AdminGrant | undefined> {\n return await db().transaction(async (tx) => {\n // All owner grant changes share one lock so concurrent revocations cannot\n // both observe the same final owner and leave the instance ownerless.\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext('auth_admin_owner_grants'))`);\n\n const rows = await tx\n .select()\n .from(adminGrants)\n .where(and(eq(adminGrants.id, params.grantId), isNull(adminGrants.revokedAt)))\n .limit(1);\n const grant = rows[0];\n if (!grant) return undefined;\n\n if (grant.role === 'admin-owner') {\n const activeOwners = await tx\n .select({id: adminGrants.id})\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(\n and(\n eq(adminGrants.role, 'admin-owner'),\n isNull(adminGrants.revokedAt),\n eq(users.status, 'active'),\n ),\n );\n if (activeOwners.length <= 1) throw new LastAdminOwnerError();\n }\n\n const updated = await tx\n .update(adminGrants)\n .set({revokedAt: new Date(), updatedAt: new Date()})\n .where(and(eq(adminGrants.id, grant.id), isNull(adminGrants.revokedAt)))\n .returning();\n const row = updated[0];\n return row ? toAdminGrant(row) : undefined;\n });\n}\n\ninterface AuditedAdminCommandParams {\n actorId: string;\n idempotencyKeyFingerprint: string;\n requestFingerprint: string;\n event: AdministrationActionEvent;\n}\n\nfunction toStoredAdminGrant(grant: AdminGrant): StoredAdminGrant {\n return {\n id: grant.id,\n userId: grant.userId,\n role: grant.role,\n revokedAt: grant.revokedAt?.toISOString() ?? null,\n createdAt: grant.createdAt.toISOString(),\n updatedAt: grant.updatedAt.toISOString(),\n };\n}\n\nfunction fromStoredAdminGrant(grant: StoredAdminGrant): AdminGrant {\n return {\n id: grant.id,\n userId: grant.userId,\n role: grant.role,\n revokedAt: grant.revokedAt ? new Date(grant.revokedAt) : null,\n createdAt: new Date(grant.createdAt),\n updatedAt: new Date(grant.updatedAt),\n };\n}\n\nasync function findCommandResult(\n tx: Tx,\n params: Pick<\n AuditedAdminCommandParams,\n 'actorId' | 'idempotencyKeyFingerprint' | 'requestFingerprint'\n > & {\n command: string;\n },\n): Promise<AdminGrant | undefined> {\n const rows = await tx\n .select()\n .from(adminCommandResults)\n .where(\n and(\n eq(adminCommandResults.actorId, params.actorId),\n eq(adminCommandResults.idempotencyKeyFingerprint, params.idempotencyKeyFingerprint),\n ),\n )\n .limit(1);\n const result: AdminCommandResultDb | undefined = rows[0];\n if (!result) return undefined;\n if (\n result.command !== params.command ||\n result.requestFingerprint !== params.requestFingerprint\n ) {\n throw new AdminIdempotencyKeyReuseError();\n }\n return fromStoredAdminGrant(result.result.grant);\n}\n\nasync function storeCommandResult(\n tx: Tx,\n params: AuditedAdminCommandParams,\n grant: AdminGrant,\n): Promise<void> {\n await tx.insert(adminCommandResults).values({\n actorId: params.actorId,\n idempotencyKeyFingerprint: params.idempotencyKeyFingerprint,\n command: params.event.command,\n requestFingerprint: params.requestFingerprint,\n result: {grant: toStoredAdminGrant(grant)},\n });\n}\n\nasync function writeAdminAction(tx: Tx, event: AdministrationActionEvent): Promise<void> {\n await writeOutboxEvent<AdministrationActionEventMap>(tx, authOutbox, {\n type: 'administration.action.performed',\n payload: event,\n });\n}\n\nasync function lockAdminCommand(\n tx: Tx,\n params: {actorId: string; idempotencyKeyFingerprint: string},\n) {\n await tx.execute(\n sql`select pg_advisory_xact_lock(hashtext(${`auth_admin_command:${params.actorId}:${params.idempotencyKeyFingerprint}`}))`,\n );\n}\n\nexport async function bootstrapFirstAdminOwner(\n params: AuditedAdminCommandParams & {userId: string},\n): Promise<AdminGrant> {\n return await db().transaction(async (tx) => {\n await lockAdminCommand(tx, params);\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext('auth_admin_owner_grants'))`);\n\n const existing = await findCommandResult(tx, {\n ...params,\n command: params.event.command,\n });\n if (existing) return existing;\n\n const activeOwners = await tx\n .select({id: adminGrants.id})\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(\n and(\n eq(adminGrants.role, 'admin-owner'),\n isNull(adminGrants.revokedAt),\n eq(users.status, 'active'),\n ),\n )\n .limit(1);\n if (activeOwners.length > 0) throw new AdminBootstrapClosedError();\n\n const userRows = await tx\n .select({id: users.id, status: users.status})\n .from(users)\n .where(eq(users.id, params.userId))\n .limit(1);\n const user = userRows[0];\n if (user?.status !== 'active') throw new UserNotFoundError(params.userId);\n\n const rows = await tx\n .insert(adminGrants)\n .values({userId: params.userId, role: 'admin-owner'})\n .returning();\n const row = rows[0];\n if (!row) throw new Error('Bootstrap grant insert returned no rows');\n\n const grant = toAdminGrant(row);\n await writeAdminAction(tx, params.event);\n await storeCommandResult(tx, params, grant);\n return grant;\n });\n}\n\nexport async function grantAdminRoleWithAudit(\n params: AuditedAdminCommandParams & {userId: string; role: AdminRole},\n): Promise<AdminGrant> {\n return await db().transaction(async (tx) => {\n await lockAdminCommand(tx, params);\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext('auth_admin_owner_grants'))`);\n\n const existing = await findCommandResult(tx, {\n ...params,\n command: params.event.command,\n });\n if (existing) return existing;\n\n const userRows = await tx\n .select({id: users.id, status: users.status})\n .from(users)\n .where(eq(users.id, params.userId))\n .limit(1);\n const user = userRows[0];\n if (user?.status !== 'active') throw new UserNotFoundError(params.userId);\n\n const activeRows = await tx\n .select({id: adminGrants.id})\n .from(adminGrants)\n .where(\n and(\n eq(adminGrants.userId, params.userId),\n eq(adminGrants.role, params.role),\n isNull(adminGrants.revokedAt),\n ),\n )\n .limit(1);\n if (activeRows.length > 0) throw new AdminGrantAlreadyExistsError();\n\n const rows = await tx\n .insert(adminGrants)\n .values({userId: params.userId, role: params.role})\n .returning();\n const row = rows[0];\n if (!row) throw new Error('Administrator grant insert returned no rows');\n\n const grant = toAdminGrant(row);\n await writeAdminAction(tx, params.event);\n await storeCommandResult(tx, params, grant);\n return grant;\n });\n}\n\nexport async function revokeAdminGrantWithAudit(\n params: AuditedAdminCommandParams & {grantId: string},\n): Promise<AdminGrant> {\n return await db().transaction(async (tx) => {\n await lockAdminCommand(tx, params);\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext('auth_admin_owner_grants'))`);\n\n const existing = await findCommandResult(tx, {\n ...params,\n command: params.event.command,\n });\n if (existing) return existing;\n\n const rows = await tx\n .select()\n .from(adminGrants)\n .where(and(eq(adminGrants.id, params.grantId), isNull(adminGrants.revokedAt)))\n .limit(1);\n const grant = rows[0];\n if (!grant) throw new AdminGrantNotFoundError();\n\n if (grant.role === 'admin-owner') {\n const activeOwners = await tx\n .select({id: adminGrants.id})\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(\n and(\n eq(adminGrants.role, 'admin-owner'),\n isNull(adminGrants.revokedAt),\n eq(users.status, 'active'),\n ),\n );\n if (activeOwners.length <= 1) throw new LastAdminOwnerError();\n }\n\n const updated = await tx\n .update(adminGrants)\n .set({revokedAt: new Date(), updatedAt: new Date()})\n .where(and(eq(adminGrants.id, grant.id), isNull(adminGrants.revokedAt)))\n .returning();\n const updatedRow = updated[0];\n if (!updatedRow) throw new AdminGrantNotFoundError();\n\n const revoked = toAdminGrant(updatedRow);\n await writeAdminAction(tx, params.event);\n await storeCommandResult(tx, params, revoked);\n return revoked;\n });\n}\n"],"names":["writeOutboxEvent","and","asc","eq","isNull","sql","highestAdminRole","AdminBootstrapClosedError","AdminGrantAlreadyExistsError","AdminGrantNotFoundError","AdminIdempotencyKeyReuseError","LastAdminOwnerError","UserNotFoundError","db","adminCommandResults","adminGrants","toAdminGrant","authOutbox","users","createAdminGrant","params","rows","insert","values","userId","role","returning","row","Error","listAdminGrants","select","from","orderBy","createdAt","map","findCurrentAdminRole","innerJoin","id","where","revokedAt","status","revokeAdminGrant","transaction","tx","execute","grantId","limit","grant","undefined","activeOwners","length","updated","update","set","Date","updatedAt","toStoredAdminGrant","toISOString","fromStoredAdminGrant","findCommandResult","actorId","idempotencyKeyFingerprint","result","command","requestFingerprint","storeCommandResult","event","writeAdminAction","type","payload","lockAdminCommand","bootstrapFirstAdminOwner","existing","userRows","user","grantAdminRoleWithAudit","activeRows","revokeAdminGrantWithAudit","updatedRow","revoked"],"mappings":"AAKA,SAAQA,gBAAgB,QAAO,uBAAuB;AACtD,SAAQC,GAAG,EAAEC,GAAG,EAAEC,EAAE,EAAEC,MAAM,EAAEC,GAAG,QAAO,cAAc;AACtD,SAAQC,gBAAgB,QAAO,4BAA4B;AAE3D,SACEC,yBAAyB,EACzBC,4BAA4B,EAC5BC,uBAAuB,EACvBC,6BAA6B,EAC7BC,mBAAmB,EACnBC,iBAAiB,QACZ,kBAAkB;AACzB,SAAQC,EAAE,QAAO,UAAU;AAC3B,SAEEC,mBAAmB,QAEd,oCAAoC;AAC3C,SAAQC,WAAW,EAAEC,YAAY,QAAO,2BAA2B;AACnE,SAAQC,UAAU,QAAO,qBAAqB;AAC9C,SAAQC,KAAK,QAAO,oBAAoB;AASxC,OAAO,eAAeC,iBAAiBC,MAA8B;IACnE,MAAMC,OAAO,MAAMR,KAChBS,MAAM,CAACP,aACPQ,MAAM,CAAC;QAACC,QAAQJ,OAAOI,MAAM;QAAEC,MAAML,OAAOK,IAAI;IAAA,GAChDC,SAAS;IACZ,MAAMC,MAAMN,IAAI,CAAC,EAAE;IACnB,IAAI,CAACM,KAAK,MAAM,IAAIC,MAAM;IAC1B,OAAOZ,aAAaW;AACtB;AAEA,OAAO,eAAeE;IACpB,MAAMR,OAAO,MAAMR,KAAKiB,MAAM,GAAGC,IAAI,CAAChB,aAAaiB,OAAO,CAAC9B,IAAIa,YAAYkB,SAAS;IACpF,OAAOZ,KAAKa,GAAG,CAAClB;AAClB;AAEA,OAAO,eAAemB,qBAAqBf,MAAwB;IACjE,MAAMC,OAAO,MAAMR,KAChBiB,MAAM,CAAC;QAACL,MAAMV,YAAYU,IAAI;IAAA,GAC9BM,IAAI,CAAChB,aACLqB,SAAS,CAAClB,OAAOf,GAAGY,YAAYS,MAAM,EAAEN,MAAMmB,EAAE,GAChDC,KAAK,CACJrC,IACEE,GAAGY,YAAYS,MAAM,EAAEJ,OAAOI,MAAM,GACpCpB,OAAOW,YAAYwB,SAAS,GAC5BpC,GAAGe,MAAMsB,MAAM,EAAE;IAIvB,OAAOlC,iBAAiBe,KAAKa,GAAG,CAAC,CAAC,EAACT,IAAI,EAAC,GAAKA;AAC/C;AAEA,OAAO,eAAegB,iBAAiBrB,MAAyB;IAC9D,OAAO,MAAMP,KAAK6B,WAAW,CAAC,OAAOC;QACnC,0EAA0E;QAC1E,sEAAsE;QACtE,MAAMA,GAAGC,OAAO,CAACvC,GAAG,CAAC,iEAAiE,CAAC;QAEvF,MAAMgB,OAAO,MAAMsB,GAChBb,MAAM,GACNC,IAAI,CAAChB,aACLuB,KAAK,CAACrC,IAAIE,GAAGY,YAAYsB,EAAE,EAAEjB,OAAOyB,OAAO,GAAGzC,OAAOW,YAAYwB,SAAS,IAC1EO,KAAK,CAAC;QACT,MAAMC,QAAQ1B,IAAI,CAAC,EAAE;QACrB,IAAI,CAAC0B,OAAO,OAAOC;QAEnB,IAAID,MAAMtB,IAAI,KAAK,eAAe;YAChC,MAAMwB,eAAe,MAAMN,GACxBb,MAAM,CAAC;gBAACO,IAAItB,YAAYsB,EAAE;YAAA,GAC1BN,IAAI,CAAChB,aACLqB,SAAS,CAAClB,OAAOf,GAAGY,YAAYS,MAAM,EAAEN,MAAMmB,EAAE,GAChDC,KAAK,CACJrC,IACEE,GAAGY,YAAYU,IAAI,EAAE,gBACrBrB,OAAOW,YAAYwB,SAAS,GAC5BpC,GAAGe,MAAMsB,MAAM,EAAE;YAGvB,IAAIS,aAAaC,MAAM,IAAI,GAAG,MAAM,IAAIvC;QAC1C;QAEA,MAAMwC,UAAU,MAAMR,GACnBS,MAAM,CAACrC,aACPsC,GAAG,CAAC;YAACd,WAAW,IAAIe;YAAQC,WAAW,IAAID;QAAM,GACjDhB,KAAK,CAACrC,IAAIE,GAAGY,YAAYsB,EAAE,EAAEU,MAAMV,EAAE,GAAGjC,OAAOW,YAAYwB,SAAS,IACpEb,SAAS;QACZ,MAAMC,MAAMwB,OAAO,CAAC,EAAE;QACtB,OAAOxB,MAAMX,aAAaW,OAAOqB;IACnC;AACF;AASA,SAASQ,mBAAmBT,KAAiB;IAC3C,OAAO;QACLV,IAAIU,MAAMV,EAAE;QACZb,QAAQuB,MAAMvB,MAAM;QACpBC,MAAMsB,MAAMtB,IAAI;QAChBc,WAAWQ,MAAMR,SAAS,EAAEkB,iBAAiB;QAC7CxB,WAAWc,MAAMd,SAAS,CAACwB,WAAW;QACtCF,WAAWR,MAAMQ,SAAS,CAACE,WAAW;IACxC;AACF;AAEA,SAASC,qBAAqBX,KAAuB;IACnD,OAAO;QACLV,IAAIU,MAAMV,EAAE;QACZb,QAAQuB,MAAMvB,MAAM;QACpBC,MAAMsB,MAAMtB,IAAI;QAChBc,WAAWQ,MAAMR,SAAS,GAAG,IAAIe,KAAKP,MAAMR,SAAS,IAAI;QACzDN,WAAW,IAAIqB,KAAKP,MAAMd,SAAS;QACnCsB,WAAW,IAAID,KAAKP,MAAMQ,SAAS;IACrC;AACF;AAEA,eAAeI,kBACbhB,EAAM,EACNvB,MAKC;IAED,MAAMC,OAAO,MAAMsB,GAChBb,MAAM,GACNC,IAAI,CAACjB,qBACLwB,KAAK,CACJrC,IACEE,GAAGW,oBAAoB8C,OAAO,EAAExC,OAAOwC,OAAO,GAC9CzD,GAAGW,oBAAoB+C,yBAAyB,EAAEzC,OAAOyC,yBAAyB,IAGrFf,KAAK,CAAC;IACT,MAAMgB,SAA2CzC,IAAI,CAAC,EAAE;IACxD,IAAI,CAACyC,QAAQ,OAAOd;IACpB,IACEc,OAAOC,OAAO,KAAK3C,OAAO2C,OAAO,IACjCD,OAAOE,kBAAkB,KAAK5C,OAAO4C,kBAAkB,EACvD;QACA,MAAM,IAAItD;IACZ;IACA,OAAOgD,qBAAqBI,OAAOA,MAAM,CAACf,KAAK;AACjD;AAEA,eAAekB,mBACbtB,EAAM,EACNvB,MAAiC,EACjC2B,KAAiB;IAEjB,MAAMJ,GAAGrB,MAAM,CAACR,qBAAqBS,MAAM,CAAC;QAC1CqC,SAASxC,OAAOwC,OAAO;QACvBC,2BAA2BzC,OAAOyC,yBAAyB;QAC3DE,SAAS3C,OAAO8C,KAAK,CAACH,OAAO;QAC7BC,oBAAoB5C,OAAO4C,kBAAkB;QAC7CF,QAAQ;YAACf,OAAOS,mBAAmBT;QAAM;IAC3C;AACF;AAEA,eAAeoB,iBAAiBxB,EAAM,EAAEuB,KAAgC;IACtE,MAAMlE,iBAA+C2C,IAAI1B,YAAY;QACnEmD,MAAM;QACNC,SAASH;IACX;AACF;AAEA,eAAeI,iBACb3B,EAAM,EACNvB,MAA4D;IAE5D,MAAMuB,GAAGC,OAAO,CACdvC,GAAG,CAAC,sCAAsC,EAAE,CAAC,mBAAmB,EAAEe,OAAOwC,OAAO,CAAC,CAAC,EAAExC,OAAOyC,yBAAyB,EAAE,CAAC,EAAE,CAAC;AAE9H;AAEA,OAAO,eAAeU,yBACpBnD,MAAoD;IAEpD,OAAO,MAAMP,KAAK6B,WAAW,CAAC,OAAOC;QACnC,MAAM2B,iBAAiB3B,IAAIvB;QAC3B,MAAMuB,GAAGC,OAAO,CAACvC,GAAG,CAAC,iEAAiE,CAAC;QAEvF,MAAMmE,WAAW,MAAMb,kBAAkBhB,IAAI;YAC3C,GAAGvB,MAAM;YACT2C,SAAS3C,OAAO8C,KAAK,CAACH,OAAO;QAC/B;QACA,IAAIS,UAAU,OAAOA;QAErB,MAAMvB,eAAe,MAAMN,GACxBb,MAAM,CAAC;YAACO,IAAItB,YAAYsB,EAAE;QAAA,GAC1BN,IAAI,CAAChB,aACLqB,SAAS,CAAClB,OAAOf,GAAGY,YAAYS,MAAM,EAAEN,MAAMmB,EAAE,GAChDC,KAAK,CACJrC,IACEE,GAAGY,YAAYU,IAAI,EAAE,gBACrBrB,OAAOW,YAAYwB,SAAS,GAC5BpC,GAAGe,MAAMsB,MAAM,EAAE,YAGpBM,KAAK,CAAC;QACT,IAAIG,aAAaC,MAAM,GAAG,GAAG,MAAM,IAAI3C;QAEvC,MAAMkE,WAAW,MAAM9B,GACpBb,MAAM,CAAC;YAACO,IAAInB,MAAMmB,EAAE;YAAEG,QAAQtB,MAAMsB,MAAM;QAAA,GAC1CT,IAAI,CAACb,OACLoB,KAAK,CAACnC,GAAGe,MAAMmB,EAAE,EAAEjB,OAAOI,MAAM,GAChCsB,KAAK,CAAC;QACT,MAAM4B,OAAOD,QAAQ,CAAC,EAAE;QACxB,IAAIC,MAAMlC,WAAW,UAAU,MAAM,IAAI5B,kBAAkBQ,OAAOI,MAAM;QAExE,MAAMH,OAAO,MAAMsB,GAChBrB,MAAM,CAACP,aACPQ,MAAM,CAAC;YAACC,QAAQJ,OAAOI,MAAM;YAAEC,MAAM;QAAa,GAClDC,SAAS;QACZ,MAAMC,MAAMN,IAAI,CAAC,EAAE;QACnB,IAAI,CAACM,KAAK,MAAM,IAAIC,MAAM;QAE1B,MAAMmB,QAAQ/B,aAAaW;QAC3B,MAAMwC,iBAAiBxB,IAAIvB,OAAO8C,KAAK;QACvC,MAAMD,mBAAmBtB,IAAIvB,QAAQ2B;QACrC,OAAOA;IACT;AACF;AAEA,OAAO,eAAe4B,wBACpBvD,MAAqE;IAErE,OAAO,MAAMP,KAAK6B,WAAW,CAAC,OAAOC;QACnC,MAAM2B,iBAAiB3B,IAAIvB;QAC3B,MAAMuB,GAAGC,OAAO,CAACvC,GAAG,CAAC,iEAAiE,CAAC;QAEvF,MAAMmE,WAAW,MAAMb,kBAAkBhB,IAAI;YAC3C,GAAGvB,MAAM;YACT2C,SAAS3C,OAAO8C,KAAK,CAACH,OAAO;QAC/B;QACA,IAAIS,UAAU,OAAOA;QAErB,MAAMC,WAAW,MAAM9B,GACpBb,MAAM,CAAC;YAACO,IAAInB,MAAMmB,EAAE;YAAEG,QAAQtB,MAAMsB,MAAM;QAAA,GAC1CT,IAAI,CAACb,OACLoB,KAAK,CAACnC,GAAGe,MAAMmB,EAAE,EAAEjB,OAAOI,MAAM,GAChCsB,KAAK,CAAC;QACT,MAAM4B,OAAOD,QAAQ,CAAC,EAAE;QACxB,IAAIC,MAAMlC,WAAW,UAAU,MAAM,IAAI5B,kBAAkBQ,OAAOI,MAAM;QAExE,MAAMoD,aAAa,MAAMjC,GACtBb,MAAM,CAAC;YAACO,IAAItB,YAAYsB,EAAE;QAAA,GAC1BN,IAAI,CAAChB,aACLuB,KAAK,CACJrC,IACEE,GAAGY,YAAYS,MAAM,EAAEJ,OAAOI,MAAM,GACpCrB,GAAGY,YAAYU,IAAI,EAAEL,OAAOK,IAAI,GAChCrB,OAAOW,YAAYwB,SAAS,IAG/BO,KAAK,CAAC;QACT,IAAI8B,WAAW1B,MAAM,GAAG,GAAG,MAAM,IAAI1C;QAErC,MAAMa,OAAO,MAAMsB,GAChBrB,MAAM,CAACP,aACPQ,MAAM,CAAC;YAACC,QAAQJ,OAAOI,MAAM;YAAEC,MAAML,OAAOK,IAAI;QAAA,GAChDC,SAAS;QACZ,MAAMC,MAAMN,IAAI,CAAC,EAAE;QACnB,IAAI,CAACM,KAAK,MAAM,IAAIC,MAAM;QAE1B,MAAMmB,QAAQ/B,aAAaW;QAC3B,MAAMwC,iBAAiBxB,IAAIvB,OAAO8C,KAAK;QACvC,MAAMD,mBAAmBtB,IAAIvB,QAAQ2B;QACrC,OAAOA;IACT;AACF;AAEA,OAAO,eAAe8B,0BACpBzD,MAAqD;IAErD,OAAO,MAAMP,KAAK6B,WAAW,CAAC,OAAOC;QACnC,MAAM2B,iBAAiB3B,IAAIvB;QAC3B,MAAMuB,GAAGC,OAAO,CAACvC,GAAG,CAAC,iEAAiE,CAAC;QAEvF,MAAMmE,WAAW,MAAMb,kBAAkBhB,IAAI;YAC3C,GAAGvB,MAAM;YACT2C,SAAS3C,OAAO8C,KAAK,CAACH,OAAO;QAC/B;QACA,IAAIS,UAAU,OAAOA;QAErB,MAAMnD,OAAO,MAAMsB,GAChBb,MAAM,GACNC,IAAI,CAAChB,aACLuB,KAAK,CAACrC,IAAIE,GAAGY,YAAYsB,EAAE,EAAEjB,OAAOyB,OAAO,GAAGzC,OAAOW,YAAYwB,SAAS,IAC1EO,KAAK,CAAC;QACT,MAAMC,QAAQ1B,IAAI,CAAC,EAAE;QACrB,IAAI,CAAC0B,OAAO,MAAM,IAAItC;QAEtB,IAAIsC,MAAMtB,IAAI,KAAK,eAAe;YAChC,MAAMwB,eAAe,MAAMN,GACxBb,MAAM,CAAC;gBAACO,IAAItB,YAAYsB,EAAE;YAAA,GAC1BN,IAAI,CAAChB,aACLqB,SAAS,CAAClB,OAAOf,GAAGY,YAAYS,MAAM,EAAEN,MAAMmB,EAAE,GAChDC,KAAK,CACJrC,IACEE,GAAGY,YAAYU,IAAI,EAAE,gBACrBrB,OAAOW,YAAYwB,SAAS,GAC5BpC,GAAGe,MAAMsB,MAAM,EAAE;YAGvB,IAAIS,aAAaC,MAAM,IAAI,GAAG,MAAM,IAAIvC;QAC1C;QAEA,MAAMwC,UAAU,MAAMR,GACnBS,MAAM,CAACrC,aACPsC,GAAG,CAAC;YAACd,WAAW,IAAIe;YAAQC,WAAW,IAAID;QAAM,GACjDhB,KAAK,CAACrC,IAAIE,GAAGY,YAAYsB,EAAE,EAAEU,MAAMV,EAAE,GAAGjC,OAAOW,YAAYwB,SAAS,IACpEb,SAAS;QACZ,MAAMoD,aAAa3B,OAAO,CAAC,EAAE;QAC7B,IAAI,CAAC2B,YAAY,MAAM,IAAIrE;QAE3B,MAAMsE,UAAU/D,aAAa8D;QAC7B,MAAMX,iBAAiBxB,IAAIvB,OAAO8C,KAAK;QACvC,MAAMD,mBAAmBtB,IAAIvB,QAAQ2D;QACrC,OAAOA;IACT;AACF"}
|