@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 +21 -0
- package/README.md +79 -0
- package/dist/credential-secret.d.ts +70 -0
- package/dist/credential-secret.js +89 -0
- package/dist/grants.d.ts +395 -0
- package/dist/grants.js +68 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/migrate.d.ts +9 -0
- package/dist/migrate.js +41 -0
- package/dist/owners.d.ts +23 -0
- package/dist/owners.js +40 -0
- package/dist/policy-schema.d.ts +812 -0
- package/dist/policy-schema.js +57 -0
- package/dist/resource-schema.d.ts +188 -0
- package/dist/resource-schema.js +22 -0
- package/dist/schema.d.ts +1821 -0
- package/dist/schema.js +193 -0
- package/dist/scoped.d.ts +140 -0
- package/dist/scoped.js +176 -0
- package/migrations/0001_baseline.sql +571 -0
- package/migrations/0002_grants.sql +27 -0
- package/package.json +44 -0
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
|
+
}
|
package/dist/grants.d.ts
ADDED
|
@@ -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[]>;
|