@wtfalch/authz-store 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 William Tallis Falch
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # @wtfalch/authz-store
2
+
3
+ `@wtfalch/authz` decides. This stores.
4
+
5
+ Both applications on the engine wrote their own persistence and lifecycle layer
6
+ — the schema, the audit writer, the tenant and team trees, invitations,
7
+ credentials, break-glass, the boot checks — and arrived at the same one. Of the
8
+ 13,107 shared lines, 106 differ, and three quarters of those are one application
9
+ lagging the other or a stale comment. This package is where that code goes so it
10
+ exists once.
11
+
12
+ `/Users/william.falch/Documents/dev/authz/docs/shared-persistence-map.md` is the
13
+ map: what moves here, what stays per-application, and why this is a second
14
+ package rather than a subpath on the engine.
15
+
16
+ ## Status
17
+
18
+ **Unadopted, unpublished, private.** Moved so far: credential secrets, the
19
+ `ScopedDb` seam, every table definition, and `ownerCoverage`, the first module
20
+ that reads rows. Neither application depends on this yet.
21
+
22
+ New here, not moved: `authz_grants`, grants stored per person rather than
23
+ compiled from a role, and `guestGrants`, which reads a tenant's guest grants
24
+ with each one's status (`current`, `lapsed` or `revoked`). A guest row must
25
+ have `expires_at`. Chat reads this to act on expiry; see
26
+ https://github.com/wtfalch/authz/issues/42.
27
+
28
+ Tests run against a real Postgres — PGlite, compiled to wasm and started in the
29
+ test process — so no container, service or network is needed. Most tests build
30
+ the tables from the drizzle definitions. `src/migrate.test.ts` applies the
31
+ shipped SQL instead and fails when the two disagree on a column, a type,
32
+ nullability or an index name.
33
+
34
+ ## Migrations
35
+
36
+ The package ships its own SQL in `migrations/`, and `migrateStore(db)` applies
37
+ it. Run it before the app's own migrations, so every table the package owns
38
+ exists before an app migration references it. Each file runs once, in one
39
+ transaction, and is recorded in `authz_store_migrations`. Never edit a shipped
40
+ file; a change is a new numbered one.
41
+
42
+ `0001_baseline.sql` is every store table as manage's live schema had it on
43
+ 2026-09-22. Three things stay with each app, because each lists something the
44
+ app owns:
45
+
46
+ - `tenants_ceiling_offered_check` and `tenants_self_denied_offered_check`, which
47
+ list that app's offered permissions.
48
+ - The foreign key from `break_glass_sessions.operator_id` to `profiles`.
49
+
50
+ The baseline is for a new database. manage and otf `web/` already have these
51
+ tables, so each needs a one-off reconciliation: bring the schema to the
52
+ baseline, then insert `0001_baseline.sql` into `authz_store_migrations` by
53
+ hand. otf also lacks `authz_roles.updated_by` and the `'activation'`
54
+ assignment source.
55
+
56
+ What a host must supply is growing, and it is all explicit: a module that needs
57
+ the host's `APPLICATION_ID` and `PLATFORM_ID` takes them as a `PolicyBinding`
58
+ argument rather than importing them, because the two applications differ there.
59
+
60
+ The remaining ~12,800 lines, and the one-off migration each application needs to
61
+ adopt them, are not started.
62
+
63
+ ## Why it is not part of `@wtfalch/authz`
64
+
65
+ The engine has no runtime dependencies, and a test scans its whole published
66
+ `dist` for anything Node-only — a `node:` import, `require(`, `process.`,
67
+ `Buffer` — then runs it inside a real workerd process. This package's first file
68
+ imports `node:crypto`, which is precisely what that scan rejects. The separation
69
+ is asserted, not intended: see `the store package exports exactly what it
70
+ promises, and is Node-only by nature` in
71
+ `/Users/william.falch/Documents/dev/authz/scripts/tests/package-contract.test.mjs`.
72
+
73
+ ## A note for a Next.js host
74
+
75
+ Both applications guard this code with Next's `server-only` marker. A
76
+ framework-neutral package cannot import it, so that import is dropped here. A
77
+ host that wants the guard re-exports these functions through its own
78
+ `server-only` module. `generateCredentialSecret` reaching a client bundle is the
79
+ thing worth preventing.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * What a credential's secret is, and the three things every side of the
3
+ * bearer path has to agree on: how one is generated, how it is stored, and
4
+ * how a presented string is turned back into something a query can look up
5
+ * (D7, D20).
6
+ *
7
+ * Kept in its own file rather than beside either half, because the minting
8
+ * side (`credentials.ts`) and the authenticating side (`bearer.ts`) are
9
+ * written against each other and a second copy of these rules in one of them
10
+ * is how the two stop matching. Nothing here touches the database.
11
+ *
12
+ * **The shape: `<PREFIX>_<random>`.** The leading marker is the reason a
13
+ * secret is findable at all once it has leaked: it makes a credential
14
+ * greppable in a log, a repository scan or a paste, which is what lets it be
15
+ * revoked before anyone has to work out whose it was. That is the same
16
+ * reasoning behind every provider's token prefix, and it costs nothing.
17
+ *
18
+ * **This module is server-side only.** Both hosts guard that with Next's
19
+ * `server-only` marker, which a framework-neutral package cannot import. A host
20
+ * that needs the guard re-exports these through its own `server-only` module;
21
+ * `generateCredentialSecret` in a client bundle is the thing to prevent.
22
+ *
23
+ * **Stored hashed, shown once**, exactly as an invitation token is (D10).
24
+ * The row keeps `sha256(secret)` and a short display prefix; the secret
25
+ * itself exists only in the response to the mint that created it. A plain
26
+ * sha256 rather than a password hash is correct here and would not be for a
27
+ * password: this is 32 bytes of `randomBytes`, not something a person chose,
28
+ * so there is no dictionary to run and no work factor worth paying on every
29
+ * request the credential makes.
30
+ */
31
+ /** The marker every secret this app mints begins with. */
32
+ export declare const CREDENTIAL_PREFIX = "wtfk";
33
+ /** Bytes of randomness behind the marker. The same 32 the invitation token uses. */
34
+ export declare const CREDENTIAL_SECRET_BYTES = 32;
35
+ /**
36
+ * How much of the random half is kept in clear for display. Eight hex
37
+ * characters is four bytes: enough to tell a tenant's credentials apart on a
38
+ * page, and 2^-224 of what an attacker would still need to guess.
39
+ */
40
+ export declare const CREDENTIAL_PREFIX_LENGTH = 8;
41
+ /** A minted secret, and the two things the row stores about it. */
42
+ export interface GeneratedCredentialSecret {
43
+ /** Returned to the minter once and never stored or logged in clear. */
44
+ readonly secret: string;
45
+ readonly secretHash: string;
46
+ readonly secretPrefix: string;
47
+ }
48
+ export declare function generateCredentialSecret(): GeneratedCredentialSecret;
49
+ /** The stored form. The whole secret is hashed, marker included, so a lookup can never match on the marker alone. */
50
+ export declare function hashCredentialSecret(secret: string): string;
51
+ /**
52
+ * Whether a presented string could be one of this app's secrets at all,
53
+ * checked before it is hashed or sent anywhere.
54
+ *
55
+ * This is a cheap filter, not a security boundary: the hash lookup is what
56
+ * decides. It exists so that an `Authorization: Bearer <a session cookie>`,
57
+ * or a token belonging to some other system, is refused without a database
58
+ * round trip, and so that nothing unbounded reaches `createHash`.
59
+ */
60
+ export declare function looksLikeCredentialSecret(presented: string): boolean;
61
+ /**
62
+ * The secret carried by an `Authorization` header, or null.
63
+ *
64
+ * Only the `Bearer` scheme, and only one space, because being generous about
65
+ * the framing of a credential is how one ends up matching something that was
66
+ * never meant to be one. A header this does not recognise is not an error to
67
+ * report: it is a request with no credential, which the caller treats the
68
+ * same as a request that sent none.
69
+ */
70
+ export declare function bearerFromHeader(header: string | null | undefined): string | null;
@@ -0,0 +1,89 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ /**
3
+ * What a credential's secret is, and the three things every side of the
4
+ * bearer path has to agree on: how one is generated, how it is stored, and
5
+ * how a presented string is turned back into something a query can look up
6
+ * (D7, D20).
7
+ *
8
+ * Kept in its own file rather than beside either half, because the minting
9
+ * side (`credentials.ts`) and the authenticating side (`bearer.ts`) are
10
+ * written against each other and a second copy of these rules in one of them
11
+ * is how the two stop matching. Nothing here touches the database.
12
+ *
13
+ * **The shape: `<PREFIX>_<random>`.** The leading marker is the reason a
14
+ * secret is findable at all once it has leaked: it makes a credential
15
+ * greppable in a log, a repository scan or a paste, which is what lets it be
16
+ * revoked before anyone has to work out whose it was. That is the same
17
+ * reasoning behind every provider's token prefix, and it costs nothing.
18
+ *
19
+ * **This module is server-side only.** Both hosts guard that with Next's
20
+ * `server-only` marker, which a framework-neutral package cannot import. A host
21
+ * that needs the guard re-exports these through its own `server-only` module;
22
+ * `generateCredentialSecret` in a client bundle is the thing to prevent.
23
+ *
24
+ * **Stored hashed, shown once**, exactly as an invitation token is (D10).
25
+ * The row keeps `sha256(secret)` and a short display prefix; the secret
26
+ * itself exists only in the response to the mint that created it. A plain
27
+ * sha256 rather than a password hash is correct here and would not be for a
28
+ * password: this is 32 bytes of `randomBytes`, not something a person chose,
29
+ * so there is no dictionary to run and no work factor worth paying on every
30
+ * request the credential makes.
31
+ */
32
+ /** The marker every secret this app mints begins with. */
33
+ export const CREDENTIAL_PREFIX = 'wtfk';
34
+ /** Bytes of randomness behind the marker. The same 32 the invitation token uses. */
35
+ export const CREDENTIAL_SECRET_BYTES = 32;
36
+ /**
37
+ * How much of the random half is kept in clear for display. Eight hex
38
+ * characters is four bytes: enough to tell a tenant's credentials apart on a
39
+ * page, and 2^-224 of what an attacker would still need to guess.
40
+ */
41
+ export const CREDENTIAL_PREFIX_LENGTH = 8;
42
+ export function generateCredentialSecret() {
43
+ const random = randomBytes(CREDENTIAL_SECRET_BYTES).toString('hex');
44
+ const secret = `${CREDENTIAL_PREFIX}_${random}`;
45
+ return {
46
+ secret,
47
+ secretHash: hashCredentialSecret(secret),
48
+ secretPrefix: random.slice(0, CREDENTIAL_PREFIX_LENGTH),
49
+ };
50
+ }
51
+ /** The stored form. The whole secret is hashed, marker included, so a lookup can never match on the marker alone. */
52
+ export function hashCredentialSecret(secret) {
53
+ return createHash('sha256').update(secret).digest('hex');
54
+ }
55
+ /**
56
+ * Whether a presented string could be one of this app's secrets at all,
57
+ * checked before it is hashed or sent anywhere.
58
+ *
59
+ * This is a cheap filter, not a security boundary: the hash lookup is what
60
+ * decides. It exists so that an `Authorization: Bearer <a session cookie>`,
61
+ * or a token belonging to some other system, is refused without a database
62
+ * round trip, and so that nothing unbounded reaches `createHash`.
63
+ */
64
+ export function looksLikeCredentialSecret(presented) {
65
+ const expected = `${CREDENTIAL_PREFIX}_`.length + CREDENTIAL_SECRET_BYTES * 2;
66
+ if (presented.length !== expected)
67
+ return false;
68
+ if (!presented.startsWith(`${CREDENTIAL_PREFIX}_`))
69
+ return false;
70
+ return /^[0-9a-f]+$/.test(presented.slice(`${CREDENTIAL_PREFIX}_`.length));
71
+ }
72
+ /**
73
+ * The secret carried by an `Authorization` header, or null.
74
+ *
75
+ * Only the `Bearer` scheme, and only one space, because being generous about
76
+ * the framing of a credential is how one ends up matching something that was
77
+ * never meant to be one. A header this does not recognise is not an error to
78
+ * report: it is a request with no credential, which the caller treats the
79
+ * same as a request that sent none.
80
+ */
81
+ export function bearerFromHeader(header) {
82
+ if (!header)
83
+ return null;
84
+ const match = /^Bearer ([^\s]+)$/.exec(header);
85
+ if (!match?.[1])
86
+ return null;
87
+ const presented = match[1];
88
+ return looksLikeCredentialSecret(presented) ? presented : null;
89
+ }
@@ -0,0 +1,395 @@
1
+ import type { ResourceGrant } from '@wtfalch/authz';
2
+ import type { PolicyBinding } from './owners.js';
3
+ import type { DbOrTx } from './scoped.js';
4
+ /**
5
+ * Grants to one person, stored rather than compiled from a role. Guests live here: an invitation
6
+ * mints rows inside the catalogue's `guest` ceiling, and the engine refuses anything above it.
7
+ */
8
+ export declare const policyGrants: import("drizzle-orm/pg-core").PgTableWithColumns<{
9
+ name: "authz_grants";
10
+ schema: undefined;
11
+ columns: {
12
+ id: import("drizzle-orm/pg-core").PgColumn<{
13
+ name: "id";
14
+ tableName: "authz_grants";
15
+ dataType: "string";
16
+ columnType: "PgUUID";
17
+ data: string;
18
+ driverParam: string;
19
+ notNull: true;
20
+ hasDefault: true;
21
+ isPrimaryKey: true;
22
+ isAutoincrement: false;
23
+ hasRuntimeDefault: false;
24
+ enumValues: undefined;
25
+ baseColumn: never;
26
+ identity: undefined;
27
+ generated: undefined;
28
+ }, {}, {}>;
29
+ tenantId: import("drizzle-orm/pg-core").PgColumn<{
30
+ name: "tenant_id";
31
+ tableName: "authz_grants";
32
+ dataType: "string";
33
+ columnType: "PgUUID";
34
+ data: string;
35
+ driverParam: string;
36
+ notNull: true;
37
+ hasDefault: false;
38
+ isPrimaryKey: false;
39
+ isAutoincrement: false;
40
+ hasRuntimeDefault: false;
41
+ enumValues: undefined;
42
+ baseColumn: never;
43
+ identity: undefined;
44
+ generated: undefined;
45
+ }, {}, {}>;
46
+ applicationId: import("drizzle-orm/pg-core").PgColumn<{
47
+ name: "application_id";
48
+ tableName: "authz_grants";
49
+ dataType: "string";
50
+ columnType: "PgText";
51
+ data: string;
52
+ driverParam: string;
53
+ notNull: true;
54
+ hasDefault: false;
55
+ isPrimaryKey: false;
56
+ isAutoincrement: false;
57
+ hasRuntimeDefault: false;
58
+ enumValues: [string, ...string[]];
59
+ baseColumn: never;
60
+ identity: undefined;
61
+ generated: undefined;
62
+ }, {}, {}>;
63
+ platformId: import("drizzle-orm/pg-core").PgColumn<{
64
+ name: "platform_id";
65
+ tableName: "authz_grants";
66
+ dataType: "string";
67
+ columnType: "PgText";
68
+ data: string;
69
+ driverParam: string;
70
+ notNull: true;
71
+ hasDefault: false;
72
+ isPrimaryKey: false;
73
+ isAutoincrement: false;
74
+ hasRuntimeDefault: false;
75
+ enumValues: [string, ...string[]];
76
+ baseColumn: never;
77
+ identity: undefined;
78
+ generated: undefined;
79
+ }, {}, {}>;
80
+ permission: import("drizzle-orm/pg-core").PgColumn<{
81
+ name: "permission";
82
+ tableName: "authz_grants";
83
+ dataType: "string";
84
+ columnType: "PgText";
85
+ data: string;
86
+ driverParam: string;
87
+ notNull: true;
88
+ hasDefault: false;
89
+ isPrimaryKey: false;
90
+ isAutoincrement: false;
91
+ hasRuntimeDefault: false;
92
+ enumValues: [string, ...string[]];
93
+ baseColumn: never;
94
+ identity: undefined;
95
+ generated: undefined;
96
+ }, {}, {}>;
97
+ principalId: import("drizzle-orm/pg-core").PgColumn<{
98
+ name: "principal_id";
99
+ tableName: "authz_grants";
100
+ dataType: "string";
101
+ columnType: "PgText";
102
+ data: string;
103
+ driverParam: string;
104
+ notNull: true;
105
+ hasDefault: false;
106
+ isPrimaryKey: false;
107
+ isAutoincrement: false;
108
+ hasRuntimeDefault: false;
109
+ enumValues: [string, ...string[]];
110
+ baseColumn: never;
111
+ identity: undefined;
112
+ generated: undefined;
113
+ }, {}, {}>;
114
+ principalClass: import("drizzle-orm/pg-core").PgColumn<{
115
+ name: "principal_class";
116
+ tableName: "authz_grants";
117
+ dataType: "string";
118
+ columnType: "PgText";
119
+ data: "human" | "api_key" | "agent" | "service";
120
+ driverParam: string;
121
+ notNull: true;
122
+ hasDefault: false;
123
+ isPrimaryKey: false;
124
+ isAutoincrement: false;
125
+ hasRuntimeDefault: false;
126
+ enumValues: [string, ...string[]];
127
+ baseColumn: never;
128
+ identity: undefined;
129
+ generated: undefined;
130
+ }, {}, {
131
+ $type: "human" | "api_key" | "agent" | "service";
132
+ }>;
133
+ boundary: import("drizzle-orm/pg-core").PgColumn<{
134
+ name: "boundary";
135
+ tableName: "authz_grants";
136
+ dataType: "json";
137
+ columnType: "PgJsonb";
138
+ data: {
139
+ kind: "organisation";
140
+ organisationId: string;
141
+ } | {
142
+ kind: "organisations";
143
+ organisationIds: string[];
144
+ } | {
145
+ kind: "platform";
146
+ };
147
+ driverParam: unknown;
148
+ notNull: true;
149
+ hasDefault: false;
150
+ isPrimaryKey: false;
151
+ isAutoincrement: false;
152
+ hasRuntimeDefault: false;
153
+ enumValues: undefined;
154
+ baseColumn: never;
155
+ identity: undefined;
156
+ generated: undefined;
157
+ }, {}, {
158
+ $type: {
159
+ kind: "organisation";
160
+ organisationId: string;
161
+ } | {
162
+ kind: "organisations";
163
+ organisationIds: string[];
164
+ } | {
165
+ kind: "platform";
166
+ };
167
+ }>;
168
+ scope: import("drizzle-orm/pg-core").PgColumn<{
169
+ name: "scope";
170
+ tableName: "authz_grants";
171
+ dataType: "json";
172
+ columnType: "PgJsonb";
173
+ data: {
174
+ kind: "organisation";
175
+ } | {
176
+ kind: "team";
177
+ teamId: string;
178
+ descendants: boolean;
179
+ } | {
180
+ kind: "own_teams";
181
+ descendants: boolean;
182
+ } | {
183
+ kind: "resource";
184
+ resourceType: string;
185
+ resourceId: string;
186
+ };
187
+ driverParam: unknown;
188
+ notNull: true;
189
+ hasDefault: false;
190
+ isPrimaryKey: false;
191
+ isAutoincrement: false;
192
+ hasRuntimeDefault: false;
193
+ enumValues: undefined;
194
+ baseColumn: never;
195
+ identity: undefined;
196
+ generated: undefined;
197
+ }, {}, {
198
+ $type: {
199
+ kind: "organisation";
200
+ } | {
201
+ kind: "team";
202
+ teamId: string;
203
+ descendants: boolean;
204
+ } | {
205
+ kind: "own_teams";
206
+ descendants: boolean;
207
+ } | {
208
+ kind: "resource";
209
+ resourceType: string;
210
+ resourceId: string;
211
+ };
212
+ }>;
213
+ constraints: import("drizzle-orm/pg-core").PgColumn<{
214
+ name: "constraints";
215
+ tableName: "authz_grants";
216
+ dataType: "json";
217
+ columnType: "PgJsonb";
218
+ data: ({
219
+ kind: "organisation";
220
+ } | {
221
+ kind: "team";
222
+ teamId: string;
223
+ descendants: boolean;
224
+ } | {
225
+ kind: "own_teams";
226
+ descendants: boolean;
227
+ } | {
228
+ kind: "resource";
229
+ resourceType: string;
230
+ resourceId: string;
231
+ })[];
232
+ driverParam: unknown;
233
+ notNull: false;
234
+ hasDefault: false;
235
+ isPrimaryKey: false;
236
+ isAutoincrement: false;
237
+ hasRuntimeDefault: false;
238
+ enumValues: undefined;
239
+ baseColumn: never;
240
+ identity: undefined;
241
+ generated: undefined;
242
+ }, {}, {
243
+ $type: ({
244
+ kind: "organisation";
245
+ } | {
246
+ kind: "team";
247
+ teamId: string;
248
+ descendants: boolean;
249
+ } | {
250
+ kind: "own_teams";
251
+ descendants: boolean;
252
+ } | {
253
+ kind: "resource";
254
+ resourceType: string;
255
+ resourceId: string;
256
+ })[];
257
+ }>;
258
+ relation: import("drizzle-orm/pg-core").PgColumn<{
259
+ name: "relation";
260
+ tableName: "authz_grants";
261
+ dataType: "string";
262
+ columnType: "PgText";
263
+ data: "any" | "owner" | "actor" | "subject";
264
+ driverParam: string;
265
+ notNull: true;
266
+ hasDefault: true;
267
+ isPrimaryKey: false;
268
+ isAutoincrement: false;
269
+ hasRuntimeDefault: false;
270
+ enumValues: [string, ...string[]];
271
+ baseColumn: never;
272
+ identity: undefined;
273
+ generated: undefined;
274
+ }, {}, {
275
+ $type: "any" | "owner" | "actor" | "subject";
276
+ }>;
277
+ guest: import("drizzle-orm/pg-core").PgColumn<{
278
+ name: "guest";
279
+ tableName: "authz_grants";
280
+ dataType: "boolean";
281
+ columnType: "PgBoolean";
282
+ data: boolean;
283
+ driverParam: boolean;
284
+ notNull: true;
285
+ hasDefault: true;
286
+ isPrimaryKey: false;
287
+ isAutoincrement: false;
288
+ hasRuntimeDefault: false;
289
+ enumValues: undefined;
290
+ baseColumn: never;
291
+ identity: undefined;
292
+ generated: undefined;
293
+ }, {}, {}>;
294
+ reason: import("drizzle-orm/pg-core").PgColumn<{
295
+ name: "reason";
296
+ tableName: "authz_grants";
297
+ dataType: "string";
298
+ columnType: "PgText";
299
+ data: string;
300
+ driverParam: string;
301
+ notNull: false;
302
+ hasDefault: false;
303
+ isPrimaryKey: false;
304
+ isAutoincrement: false;
305
+ hasRuntimeDefault: false;
306
+ enumValues: [string, ...string[]];
307
+ baseColumn: never;
308
+ identity: undefined;
309
+ generated: undefined;
310
+ }, {}, {}>;
311
+ expiresAt: import("drizzle-orm/pg-core").PgColumn<{
312
+ name: "expires_at";
313
+ tableName: "authz_grants";
314
+ dataType: "date";
315
+ columnType: "PgTimestamp";
316
+ data: Date;
317
+ driverParam: string;
318
+ notNull: false;
319
+ hasDefault: false;
320
+ isPrimaryKey: false;
321
+ isAutoincrement: false;
322
+ hasRuntimeDefault: false;
323
+ enumValues: undefined;
324
+ baseColumn: never;
325
+ identity: undefined;
326
+ generated: undefined;
327
+ }, {}, {}>;
328
+ revokedAt: import("drizzle-orm/pg-core").PgColumn<{
329
+ name: "revoked_at";
330
+ tableName: "authz_grants";
331
+ dataType: "date";
332
+ columnType: "PgTimestamp";
333
+ data: Date;
334
+ driverParam: string;
335
+ notNull: false;
336
+ hasDefault: false;
337
+ isPrimaryKey: false;
338
+ isAutoincrement: false;
339
+ hasRuntimeDefault: false;
340
+ enumValues: undefined;
341
+ baseColumn: never;
342
+ identity: undefined;
343
+ generated: undefined;
344
+ }, {}, {}>;
345
+ createdBy: import("drizzle-orm/pg-core").PgColumn<{
346
+ name: "created_by";
347
+ tableName: "authz_grants";
348
+ dataType: "string";
349
+ columnType: "PgText";
350
+ data: string;
351
+ driverParam: string;
352
+ notNull: false;
353
+ hasDefault: false;
354
+ isPrimaryKey: false;
355
+ isAutoincrement: false;
356
+ hasRuntimeDefault: false;
357
+ enumValues: [string, ...string[]];
358
+ baseColumn: never;
359
+ identity: undefined;
360
+ generated: undefined;
361
+ }, {}, {}>;
362
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
363
+ name: "created_at";
364
+ tableName: "authz_grants";
365
+ dataType: "date";
366
+ columnType: "PgTimestamp";
367
+ data: Date;
368
+ driverParam: string;
369
+ notNull: true;
370
+ hasDefault: true;
371
+ isPrimaryKey: false;
372
+ isAutoincrement: false;
373
+ hasRuntimeDefault: false;
374
+ enumValues: undefined;
375
+ baseColumn: never;
376
+ identity: undefined;
377
+ generated: undefined;
378
+ }, {}, {}>;
379
+ };
380
+ dialect: "pg";
381
+ }>;
382
+ /** A revoked grant stays revoked even before its end date; a lapsed one ran out on its own. */
383
+ export type GrantStatus = 'current' | 'lapsed' | 'revoked';
384
+ export interface GuestGrant {
385
+ /** Ready for `resourceAccess`. Times are epoch milliseconds, so pass `now` as `Date.now()`. */
386
+ readonly grant: ResourceGrant;
387
+ readonly reason: string | null;
388
+ readonly status: GrantStatus;
389
+ readonly revokedAt: Date | null;
390
+ }
391
+ /**
392
+ * Every guest grant a tenant has issued in one application, lapsed and revoked ones included, so a
393
+ * caller that acts on expiry sees the lapse rather than a row that silently disappeared.
394
+ */
395
+ export declare function guestGrants(tx: DbOrTx, binding: PolicyBinding, tenantId: string, now?: Date): Promise<GuestGrant[]>;