@revealui/auth 0.5.3 → 0.5.5

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,34 @@
1
+ /**
2
+ * GAP-256 PR-4 — admission_waitlist I/O (NOT marketing waitlist).
3
+ *
4
+ * Stores only SHA-256 of claim tokens. Raw token returned once at enqueue.
5
+ */
6
+ import { type AdmissionWaitlistRow } from '@revealui/db/schema';
7
+ export declare function hashAdmissionToken(raw: string): string;
8
+ export declare function generateAdmissionToken(): string;
9
+ export interface EnqueueAdmissionWaitlistParams {
10
+ email: string;
11
+ snapshotId: string | null;
12
+ modeAtEnqueue?: string;
13
+ source?: string;
14
+ now?: Date;
15
+ }
16
+ export interface EnqueueAdmissionWaitlistResult {
17
+ waitlistToken: string;
18
+ positionEstimate: number | null;
19
+ id: string;
20
+ }
21
+ /**
22
+ * Insert or rotate a pending admission waitlist row for email.
23
+ * Returns a fresh raw token (never stored).
24
+ */
25
+ export declare function enqueueAdmissionWaitlist(params: EnqueueAdmissionWaitlistParams): Promise<EnqueueAdmissionWaitlistResult>;
26
+ /** Active claim rows: pending or invited, not past expiresAt. */
27
+ export declare function getAdmissionWaitlistByToken(rawToken: string): Promise<AdmissionWaitlistRow | null>;
28
+ export declare function getAdmissionWaitlistByTokenAnyStatus(rawToken: string): Promise<AdmissionWaitlistRow | null>;
29
+ export declare function markAdmissionWaitlistConverted(id: string, now?: Date): Promise<void>;
30
+ /** Mask email for public status (anti-enumeration still requires valid token). */
31
+ export declare function maskAdmissionEmail(email: string): string;
32
+ /** Position estimate among pending (best-effort; null on failure). */
33
+ export declare function estimateAdmissionWaitlistPosition(id: string): Promise<number | null>;
34
+ //# sourceMappingURL=admission-waitlist.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admission-waitlist.d.ts","sourceRoot":"","sources":["../../src/server/admission-waitlist.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EAAE,KAAK,oBAAoB,EAAqB,MAAM,qBAAqB,CAAC;AAKnF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AA0BD,MAAM,WAAW,8BAA8B;IAC7C,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;AAED,MAAM,WAAW,8BAA8B;IAC7C,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,EAAE,EAAE,MAAM,CAAC;CACZ;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,8BAA8B,GACrC,OAAO,CAAC,8BAA8B,CAAC,CA8FzC;AAED,iEAAiE;AACjE,wBAAsB,2BAA2B,CAC/C,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAqBtC;AAED,wBAAsB,oCAAoC,CACxD,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAYtC;AAED,wBAAsB,8BAA8B,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAU1F;AAED,kFAAkF;AAClF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUxD;AAED,sEAAsE;AACtE,wBAAsB,iCAAiC,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA2B1F"}
@@ -0,0 +1,210 @@
1
+ /**
2
+ * GAP-256 PR-4 — admission_waitlist I/O (NOT marketing waitlist).
3
+ *
4
+ * Stores only SHA-256 of claim tokens. Raw token returned once at enqueue.
5
+ */
6
+ import { createHash, randomBytes } from 'node:crypto';
7
+ import { logger } from '@revealui/core/observability/logger';
8
+ import { getClient } from '@revealui/db';
9
+ import { admissionWaitlist } from '@revealui/db/schema';
10
+ import { and, count, eq, gt, inArray, isNull, or, sql } from 'drizzle-orm';
11
+ const TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000;
12
+ export function hashAdmissionToken(raw) {
13
+ return createHash('sha256').update(raw).digest('hex');
14
+ }
15
+ export function generateAdmissionToken() {
16
+ return randomBytes(32).toString('hex');
17
+ }
18
+ function normalizeEmail(email) {
19
+ return email.trim().toLowerCase();
20
+ }
21
+ function expiresAtFrom(now) {
22
+ return new Date(now.getTime() + TOKEN_TTL_MS);
23
+ }
24
+ async function countPendingRows() {
25
+ try {
26
+ const db = getClient();
27
+ const [row] = await db
28
+ .select({ total: count() })
29
+ .from(admissionWaitlist)
30
+ .where(eq(admissionWaitlist.status, 'pending'));
31
+ return row?.total ?? 0;
32
+ }
33
+ catch (err) {
34
+ logger.warn('[admission-waitlist] pending count failed', {
35
+ error: err instanceof Error ? err.message : String(err),
36
+ });
37
+ return null;
38
+ }
39
+ }
40
+ /**
41
+ * Insert or rotate a pending admission waitlist row for email.
42
+ * Returns a fresh raw token (never stored).
43
+ */
44
+ export async function enqueueAdmissionWaitlist(params) {
45
+ const db = getClient();
46
+ const email = normalizeEmail(params.email);
47
+ const now = params.now ?? new Date();
48
+ const rawToken = generateAdmissionToken();
49
+ const tokenHash = hashAdmissionToken(rawToken);
50
+ const expiresAt = expiresAtFrom(now);
51
+ const modeAtEnqueue = params.modeAtEnqueue ?? 'waitlist';
52
+ const source = params.source ?? 'free_signup';
53
+ const [existing] = await db
54
+ .select({
55
+ id: admissionWaitlist.id,
56
+ position: admissionWaitlist.position,
57
+ })
58
+ .from(admissionWaitlist)
59
+ .where(and(eq(admissionWaitlist.email, email), eq(admissionWaitlist.status, 'pending')))
60
+ .limit(1);
61
+ if (existing) {
62
+ await db
63
+ .update(admissionWaitlist)
64
+ .set({
65
+ tokenHash,
66
+ snapshotId: params.snapshotId,
67
+ modeAtEnqueue,
68
+ source,
69
+ expiresAt,
70
+ })
71
+ .where(eq(admissionWaitlist.id, existing.id));
72
+ return {
73
+ waitlistToken: rawToken,
74
+ positionEstimate: existing.position,
75
+ id: existing.id,
76
+ };
77
+ }
78
+ const pendingCount = await countPendingRows();
79
+ const positionEstimate = pendingCount === null ? null : pendingCount + 1;
80
+ const id = crypto.randomUUID();
81
+ try {
82
+ await db.insert(admissionWaitlist).values({
83
+ id,
84
+ email,
85
+ status: 'pending',
86
+ tokenHash,
87
+ snapshotId: params.snapshotId,
88
+ modeAtEnqueue,
89
+ position: positionEstimate,
90
+ source,
91
+ metadata: {},
92
+ createdAt: now,
93
+ expiresAt,
94
+ });
95
+ }
96
+ catch (err) {
97
+ // Race on unique pending email: rotate the winning row's token.
98
+ logger.warn('[admission-waitlist] insert conflict; rotating existing pending', {
99
+ error: err instanceof Error ? err.message : String(err),
100
+ });
101
+ const [raced] = await db
102
+ .select({
103
+ id: admissionWaitlist.id,
104
+ position: admissionWaitlist.position,
105
+ })
106
+ .from(admissionWaitlist)
107
+ .where(and(eq(admissionWaitlist.email, email), eq(admissionWaitlist.status, 'pending')))
108
+ .limit(1);
109
+ if (!raced) {
110
+ throw err instanceof Error ? err : new Error(String(err));
111
+ }
112
+ await db
113
+ .update(admissionWaitlist)
114
+ .set({
115
+ tokenHash,
116
+ snapshotId: params.snapshotId,
117
+ modeAtEnqueue,
118
+ source,
119
+ expiresAt,
120
+ })
121
+ .where(eq(admissionWaitlist.id, raced.id));
122
+ return {
123
+ waitlistToken: rawToken,
124
+ positionEstimate: raced.position,
125
+ id: raced.id,
126
+ };
127
+ }
128
+ return {
129
+ waitlistToken: rawToken,
130
+ positionEstimate,
131
+ id,
132
+ };
133
+ }
134
+ /** Active claim rows: pending or invited, not past expiresAt. */
135
+ export async function getAdmissionWaitlistByToken(rawToken) {
136
+ if (!rawToken || rawToken.trim() === '') {
137
+ return null;
138
+ }
139
+ const db = getClient();
140
+ const tokenHash = hashAdmissionToken(rawToken);
141
+ const now = new Date();
142
+ const [row] = await db
143
+ .select()
144
+ .from(admissionWaitlist)
145
+ .where(and(eq(admissionWaitlist.tokenHash, tokenHash), inArray(admissionWaitlist.status, ['pending', 'invited']), or(isNull(admissionWaitlist.expiresAt), gt(admissionWaitlist.expiresAt, now))))
146
+ .limit(1);
147
+ return row ?? null;
148
+ }
149
+ export async function getAdmissionWaitlistByTokenAnyStatus(rawToken) {
150
+ if (!rawToken || rawToken.trim() === '') {
151
+ return null;
152
+ }
153
+ const db = getClient();
154
+ const tokenHash = hashAdmissionToken(rawToken);
155
+ const [row] = await db
156
+ .select()
157
+ .from(admissionWaitlist)
158
+ .where(eq(admissionWaitlist.tokenHash, tokenHash))
159
+ .limit(1);
160
+ return row ?? null;
161
+ }
162
+ export async function markAdmissionWaitlistConverted(id, now) {
163
+ const db = getClient();
164
+ const convertedAt = now ?? new Date();
165
+ await db
166
+ .update(admissionWaitlist)
167
+ .set({
168
+ status: 'converted',
169
+ convertedAt,
170
+ })
171
+ .where(eq(admissionWaitlist.id, id));
172
+ }
173
+ /** Mask email for public status (anti-enumeration still requires valid token). */
174
+ export function maskAdmissionEmail(email) {
175
+ const normalized = normalizeEmail(email);
176
+ const at = normalized.indexOf('@');
177
+ if (at <= 0) {
178
+ return '***';
179
+ }
180
+ const local = normalized.slice(0, at);
181
+ const domain = normalized.slice(at + 1);
182
+ const visible = local.slice(0, 1);
183
+ return `${visible}***@${domain}`;
184
+ }
185
+ /** Position estimate among pending (best-effort; null on failure). */
186
+ export async function estimateAdmissionWaitlistPosition(id) {
187
+ try {
188
+ const db = getClient();
189
+ const [self] = await db
190
+ .select({
191
+ position: admissionWaitlist.position,
192
+ createdAt: admissionWaitlist.createdAt,
193
+ })
194
+ .from(admissionWaitlist)
195
+ .where(eq(admissionWaitlist.id, id))
196
+ .limit(1);
197
+ if (!self)
198
+ return null;
199
+ if (self.position != null)
200
+ return self.position;
201
+ const [row] = await db
202
+ .select({ total: count() })
203
+ .from(admissionWaitlist)
204
+ .where(and(eq(admissionWaitlist.status, 'pending'), sql `${admissionWaitlist.createdAt} <= ${self.createdAt}`));
205
+ return row?.total ?? null;
206
+ }
207
+ catch {
208
+ return null;
209
+ }
210
+ }
@@ -66,4 +66,36 @@ export declare function auditSessionRevoked(userId: string, sessionId: string, i
66
66
  * @param failedAttempts - Number of failed attempts that triggered the lockout
67
67
  */
68
68
  export declare function auditAccountLocked(email: string, ip: string, failedAttempts: number): Promise<void>;
69
+ /**
70
+ * Structured fields required by GAP-464 / #449 Audit wiring.
71
+ * `userId` may be absent on pre-JIT login failures.
72
+ */
73
+ export interface SsoAuditContext {
74
+ providerType?: string;
75
+ accountId?: string;
76
+ userId?: string;
77
+ issuer?: string;
78
+ providerId?: string;
79
+ ip?: string;
80
+ userAgent?: string;
81
+ }
82
+ /**
83
+ * Record a successful Enterprise SSO login (OIDC or SAML ACS).
84
+ * Persists via the audit door before the HTTP response returns.
85
+ */
86
+ export declare function auditSsoLoginSuccess(context: SsoAuditContext): Promise<void>;
87
+ /**
88
+ * Record a failed Enterprise SSO login attempt.
89
+ * Persists via the audit door before the HTTP response returns.
90
+ */
91
+ export declare function auditSsoLoginFailure(context: SsoAuditContext & {
92
+ reason: string;
93
+ }): Promise<void>;
94
+ /**
95
+ * Record an SSO provider create / update / delete (or equivalent mapping change).
96
+ * Persists via the audit door before the HTTP response returns.
97
+ */
98
+ export declare function auditSsoConfigChanged(context: SsoAuditContext & {
99
+ action: string;
100
+ }): Promise<void>;
69
101
  //# sourceMappingURL=audit-bridge.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"audit-bridge.d.ts","sourceRoot":"","sources":["../../src/server/audit-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA2CH;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,MAAM,EACV,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CASf;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,MAAM,EACV,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAUf;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CASnF;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAUjF;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS/E;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAShF;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,IAAI,CAAC,CAUf;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,MAAM,EACV,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,IAAI,CAAC,CAUf"}
1
+ {"version":3,"file":"audit-bridge.d.ts","sourceRoot":"","sources":["../../src/server/audit-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA2CH;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,MAAM,EACV,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CASf;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,MAAM,EACV,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAUf;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CASnF;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAUjF;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS/E;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAShF;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,IAAI,CAAC,CAUf;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,MAAM,EACV,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,IAAI,CAAC,CAUf;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAYD;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBlF;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,eAAe,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAC5C,OAAO,CAAC,IAAI,CAAC,CAiBf;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,eAAe,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAC5C,OAAO,CAAC,IAAI,CAAC,CAiBf"}
@@ -179,3 +179,78 @@ export async function auditAccountLocked(email, ip, failedAttempts) {
179
179
  metadata: { email, failedAttempts },
180
180
  });
181
181
  }
182
+ function ssoMetadata(context) {
183
+ return {
184
+ providerType: context.providerType ?? null,
185
+ accountId: context.accountId ?? null,
186
+ userId: context.userId ?? null,
187
+ issuer: context.issuer ?? null,
188
+ providerId: context.providerId ?? null,
189
+ };
190
+ }
191
+ /**
192
+ * Record a successful Enterprise SSO login (OIDC or SAML ACS).
193
+ * Persists via the audit door before the HTTP response returns.
194
+ */
195
+ export async function auditSsoLoginSuccess(context) {
196
+ const actorId = context.userId ?? context.accountId ?? 'unknown';
197
+ await logAuditEvent({
198
+ type: 'sso_login_success',
199
+ severity: 'low',
200
+ actor: {
201
+ id: actorId,
202
+ type: 'user',
203
+ ip: context.ip,
204
+ userAgent: context.userAgent,
205
+ },
206
+ resource: context.providerId ? { type: 'sso_provider', id: context.providerId } : undefined,
207
+ action: 'sso_login',
208
+ result: 'success',
209
+ message: 'SSO login succeeded',
210
+ metadata: ssoMetadata(context),
211
+ });
212
+ }
213
+ /**
214
+ * Record a failed Enterprise SSO login attempt.
215
+ * Persists via the audit door before the HTTP response returns.
216
+ */
217
+ export async function auditSsoLoginFailure(context) {
218
+ const actorId = context.userId ?? context.accountId ?? 'unknown';
219
+ await logAuditEvent({
220
+ type: 'sso_login_failure',
221
+ severity: 'medium',
222
+ actor: {
223
+ id: actorId,
224
+ type: 'user',
225
+ ip: context.ip,
226
+ userAgent: context.userAgent,
227
+ },
228
+ resource: context.providerId ? { type: 'sso_provider', id: context.providerId } : undefined,
229
+ action: 'sso_login',
230
+ result: 'failure',
231
+ message: `SSO login failed: ${context.reason}`,
232
+ metadata: { ...ssoMetadata(context), reason: context.reason },
233
+ });
234
+ }
235
+ /**
236
+ * Record an SSO provider create / update / delete (or equivalent mapping change).
237
+ * Persists via the audit door before the HTTP response returns.
238
+ */
239
+ export async function auditSsoConfigChanged(context) {
240
+ const actorId = context.userId ?? context.accountId ?? 'unknown';
241
+ await logAuditEvent({
242
+ type: 'sso_config_changed',
243
+ severity: 'medium',
244
+ actor: {
245
+ id: actorId,
246
+ type: 'user',
247
+ ip: context.ip,
248
+ userAgent: context.userAgent,
249
+ },
250
+ resource: context.providerId ? { type: 'sso_provider', id: context.providerId } : undefined,
251
+ action: context.action,
252
+ result: 'success',
253
+ message: `SSO provider ${context.action}`,
254
+ metadata: { ...ssoMetadata(context), action: context.action },
255
+ });
256
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/server/auth.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAQ,MAAM,aAAa,CAAC;AAWpE;;;;;;;GAOG;AACH,wBAAsB,MAAM,CAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GACA,OAAO,CAAC,YAAY,CAAC,CA0PvB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAoBtD;AAED;;;;;;;;GAQG;AACH,wBAAsB,MAAM,CAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GACA,OAAO,CAAC,YAAY,CAAC,CAyOvB"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/server/auth.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAQ,MAAM,aAAa,CAAC;AAWpE;;;;;;;GAOG;AACH,wBAAsB,MAAM,CAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GACA,OAAO,CAAC,YAAY,CAAC,CA0QvB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAoBtD;AAED;;;;;;;;GAQG;AACH,wBAAsB,MAAM,CAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GACA,OAAO,CAAC,YAAY,CAAC,CAyOvB"}
@@ -192,6 +192,7 @@ export async function signIn(email, password, options) {
192
192
  // Rotate session: delete all existing sessions for this user, then create
193
193
  // a fresh one. This prevents session fixation attacks where an attacker
194
194
  // plants a session token that the victim later authenticates.
195
+ // createSession also repairs membership-owner shell role (GAP-473).
195
196
  let token;
196
197
  try {
197
198
  const sessionResult = await rotateSession(user.id, {
@@ -208,6 +209,21 @@ export async function signIn(email, password, options) {
208
209
  error: 'Failed to create session',
209
210
  };
210
211
  }
212
+ // Re-read role after session mint so revealui-role cookie matches DB when
213
+ // ensureShellAdminIfAccountOwner promoted viewer → admin (GAP-473).
214
+ try {
215
+ const [roleRow] = await db
216
+ .select({ role: users.role })
217
+ .from(users)
218
+ .where(eq(users.id, user.id))
219
+ .limit(1);
220
+ if (roleRow && roleRow.role !== user.role) {
221
+ user = { ...user, role: roleRow.role };
222
+ }
223
+ }
224
+ catch {
225
+ // Cookie may lag one login; next getSession still sees repaired DB role.
226
+ }
211
227
  // A fresh session was minted: the login succeeded. Land a success receipt
212
228
  // in the audit trail (best-effort; auditLoginSuccess never throws, so an
213
229
  // audit-write failure cannot fail the login).
@@ -5,12 +5,14 @@
5
5
  * Inspired by Better Auth and Neon Auth patterns.
6
6
  */
7
7
  export type { SignInResult, SignUpResult } from '../types.js';
8
- export { auditAccountLocked, auditLoginFailure, auditLoginSuccess, auditMfaDisabled, auditMfaEnabled, auditPasswordChange, auditPasswordReset, auditSessionRevoked, } from './audit-bridge.js';
8
+ export { type EnqueueAdmissionWaitlistParams, type EnqueueAdmissionWaitlistResult, enqueueAdmissionWaitlist, estimateAdmissionWaitlistPosition, generateAdmissionToken, getAdmissionWaitlistByToken, getAdmissionWaitlistByTokenAnyStatus, hashAdmissionToken, markAdmissionWaitlistConverted, maskAdmissionEmail, } from './admission-waitlist.js';
9
+ export { auditAccountLocked, auditLoginFailure, auditLoginSuccess, auditMfaDisabled, auditMfaEnabled, auditPasswordChange, auditPasswordReset, auditSessionRevoked, auditSsoConfigChanged, auditSsoLoginFailure, auditSsoLoginSuccess, type SsoAuditContext, } from './audit-bridge.js';
9
10
  export { isSignupAllowed, signIn, signUp } from './auth.js';
10
11
  export { clearFailedAttempts, configureBruteForce, getFailedAttemptCount, isAccountLocked, recordFailedAttempt, resetBruteForceConfig, } from './brute-force.js';
11
12
  export { AuthError, AuthenticationError, DatabaseError, OAuthAccountConflictError, SessionError, TokenError, } from './errors.js';
12
13
  export type { MagicLinkConfig } from './magic-link.js';
13
14
  export { configureMagicLink, createMagicLink, resetMagicLinkConfig, verifyMagicLink, } from './magic-link.js';
15
+ export { type AdmitFreeIntakeInput, type AdmitFreeIntakeResult, admitFreeIntake, ensureFreeSignupEntitlement, OPEN_FREE_LIMITS, provisionHostedPersonalAccount, } from './margin-admit.js';
14
16
  export type { MFAConfig, MFADisableProof, MFASetupResult } from './mfa.js';
15
17
  export { configureMFA, disableMFA, initiateMFASetup, isMFAEnabled, regenerateBackupCodes, resetMFAConfig, verifyBackupCode, verifyMFACode, verifyMFASetup, } from './mfa.js';
16
18
  export type { MfaCheckResult, MfaEnforcementOptions, MfaErrorResponse, MfaRequest, MfaSession, MfaSessionUser, } from './mfa-enforcement.js';
@@ -21,7 +23,8 @@ export { configurePasskey, countUserCredentials, deletePasskey, generateAuthenti
21
23
  export type { ChangePasswordResult, PasswordResetResult, PasswordResetToken, } from './password-reset.js';
22
24
  export { changePassword, generatePasswordResetToken, invalidatePasswordResetToken, resetPasswordWithToken, validatePasswordResetToken, } from './password-reset.js';
23
25
  export { meetsMinimumPasswordRequirements, validatePasswordStrength, } from './password-validation.js';
24
- export { ensureAccountOwnerPlatformAdmin, isPlatformShellAdminRole, PLATFORM_SHELL_ADMIN_COOKIE_ROLES, PLATFORM_SHELL_ADMIN_ROLES, platformRoleForAccountOwner, } from './platform-roles.js';
26
+ export { ensurePlatformOperatorEntitlement, isPlatformOperatorUser, PLATFORM_OPERATOR_LIMITS, type PlatformOperatorUserView, rolesFromUserJson, } from './platform-operator.js';
27
+ export { ensureAccountOwnerPlatformAdmin, ensureShellAdminIfAccountOwner, isPlatformShellAdminRole, PLATFORM_SHELL_ADMIN_COOKIE_ROLES, PLATFORM_SHELL_ADMIN_ROLES, platformRoleForAccountOwner, readUsersRole, } from './platform-roles.js';
25
28
  export { checkRateLimit, configureRateLimit, getRateLimitStatus, resetRateLimit, resetRateLimitConfig, } from './rate-limit.js';
26
29
  export type { RequestContext, SessionBindingConfig, SessionData } from './session.js';
27
30
  export { configureSessionBinding, createSession, deleteAllUserSessions, deleteOtherUserSessions, deleteSession, getSession, isRecoverySession, resetSessionBindingConfig, rotateSession, validateSessionBinding, } from './session.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE9D,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,mBAAmB,CAAC;AAO3B,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAC5D,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,eAAe,EACf,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,SAAS,EACT,mBAAmB,EACnB,aAAa,EACb,yBAAyB,EACzB,YAAY,EACZ,UAAU,GACX,MAAM,aAAa,CAAC;AAErB,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,oBAAoB,EACpB,eAAe,GAChB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC3E,OAAO,EACL,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,qBAAqB,EACrB,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,cAAc,GACf,MAAM,UAAU,CAAC;AAElB,YAAY,EACV,cAAc,EACd,qBAAqB,EACrB,gBAAgB,EAChB,UAAU,EACV,UAAU,EACV,cAAc,GACf,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACjF,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,kBAAkB,EAClB,eAAe,EACf,gBAAgB,GACjB,MAAM,YAAY,CAAC;AAEpB,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,aAAa,EACb,+BAA+B,EAC/B,6BAA6B,EAC7B,YAAY,EACZ,aAAa,EACb,kBAAkB,EAClB,YAAY,EACZ,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,cAAc,EACd,0BAA0B,EAC1B,4BAA4B,EAC5B,sBAAsB,EACtB,0BAA0B,GAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,gCAAgC,EAChC,wBAAwB,GACzB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,+BAA+B,EAC/B,wBAAwB,EACxB,iCAAiC,EACjC,0BAA0B,EAC1B,2BAA2B,GAC5B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,cAAc,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACtF,OAAO,EACL,uBAAuB,EACvB,aAAa,EACb,qBAAqB,EACrB,uBAAuB,EACvB,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,yBAAyB,EACzB,aAAa,EACb,sBAAsB,GACvB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE5E,OAAO,EACL,KAAK,8BAA8B,EACnC,KAAK,kCAAkC,EACvC,KAAK,2BAA2B,EAChC,yBAAyB,EACzB,qBAAqB,EACrB,mBAAmB,EACnB,sBAAsB,EACtB,KAAK,6BAA6B,EAClC,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,gBAAgB,EAChB,sBAAsB,EACtB,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,EAC7B,gBAAgB,EAChB,kBAAkB,EAClB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,gBAAgB,EAChB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,OAAO,EACZ,KAAK,yBAAyB,EAC9B,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,KAAK,qBAAqB,EAC1B,KAAK,0BAA0B,EAC/B,KAAK,6BAA6B,EAClC,KAAK,sBAAsB,EAC3B,mBAAmB,EACnB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,aAAa,EACb,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,4BAA4B,EACjC,KAAK,qBAAqB,EAC1B,KAAK,0BAA0B,EAC/B,KAAK,iCAAiC,EACtC,KAAK,0BAA0B,EAC/B,KAAK,gBAAgB,EACrB,mBAAmB,EACnB,wBAAwB,EACxB,cAAc,GACf,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EACL,aAAa,EACb,eAAe,EACf,UAAU,EACV,eAAe,EACf,YAAY,GACb,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"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"}
@@ -4,8 +4,10 @@
4
4
  * Server-side authentication functions for Next.js and TanStack Start.
5
5
  * Inspired by Better Auth and Neon Auth patterns.
6
6
  */
7
+ // GAP-256 admission waitlist (NOT marketing waitlist)
8
+ export { enqueueAdmissionWaitlist, estimateAdmissionWaitlistPosition, generateAdmissionToken, getAdmissionWaitlistByToken, getAdmissionWaitlistByTokenAnyStatus, hashAdmissionToken, markAdmissionWaitlistConverted, maskAdmissionEmail, } from './admission-waitlist.js';
7
9
  // Audit bridge
8
- export { auditAccountLocked, auditLoginFailure, auditLoginSuccess, auditMfaDisabled, auditMfaEnabled, auditPasswordChange, auditPasswordReset, auditSessionRevoked, } from './audit-bridge.js';
10
+ export { auditAccountLocked, auditLoginFailure, auditLoginSuccess, auditMfaDisabled, auditMfaEnabled, auditPasswordChange, auditPasswordReset, auditSessionRevoked, auditSsoConfigChanged, auditSsoLoginFailure, auditSsoLoginSuccess, } from './audit-bridge.js';
9
11
  // NOTE (GAP-338): the audit storage boundary lives at the DEDICATED subpath
10
12
  // `@revealui/auth/audit-storage`, deliberately NOT re-exported from this
11
13
  // barrel. `@revealui/auth/server` is the fleet's most-mocked module (route
@@ -16,13 +18,16 @@ export { isSignupAllowed, signIn, signUp } from './auth.js';
16
18
  export { clearFailedAttempts, configureBruteForce, getFailedAttemptCount, isAccountLocked, recordFailedAttempt, resetBruteForceConfig, } from './brute-force.js';
17
19
  export { AuthError, AuthenticationError, DatabaseError, OAuthAccountConflictError, SessionError, TokenError, } from './errors.js';
18
20
  export { configureMagicLink, createMagicLink, resetMagicLinkConfig, verifyMagicLink, } from './magic-link.js';
21
+ // GAP-256 margin admit (shared admin + server free-intake)
22
+ export { admitFreeIntake, ensureFreeSignupEntitlement, OPEN_FREE_LIMITS, provisionHostedPersonalAccount, } from './margin-admit.js';
19
23
  export { configureMFA, disableMFA, initiateMFASetup, isMFAEnabled, regenerateBackupCodes, resetMFAConfig, verifyBackupCode, verifyMFACode, verifyMFASetup, } from './mfa.js';
20
24
  export { checkSessionMfa, requireMfa, toMfaSession } from './mfa-enforcement.js';
21
25
  export { buildAuthUrl, exchangeCode, fetchProviderUser, generateOAuthState, getLinkedProviders, linkOAuthAccount, unlinkOAuthAccount, upsertOAuthUser, verifyOAuthState, } from './oauth.js';
22
26
  export { configurePasskey, countUserCredentials, deletePasskey, generateAuthenticationChallenge, generateRegistrationChallenge, listPasskeys, renamePasskey, resetPasskeyConfig, storePasskey, verifyAuthentication, verifyRegistration, } from './passkey.js';
23
27
  export { changePassword, generatePasswordResetToken, invalidatePasswordResetToken, resetPasswordWithToken, validatePasswordResetToken, } from './password-reset.js';
24
28
  export { meetsMinimumPasswordRequirements, validatePasswordStrength, } from './password-validation.js';
25
- export { ensureAccountOwnerPlatformAdmin, isPlatformShellAdminRole, PLATFORM_SHELL_ADMIN_COOKIE_ROLES, PLATFORM_SHELL_ADMIN_ROLES, platformRoleForAccountOwner, } from './platform-roles.js';
29
+ export { ensurePlatformOperatorEntitlement, isPlatformOperatorUser, PLATFORM_OPERATOR_LIMITS, rolesFromUserJson, } from './platform-operator.js';
30
+ export { ensureAccountOwnerPlatformAdmin, ensureShellAdminIfAccountOwner, isPlatformShellAdminRole, PLATFORM_SHELL_ADMIN_COOKIE_ROLES, PLATFORM_SHELL_ADMIN_ROLES, platformRoleForAccountOwner, readUsersRole, } from './platform-roles.js';
26
31
  export { checkRateLimit, configureRateLimit, getRateLimitStatus, resetRateLimit, resetRateLimitConfig, } from './rate-limit.js';
27
32
  export { configureSessionBinding, createSession, deleteAllUserSessions, deleteOtherUserSessions, deleteSession, getSession, isRecoverySession, resetSessionBindingConfig, rotateSession, validateSessionBinding, } from './session.js';
28
33
  // Signed Cookie
@@ -0,0 +1,55 @@
1
+ /**
2
+ * GAP-256 free-intake admit + free@t0 entitlement (shared by server + admin).
3
+ *
4
+ * Pure decide lives in @revealui/core/margin-governor. This module is the I/O
5
+ * wrapper: read margin_snapshots, never COUNT(users). Waitlist enqueue is PR-4.
6
+ */
7
+ import { type AdmissionChannel, type CohortLimits, type GovernorFlags, type PayingIntentSignal, type PureAdmitResult } from '@revealui/core/margin-governor';
8
+ export type AdmitFreeIntakeInput = {
9
+ channel: AdmissionChannel;
10
+ email?: string;
11
+ payingIntent?: PayingIntentSignal;
12
+ deploymentMode?: 'hosted' | 'forge' | 'unknown';
13
+ env?: NodeJS.ProcessEnv;
14
+ now?: Date;
15
+ /** Waitlist enqueue source tag (default free_signup). */
16
+ source?: string;
17
+ };
18
+ export type AdmitFreeIntakeResult = PureAdmitResult & {
19
+ flags: GovernorFlags;
20
+ /** Present on waitlist decision after successful enqueue (raw once). */
21
+ waitlistToken?: string;
22
+ positionEstimate?: number | null;
23
+ };
24
+ /** Hosted free open limits — lockstep with apps/server getHostedLimitsForTier('free'). */
25
+ export declare const OPEN_FREE_LIMITS: CohortLimits;
26
+ export declare function admitFreeIntake(input: AdmitFreeIntakeInput): Promise<AdmitFreeIntakeResult>;
27
+ /**
28
+ * Provision personal account + owner membership on hosted (same as signUp).
29
+ * Idempotent-ish: skips if user already has an active owner membership.
30
+ */
31
+ export declare function provisionHostedPersonalAccount(params: {
32
+ userId: string;
33
+ displayName: string;
34
+ }): Promise<{
35
+ accountId: string;
36
+ } | {
37
+ skipped: true;
38
+ reason: string;
39
+ }>;
40
+ /**
41
+ * Free entitlement@t0 (K15) with source=signup.
42
+ * Ensures owner membership/account exist first (passkey/oauth paths).
43
+ */
44
+ export declare function ensureFreeSignupEntitlement(params: {
45
+ userId: string;
46
+ cohortLimits: CohortLimits;
47
+ displayName?: string;
48
+ now?: Date;
49
+ }): Promise<{
50
+ accountId: string;
51
+ } | {
52
+ skipped: true;
53
+ reason: string;
54
+ }>;
55
+ //# sourceMappingURL=margin-admit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"margin-admit.d.ts","sourceRoot":"","sources":["../../src/server/margin-admit.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,YAAY,EAGjB,KAAK,aAAa,EAGlB,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACrB,MAAM,gCAAgC,CAAC;AAaxC,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,gBAAgB,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,kBAAkB,CAAC;IAClC,cAAc,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;IAChD,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,GAAG,CAAC,EAAE,IAAI,CAAC;IACX,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,eAAe,GAAG;IACpD,KAAK,EAAE,aAAa,CAAC;IACrB,wEAAwE;IACxE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC,CAAC;AAEF,0FAA0F;AAC1F,eAAO,MAAM,gBAAgB,EAAE,YAI9B,CAAC;AA2DF,wBAAsB,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA2FjG;AAED;;;GAGG;AACH,wBAAsB,8BAA8B,CAAC,MAAM,EAAE;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CA2CrE;AAED;;;GAGG;AACH,wBAAsB,2BAA2B,CAAC,MAAM,EAAE;IACxD,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,YAAY,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ,GAAG,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAqFrE"}
@@ -0,0 +1,282 @@
1
+ /**
2
+ * GAP-256 free-intake admit + free@t0 entitlement (shared by server + admin).
3
+ *
4
+ * Pure decide lives in @revealui/core/margin-governor. This module is the I/O
5
+ * wrapper: read margin_snapshots, never COUNT(users). Waitlist enqueue is PR-4.
6
+ */
7
+ import { getFeaturesForTier } from '@revealui/core/features';
8
+ import { decideFreeIntake, freeCohortLimitsForMode, governorFlagsFromEnv, } from '@revealui/core/margin-governor';
9
+ import { logger } from '@revealui/core/observability/logger';
10
+ import { getClient } from '@revealui/db';
11
+ import { accountEntitlements, accountMemberships, accounts, marginSnapshots, } from '@revealui/db/schema';
12
+ import { and, desc, eq } from 'drizzle-orm';
13
+ import { enqueueAdmissionWaitlist } from './admission-waitlist.js';
14
+ import { ensureAccountOwnerPlatformAdmin } from './platform-roles.js';
15
+ /** Hosted free open limits — lockstep with apps/server getHostedLimitsForTier('free'). */
16
+ export const OPEN_FREE_LIMITS = {
17
+ maxSites: 1,
18
+ maxUsers: 3,
19
+ maxAgentTasks: 0,
20
+ };
21
+ function detectDeploymentMode(env) {
22
+ const raw = env.REVEALUI_DEPLOYMENT_MODE?.trim().toLowerCase();
23
+ if (raw === 'hosted')
24
+ return 'hosted';
25
+ if (raw === 'forge')
26
+ return 'forge';
27
+ if (env.REVEALUI_LICENSE_PRIVATE_KEY)
28
+ return 'hosted';
29
+ if (raw)
30
+ return 'unknown';
31
+ return 'hosted';
32
+ }
33
+ async function loadLatestSnapshot() {
34
+ try {
35
+ const db = getClient();
36
+ const [row] = await db
37
+ .select({
38
+ id: marginSnapshots.id,
39
+ mode: marginSnapshots.mode,
40
+ computedAt: marginSnapshots.computedAt,
41
+ netCents: marginSnapshots.netCents,
42
+ freeCostRatio: marginSnapshots.freeCostRatio,
43
+ projected7dCents: marginSnapshots.projected7dCents,
44
+ })
45
+ .from(marginSnapshots)
46
+ .orderBy(desc(marginSnapshots.computedAt))
47
+ .limit(1);
48
+ if (!row)
49
+ return null;
50
+ const mode = row.mode;
51
+ if (mode !== 'open' && mode !== 'lean' && mode !== 'waitlist') {
52
+ return null;
53
+ }
54
+ return {
55
+ id: row.id,
56
+ mode,
57
+ computedAt: row.computedAt,
58
+ netCents: row.netCents,
59
+ freeCostRatio: row.freeCostRatio,
60
+ projected7dCents: row.projected7dCents,
61
+ };
62
+ }
63
+ catch (err) {
64
+ logger.warn('[admit-free-intake] snapshot read failed; fail-open', {
65
+ error: err instanceof Error ? err.message : String(err),
66
+ });
67
+ return null;
68
+ }
69
+ }
70
+ /**
71
+ * Pre-check free intake. Call **before** any users insert (K13).
72
+ * Shadow defaults → always admit (never waitlist response).
73
+ * Enforce waitlist → enqueue admission_waitlist and attach raw token.
74
+ */
75
+ function leanMaxAgentTasksFromEnv(env) {
76
+ const raw = env.LEAN_FREE_MAX_AGENT_TASKS;
77
+ if (raw === undefined || raw.trim() === '')
78
+ return 250;
79
+ const n = Number.parseInt(raw, 10);
80
+ return Number.isFinite(n) && n > 0 ? n : 250;
81
+ }
82
+ export async function admitFreeIntake(input) {
83
+ const env = input.env ?? process.env;
84
+ const flags = governorFlagsFromEnv(env);
85
+ const snapshot = await loadLatestSnapshot();
86
+ const result = decideFreeIntake({
87
+ channel: input.channel,
88
+ deploymentMode: input.deploymentMode ?? detectDeploymentMode(env),
89
+ payingIntent: input.payingIntent ?? { kind: 'none' },
90
+ snapshot,
91
+ flags,
92
+ openLimits: OPEN_FREE_LIMITS,
93
+ leanMaxAgentTasks: leanMaxAgentTasksFromEnv(env),
94
+ now: input.now,
95
+ });
96
+ if (result.decision === 'waitlist') {
97
+ const email = input.email?.trim();
98
+ if (!email) {
99
+ // Cannot enqueue without identity — fail-open admit (not margin 403).
100
+ logger.warn('[admit-free-intake] waitlist without email; fail-open admit', {
101
+ channel: input.channel,
102
+ snapshotId: result.snapshotId,
103
+ });
104
+ return {
105
+ decision: 'admit',
106
+ mode: 'open',
107
+ cohortLimits: freeCohortLimitsForMode('open', OPEN_FREE_LIMITS),
108
+ snapshotId: result.snapshotId,
109
+ reason: 'waitlist_missing_email',
110
+ shadow: false,
111
+ flags,
112
+ };
113
+ }
114
+ try {
115
+ const enqueued = await enqueueAdmissionWaitlist({
116
+ email,
117
+ snapshotId: result.snapshotId,
118
+ modeAtEnqueue: 'waitlist',
119
+ source: input.source ?? input.channel,
120
+ now: input.now,
121
+ });
122
+ logger.info('[admit-free-intake]', {
123
+ channel: input.channel,
124
+ email: '[redacted]',
125
+ decision: 'waitlist',
126
+ mode: result.mode,
127
+ reason: result.reason,
128
+ shadow: result.shadow,
129
+ snapshotId: result.snapshotId,
130
+ governorEnabled: flags.enabled,
131
+ waitlistId: enqueued.id,
132
+ positionEstimate: enqueued.positionEstimate,
133
+ });
134
+ return {
135
+ ...result,
136
+ flags,
137
+ waitlistToken: enqueued.waitlistToken,
138
+ positionEstimate: enqueued.positionEstimate,
139
+ };
140
+ }
141
+ catch (err) {
142
+ // Enqueue failure must not 403; fail-open admit so signup can proceed.
143
+ logger.error('[admit-free-intake] waitlist enqueue failed; fail-open admit', err instanceof Error ? err : undefined, { channel: input.channel });
144
+ return {
145
+ decision: 'admit',
146
+ mode: 'open',
147
+ cohortLimits: freeCohortLimitsForMode('open', OPEN_FREE_LIMITS),
148
+ snapshotId: result.snapshotId,
149
+ reason: 'waitlist_enqueue_failed',
150
+ shadow: false,
151
+ flags,
152
+ };
153
+ }
154
+ }
155
+ logger.info('[admit-free-intake]', {
156
+ channel: input.channel,
157
+ email: input.email ? '[redacted]' : undefined,
158
+ decision: result.decision,
159
+ mode: result.mode,
160
+ reason: result.reason,
161
+ shadow: result.shadow,
162
+ snapshotId: result.snapshotId,
163
+ governorEnabled: flags.enabled,
164
+ });
165
+ return { ...result, flags };
166
+ }
167
+ /**
168
+ * Provision personal account + owner membership on hosted (same as signUp).
169
+ * Idempotent-ish: skips if user already has an active owner membership.
170
+ */
171
+ export async function provisionHostedPersonalAccount(params) {
172
+ const { isHostedDeployment } = await import('@revealui/core/deployment-mode');
173
+ if (!isHostedDeployment(process.env)) {
174
+ return { skipped: true, reason: 'not_hosted' };
175
+ }
176
+ const db = getClient();
177
+ const [existing] = await db
178
+ .select({ accountId: accountMemberships.accountId })
179
+ .from(accountMemberships)
180
+ .where(and(eq(accountMemberships.userId, params.userId), eq(accountMemberships.role, 'owner'), eq(accountMemberships.status, 'active')))
181
+ .limit(1);
182
+ if (existing) {
183
+ return { accountId: existing.accountId };
184
+ }
185
+ const accountId = crypto.randomUUID();
186
+ const now = new Date();
187
+ await db.insert(accounts).values({
188
+ id: accountId,
189
+ name: `${params.displayName || 'RevealUI'} Workspace`,
190
+ slug: `acct-${accountId}`,
191
+ status: 'active',
192
+ createdAt: now,
193
+ updatedAt: now,
194
+ });
195
+ await db.insert(accountMemberships).values({
196
+ id: crypto.randomUUID(),
197
+ accountId,
198
+ userId: params.userId,
199
+ role: 'owner',
200
+ status: 'active',
201
+ createdAt: now,
202
+ updatedAt: now,
203
+ });
204
+ await ensureAccountOwnerPlatformAdmin(db, params.userId);
205
+ return { accountId };
206
+ }
207
+ /**
208
+ * Free entitlement@t0 (K15) with source=signup.
209
+ * Ensures owner membership/account exist first (passkey/oauth paths).
210
+ */
211
+ export async function ensureFreeSignupEntitlement(params) {
212
+ const provisioned = await provisionHostedPersonalAccount({
213
+ userId: params.userId,
214
+ displayName: params.displayName ?? 'RevealUI',
215
+ });
216
+ if ('skipped' in provisioned && provisioned.skipped) {
217
+ // still try membership lookup on non-hosted? skip
218
+ if (provisioned.reason === 'not_hosted') {
219
+ return provisioned;
220
+ }
221
+ }
222
+ const db = getClient();
223
+ const now = params.now ?? new Date();
224
+ const [membership] = await db
225
+ .select({ accountId: accountMemberships.accountId })
226
+ .from(accountMemberships)
227
+ .where(and(eq(accountMemberships.userId, params.userId), eq(accountMemberships.role, 'owner'), eq(accountMemberships.status, 'active')))
228
+ .limit(1);
229
+ if (!membership) {
230
+ logger.warn('[free-signup-entitlement] no owner membership; skip', {
231
+ userId: params.userId,
232
+ });
233
+ return { skipped: true, reason: 'no_owner_membership' };
234
+ }
235
+ const accountId = membership.accountId;
236
+ const [existing] = await db
237
+ .select({ source: accountEntitlements.source })
238
+ .from(accountEntitlements)
239
+ .where(eq(accountEntitlements.accountId, accountId))
240
+ .limit(1);
241
+ if (existing?.source === 'stripe' || existing?.source === 'grant') {
242
+ // Do not overwrite paid/gifted rows
243
+ return { accountId };
244
+ }
245
+ const features = getFeaturesForTier('free');
246
+ const row = {
247
+ accountId,
248
+ planId: 'free',
249
+ tier: 'free',
250
+ status: 'active',
251
+ features,
252
+ limits: {
253
+ maxSites: params.cohortLimits.maxSites,
254
+ maxUsers: params.cohortLimits.maxUsers,
255
+ maxAgentTasks: params.cohortLimits.maxAgentTasks,
256
+ },
257
+ meteringStatus: 'active',
258
+ mode: 'live',
259
+ source: 'signup',
260
+ graceUntil: null,
261
+ lastEventAt: null,
262
+ updatedAt: now,
263
+ cogsBreakerTrippedAt: null,
264
+ cogsBreakerReason: null,
265
+ };
266
+ if (existing) {
267
+ // free_preserve: do not clobber existing free limits mid-cycle
268
+ await db
269
+ .update(accountEntitlements)
270
+ .set({ updatedAt: now })
271
+ .where(eq(accountEntitlements.accountId, accountId));
272
+ }
273
+ else {
274
+ await db.insert(accountEntitlements).values(row);
275
+ }
276
+ logger.info('[free-signup-entitlement] upserted', {
277
+ accountId,
278
+ userId: params.userId,
279
+ maxAgentTasks: params.cohortLimits.maxAgentTasks,
280
+ });
281
+ return { accountId };
282
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Platform-operator entitlement.
3
+ *
4
+ * Super-admin (`_json.roles`) is the instance operator (first signup / bootstrap).
5
+ * Hosted account owners are shell admins, not super-admin.
6
+ *
7
+ * Operators receive a real Enterprise `account_entitlements` row (source=grant).
8
+ * Gates stay fail-closed. This is not an identity bypass.
9
+ *
10
+ * Live on `@revealui/auth/platform-operator`, not the `/server` barrel.
11
+ * Route tests bare-mock that barrel for getSession (same reason as audit-storage).
12
+ */
13
+ /** Lockstep with `getHostedLimitsForTier('enterprise')` in apps/server. */
14
+ export declare const PLATFORM_OPERATOR_LIMITS: {
15
+ readonly maxAgentTasks: number;
16
+ };
17
+ export declare function rolesFromUserJson(json: unknown): string[];
18
+ /** Session/engine user fields this check reads. Extra keys (id, email) are fine. */
19
+ export interface PlatformOperatorUserView {
20
+ id?: string;
21
+ _json?: unknown;
22
+ roles?: unknown;
23
+ }
24
+ /**
25
+ * True when this user is the instance operator (engine super-admin).
26
+ * Accepts session `_json.roles` and engine `roles`.
27
+ */
28
+ export declare function isPlatformOperatorUser(user: PlatformOperatorUserView | null | undefined): boolean;
29
+ export interface EnsurePlatformOperatorEntitlementResult {
30
+ accountId: string;
31
+ wrote: boolean;
32
+ }
33
+ export interface EnsurePlatformOperatorSkipped {
34
+ skipped: true;
35
+ reason: string;
36
+ }
37
+ /**
38
+ * Upsert Enterprise (source=grant) for a platform operator's billing account.
39
+ *
40
+ * Skips users who are not super-admin. Idempotent when the row is already
41
+ * a healthy Enterprise grant with `ai`.
42
+ */
43
+ export declare function ensurePlatformOperatorEntitlement(params: {
44
+ userId: string;
45
+ accountId?: string;
46
+ now?: Date;
47
+ }): Promise<EnsurePlatformOperatorEntitlementResult | EnsurePlatformOperatorSkipped>;
48
+ //# sourceMappingURL=platform-operator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform-operator.d.ts","sourceRoot":"","sources":["../../src/server/platform-operator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AASH,2EAA2E;AAC3E,eAAO,MAAM,wBAAwB;;CAE3B,CAAC;AAIX,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,EAAE,CAiBzD;AAED,oFAAoF;AACpF,MAAM,WAAW,wBAAwB;IACvC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,wBAAwB,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAQjG;AAED,MAAM,WAAW,uCAAuC;IACtD,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,6BAA6B;IAC5C,OAAO,EAAE,IAAI,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AA4BD;;;;;GAKG;AACH,wBAAsB,iCAAiC,CAAC,MAAM,EAAE;IAC9D,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ,GAAG,OAAO,CAAC,uCAAuC,GAAG,6BAA6B,CAAC,CA0FnF"}
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Platform-operator entitlement.
3
+ *
4
+ * Super-admin (`_json.roles`) is the instance operator (first signup / bootstrap).
5
+ * Hosted account owners are shell admins, not super-admin.
6
+ *
7
+ * Operators receive a real Enterprise `account_entitlements` row (source=grant).
8
+ * Gates stay fail-closed. This is not an identity bypass.
9
+ *
10
+ * Live on `@revealui/auth/platform-operator`, not the `/server` barrel.
11
+ * Route tests bare-mock that barrel for getSession (same reason as audit-storage).
12
+ */
13
+ import { getConfiguredStripeMode } from '@revealui/config/stripe-mode';
14
+ import { getFeaturesForTier } from '@revealui/core/features';
15
+ import { logger } from '@revealui/core/observability/logger';
16
+ import { getClient } from '@revealui/db';
17
+ import { accountEntitlements, accountMemberships, users } from '@revealui/db/schema';
18
+ import { and, eq } from 'drizzle-orm';
19
+ /** Lockstep with `getHostedLimitsForTier('enterprise')` in apps/server. */
20
+ export const PLATFORM_OPERATOR_LIMITS = {
21
+ maxAgentTasks: Number.MAX_SAFE_INTEGER,
22
+ };
23
+ const SUPER_ADMIN_ROLE = 'super-admin';
24
+ export function rolesFromUserJson(json) {
25
+ let value = json;
26
+ if (typeof value === 'string') {
27
+ try {
28
+ value = JSON.parse(value);
29
+ }
30
+ catch {
31
+ return [];
32
+ }
33
+ }
34
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
35
+ return [];
36
+ }
37
+ const roles = value.roles;
38
+ if (!Array.isArray(roles)) {
39
+ return [];
40
+ }
41
+ return roles.filter((role) => typeof role === 'string');
42
+ }
43
+ /**
44
+ * True when this user is the instance operator (engine super-admin).
45
+ * Accepts session `_json.roles` and engine `roles`.
46
+ */
47
+ export function isPlatformOperatorUser(user) {
48
+ if (!user) {
49
+ return false;
50
+ }
51
+ if (Array.isArray(user.roles) && user.roles.includes(SUPER_ADMIN_ROLE)) {
52
+ return true;
53
+ }
54
+ return rolesFromUserJson(user._json).includes(SUPER_ADMIN_ROLE);
55
+ }
56
+ function featureRecord(features) {
57
+ return Object.fromEntries(Object.entries(features).filter((entry) => typeof entry[1] === 'boolean'));
58
+ }
59
+ function enterpriseRowComplete(row) {
60
+ if (row.tier !== 'enterprise') {
61
+ return false;
62
+ }
63
+ if (row.status !== 'active' && row.status !== 'trialing') {
64
+ return false;
65
+ }
66
+ const features = row.features && typeof row.features === 'object' && !Array.isArray(row.features)
67
+ ? row.features
68
+ : {};
69
+ return features.ai === true;
70
+ }
71
+ /**
72
+ * Upsert Enterprise (source=grant) for a platform operator's billing account.
73
+ *
74
+ * Skips users who are not super-admin. Idempotent when the row is already
75
+ * a healthy Enterprise grant with `ai`.
76
+ */
77
+ export async function ensurePlatformOperatorEntitlement(params) {
78
+ const db = getClient();
79
+ const [user] = await db
80
+ .select({ id: users.id, json: users._json })
81
+ .from(users)
82
+ .where(eq(users.id, params.userId))
83
+ .limit(1);
84
+ if (!(user && isPlatformOperatorUser({ _json: user.json }))) {
85
+ return { skipped: true, reason: 'not_platform_operator' };
86
+ }
87
+ const ownerMemberships = await db
88
+ .select({ accountId: accountMemberships.accountId })
89
+ .from(accountMemberships)
90
+ .where(and(eq(accountMemberships.userId, params.userId), eq(accountMemberships.role, 'owner'), eq(accountMemberships.status, 'active')));
91
+ const ownerIds = ownerMemberships.map((row) => row.accountId);
92
+ if (params.accountId && !ownerIds.includes(params.accountId)) {
93
+ logger.warn('[platform-operator-entitlement] refused non-owner workspace', {
94
+ userId: params.userId,
95
+ accountId: params.accountId,
96
+ });
97
+ return { skipped: true, reason: 'not_owner_workspace' };
98
+ }
99
+ const accountId = params.accountId ?? ownerIds[0];
100
+ if (!accountId) {
101
+ logger.warn('[platform-operator-entitlement] no owner membership; skip', {
102
+ userId: params.userId,
103
+ });
104
+ return { skipped: true, reason: 'no_owner_membership' };
105
+ }
106
+ const mode = getConfiguredStripeMode();
107
+ const [existing] = await db
108
+ .select({
109
+ tier: accountEntitlements.tier,
110
+ status: accountEntitlements.status,
111
+ features: accountEntitlements.features,
112
+ lastEventAt: accountEntitlements.lastEventAt,
113
+ })
114
+ .from(accountEntitlements)
115
+ .where(and(eq(accountEntitlements.accountId, accountId), eq(accountEntitlements.mode, mode)))
116
+ .limit(1);
117
+ if (existing && enterpriseRowComplete(existing)) {
118
+ return { accountId, wrote: false };
119
+ }
120
+ const now = params.now ?? new Date();
121
+ const values = {
122
+ planId: 'enterprise',
123
+ tier: 'enterprise',
124
+ status: 'active',
125
+ features: featureRecord(getFeaturesForTier('enterprise')),
126
+ limits: { ...PLATFORM_OPERATOR_LIMITS },
127
+ meteringStatus: 'active',
128
+ mode,
129
+ source: 'grant',
130
+ graceUntil: null,
131
+ lastEventAt: existing ? (existing.lastEventAt ?? null) : null,
132
+ updatedAt: now,
133
+ cogsBreakerTrippedAt: null,
134
+ cogsBreakerReason: null,
135
+ };
136
+ if (existing) {
137
+ await db
138
+ .update(accountEntitlements)
139
+ .set(values)
140
+ .where(and(eq(accountEntitlements.accountId, accountId), eq(accountEntitlements.mode, mode)));
141
+ }
142
+ else {
143
+ await db.insert(accountEntitlements).values({ accountId, ...values });
144
+ }
145
+ logger.info('[platform-operator-entitlement] granted enterprise', {
146
+ accountId,
147
+ userId: params.userId,
148
+ mode,
149
+ previousTier: existing?.tier ?? null,
150
+ });
151
+ return { accountId, wrote: true };
152
+ }
@@ -39,4 +39,21 @@ export declare function ensureAccountOwnerPlatformAdmin(db: Database, userId: st
39
39
  nextRole: string;
40
40
  updated: boolean;
41
41
  } | null>;
42
+ /**
43
+ * If the user has an active account_memberships.owner seat, ensure users.role
44
+ * is shell-admin (admin/owner). No-op for members/admins who are not owners.
45
+ *
46
+ * GAP-473 residual: accounts provisioned before ensureAccountOwnerPlatformAdmin
47
+ * (or any race that left membership owner + users.role=viewer) get shell access
48
+ * on the next session mint without a manual backfill. Never promotes non-owners.
49
+ *
50
+ * Best-effort call sites: createSession (covers password/MFA/passkey/OAuth mint).
51
+ */
52
+ export declare function ensureShellAdminIfAccountOwner(db: Database, userId: string): Promise<{
53
+ previousRole: string;
54
+ nextRole: string;
55
+ updated: boolean;
56
+ } | null>;
57
+ /** Fresh users.role after session mint / shell repair (for revealui-role cookie). */
58
+ export declare function readUsersRole(db: Database, userId: string): Promise<string | null>;
42
59
  //# sourceMappingURL=platform-roles.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"platform-roles.d.ts","sourceRoot":"","sources":["../../src/server/platform-roles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAIpD,gFAAgF;AAChF,eAAO,MAAM,0BAA0B,EAAE,WAAW,CAAC,MAAM,CAA+B,CAAC;AAE3F;;;GAGG;AACH,eAAO,MAAM,iCAAiC,EAAE,WAAW,CAAC,MAAM,CAIhE,CAAC;AAEH;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAKvE;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAEjF;AAED;;;;;;GAMG;AACH,wBAAsB,+BAA+B,CACnD,EAAE,EAAE,QAAQ,EACZ,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAAC,CAmB9E"}
1
+ {"version":3,"file":"platform-roles.d.ts","sourceRoot":"","sources":["../../src/server/platform-roles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAIpD,gFAAgF;AAChF,eAAO,MAAM,0BAA0B,EAAE,WAAW,CAAC,MAAM,CAA+B,CAAC;AAE3F;;;GAGG;AACH,eAAO,MAAM,iCAAiC,EAAE,WAAW,CAAC,MAAM,CAIhE,CAAC;AAEH;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAKvE;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAEjF;AAED;;;;;;GAMG;AACH,wBAAsB,+BAA+B,CACnD,EAAE,EAAE,QAAQ,EACZ,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAAC,CAmB9E;AAED;;;;;;;;;GASG;AACH,wBAAsB,8BAA8B,CAClD,EAAE,EAAE,QAAQ,EACZ,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAAC,CAkB9E;AAED,qFAAqF;AACrF,wBAAsB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAOxF"}
@@ -12,8 +12,8 @@
12
12
  * via users.role = admin so proxy isAdminRole + /settings (API keys) work.
13
13
  * That is NOT super-admin.
14
14
  */
15
- import { users } from '@revealui/db/schema';
16
- import { eq } from 'drizzle-orm';
15
+ import { accountMemberships, users } from '@revealui/db/schema';
16
+ import { and, eq } from 'drizzle-orm';
17
17
  /** DB column values that already pass isAdminRole / shell admin cookie gate. */
18
18
  export const PLATFORM_SHELL_ADMIN_ROLES = new Set(['owner', 'admin']);
19
19
  /**
@@ -63,3 +63,33 @@ export async function ensureAccountOwnerPlatformAdmin(db, userId) {
63
63
  await db.update(users).set({ role: nextRole }).where(eq(users.id, userId));
64
64
  return { previousRole, nextRole, updated: true };
65
65
  }
66
+ /**
67
+ * If the user has an active account_memberships.owner seat, ensure users.role
68
+ * is shell-admin (admin/owner). No-op for members/admins who are not owners.
69
+ *
70
+ * GAP-473 residual: accounts provisioned before ensureAccountOwnerPlatformAdmin
71
+ * (or any race that left membership owner + users.role=viewer) get shell access
72
+ * on the next session mint without a manual backfill. Never promotes non-owners.
73
+ *
74
+ * Best-effort call sites: createSession (covers password/MFA/passkey/OAuth mint).
75
+ */
76
+ export async function ensureShellAdminIfAccountOwner(db, userId) {
77
+ const [membership] = await db
78
+ .select({ id: accountMemberships.id })
79
+ .from(accountMemberships)
80
+ .where(and(eq(accountMemberships.userId, userId), eq(accountMemberships.role, 'owner'), eq(accountMemberships.status, 'active')))
81
+ .limit(1);
82
+ if (!membership) {
83
+ return null;
84
+ }
85
+ return ensureAccountOwnerPlatformAdmin(db, userId);
86
+ }
87
+ /** Fresh users.role after session mint / shell repair (for revealui-role cookie). */
88
+ export async function readUsersRole(db, userId) {
89
+ const [row] = await db
90
+ .select({ role: users.role })
91
+ .from(users)
92
+ .where(eq(users.id, userId))
93
+ .limit(1);
94
+ return row?.role ?? null;
95
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/server/session.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAQjD,MAAM,WAAW,oBAAoB;IACnC,iEAAiE;IACjE,gBAAgB,EAAE,OAAO,CAAC;IAC1B,iFAAiF;IACjF,SAAS,EAAE,OAAO,CAAC;IACnB,yEAAyE;IACzE,cAAc,EAAE,OAAO,CAAC;CACzB;AAaD,mFAAmF;AACnF,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,OAAO,CAAC,oBAAoB,CAAC,GAAG,IAAI,CAEtF;AAED,qCAAqC;AACrC,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD;AAMD,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CA0B3F;AAMD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,IAAI,CAAC;CACZ;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,GAAG,OAAO,CAI1E;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,OAAO,EAChB,cAAc,CAAC,EAAE,cAAc,GAC9B,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAgI7B;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IACR,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC,GACA,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAmE9C;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IACR,+FAA+F;IAC/F,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC,GACA,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAqC9C;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CA+BtE;AAED;;;;GAIG;AACH;;;GAGG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBzE"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/server/session.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AASjD,MAAM,WAAW,oBAAoB;IACnC,iEAAiE;IACjE,gBAAgB,EAAE,OAAO,CAAC;IAC1B,iFAAiF;IACjF,SAAS,EAAE,OAAO,CAAC;IACnB,yEAAyE;IACzE,cAAc,EAAE,OAAO,CAAC;CACzB;AAaD,mFAAmF;AACnF,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,OAAO,CAAC,oBAAoB,CAAC,GAAG,IAAI,CAEtF;AAED,qCAAqC;AACrC,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD;AAMD,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CA0B3F;AAMD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,IAAI,CAAC;CACZ;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,GAAG,OAAO,CAI1E;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,OAAO,EAChB,cAAc,CAAC,EAAE,cAAc,GAC9B,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAgI7B;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IACR,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC,GACA,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAgF9C;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IACR,+FAA+F;IAC/F,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC,GACA,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAqC9C;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CA+BtE;AAED;;;;GAIG;AACH;;;GAGG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBzE"}
@@ -10,6 +10,7 @@ import { sessions, users } from '@revealui/db/schema';
10
10
  import { and, eq, gt, inArray, isNull, ne } from 'drizzle-orm';
11
11
  import { hashToken } from '../utils/token.js';
12
12
  import { DatabaseError, TokenError } from './errors.js';
13
+ import { ensureShellAdminIfAccountOwner } from './platform-roles.js';
13
14
  const DEFAULT_SESSION_BINDING = {
14
15
  enforceUserAgent: true,
15
16
  enforceIp: false,
@@ -216,6 +217,18 @@ export async function createSession(userId, options) {
216
217
  logger.error('Error getting database client');
217
218
  throw new DatabaseError('Database connection failed', error);
218
219
  }
220
+ // GAP-473: membership owner + stale users.role=viewer must not mint a
221
+ // session that still fails the proxy admin shell gate. Best-effort only —
222
+ // never fail session mint if role repair fails.
223
+ try {
224
+ await ensureShellAdminIfAccountOwner(db, userId);
225
+ }
226
+ catch (roleRepairError) {
227
+ logger.warn('ensureShellAdminIfAccountOwner failed during createSession', {
228
+ userId,
229
+ message: roleRepairError instanceof Error ? roleRepairError.message : String(roleRepairError),
230
+ });
231
+ }
219
232
  // Generate secure session token
220
233
  let token;
221
234
  let tokenHash;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revealui/auth",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Database-backed session auth for Hono and Next.js — bcrypt, OAuth, brute-force protection, rate limiting, password reset. Ships with RevealUI.",
5
5
  "keywords": [
6
6
  "auth",
@@ -17,10 +17,10 @@
17
17
  "jose": "^5.10.0",
18
18
  "zod": "^4.4.3",
19
19
  "@revealui/config": "0.6.0",
20
- "@revealui/contracts": "0.8.2",
21
- "@revealui/core": "0.13.1",
20
+ "@revealui/contracts": "0.8.3",
21
+ "@revealui/core": "0.14.1",
22
22
  "@revealui/db": "0.10.0",
23
- "@revealui/security": "0.7.0"
23
+ "@revealui/security": "0.8.0"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@simplewebauthn/browser": "^13.3.0",
@@ -31,7 +31,7 @@
31
31
  "happy-dom": "^20.10.6",
32
32
  "react": "19.2.8",
33
33
  "selfsigned": "2.4.1",
34
- "typescript": "^6.0.3",
34
+ "typescript": "6.0.3",
35
35
  "vitest": "^4.1.10",
36
36
  "xml-crypto": "6.1.2",
37
37
  "@revealui/dev": "0.1.0"
@@ -50,11 +50,21 @@
50
50
  "import": "./dist/server/index.js",
51
51
  "default": "./dist/server/index.js"
52
52
  },
53
+ "./server/storage": {
54
+ "types": "./dist/server/storage/index.d.ts",
55
+ "import": "./dist/server/storage/index.js",
56
+ "default": "./dist/server/storage/index.js"
57
+ },
53
58
  "./audit-storage": {
54
59
  "types": "./dist/server/audit-storage.d.ts",
55
60
  "import": "./dist/server/audit-storage.js",
56
61
  "default": "./dist/server/audit-storage.js"
57
62
  },
63
+ "./platform-operator": {
64
+ "types": "./dist/server/platform-operator.d.ts",
65
+ "import": "./dist/server/platform-operator.js",
66
+ "default": "./dist/server/platform-operator.js"
67
+ },
58
68
  "./react": {
59
69
  "types": "./dist/react/index.d.ts",
60
70
  "import": "./dist/react/index.js",