@kontrolia/db 1.0.0 → 1.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/dist/api-key.d.ts +5 -0
- package/dist/api-key.d.ts.map +1 -0
- package/dist/api-key.js +10 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/register-application.d.ts +12 -4
- package/dist/register-application.d.ts.map +1 -1
- package/dist/register-application.js +10 -8
- package/migrations/0014_fix_device_revoked_actor_fk.sql +28 -0
- package/migrations/0015_application_api_key.sql +11 -0
- package/package.json +1 -1
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Generates a new plaintext application sync API key. Shown once, never stored. */
|
|
2
|
+
export declare function generateApplicationApiKey(): string;
|
|
3
|
+
/** sha256 hex digest — what actually gets stored in kontrolia.applications.api_key_hash. */
|
|
4
|
+
export declare function hashApplicationApiKey(plaintext: string): string;
|
|
5
|
+
//# sourceMappingURL=api-key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-key.d.ts","sourceRoot":"","sources":["../src/api-key.ts"],"names":[],"mappings":"AAIA,oFAAoF;AACpF,wBAAgB,yBAAyB,IAAI,MAAM,CAElD;AAED,4FAA4F;AAC5F,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE/D"}
|
package/dist/api-key.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
const API_KEY_PREFIX = "kapp_";
|
|
3
|
+
/** Generates a new plaintext application sync API key. Shown once, never stored. */
|
|
4
|
+
export function generateApplicationApiKey() {
|
|
5
|
+
return `${API_KEY_PREFIX}${randomBytes(24).toString("base64url")}`;
|
|
6
|
+
}
|
|
7
|
+
/** sha256 hex digest — what actually gets stored in kontrolia.applications.api_key_hash. */
|
|
8
|
+
export function hashApplicationApiKey(plaintext) {
|
|
9
|
+
return createHash("sha256").update(plaintext).digest("hex");
|
|
10
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { migrate, type MigrateOptions } from "./migrate.js";
|
|
2
2
|
export { registerApplication, type RegisterApplicationOptions, type RegisteredApplication, type PermissionInput, } from "./register-application.js";
|
|
3
|
+
export { generateApplicationApiKey, hashApplicationApiKey } from "./api-key.js";
|
|
3
4
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EACL,mBAAmB,EACnB,KAAK,0BAA0B,EAC/B,KAAK,qBAAqB,EAC1B,KAAK,eAAe,GACrB,MAAM,2BAA2B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EACL,mBAAmB,EACnB,KAAK,0BAA0B,EAC/B,KAAK,qBAAqB,EAC1B,KAAK,eAAe,GACrB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,yBAAyB,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -14,15 +14,23 @@ export interface RegisterApplicationOptions {
|
|
|
14
14
|
export interface RegisteredApplication {
|
|
15
15
|
applicationId: string;
|
|
16
16
|
permissionKeys: string[];
|
|
17
|
+
/**
|
|
18
|
+
* The plaintext sync API key (see POST /api/applications/sync on
|
|
19
|
+
* auth-server) — only present the first time this slug is registered.
|
|
20
|
+
* Only the hash is stored; there is no way to recover it later, so the
|
|
21
|
+
* caller must surface it to the operator immediately. `null` means the
|
|
22
|
+
* application already existed and its key (if any) was left untouched.
|
|
23
|
+
*/
|
|
24
|
+
apiKey: string | null;
|
|
17
25
|
}
|
|
18
26
|
/**
|
|
19
27
|
* Inserts an application and its permission catalog directly against
|
|
20
28
|
* Postgres. kontrolia.applications/permissions have no insert policy for
|
|
21
29
|
* regular users (see migrations/0010_rls_policies.sql — writes are meant to
|
|
22
|
-
* go through a platform-admin path)
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* and
|
|
30
|
+
* go through a platform-admin path), so this is that path: a direct,
|
|
31
|
+
* service-role-equivalent write, the same way migrate() bypasses RLS to
|
|
32
|
+
* apply schema changes. Safe to re-run — the slug and permission key are
|
|
33
|
+
* both upserted, and re-running never rotates an existing api_key_hash.
|
|
26
34
|
*/
|
|
27
35
|
export declare function registerApplication(options: RegisterApplicationOptions): Promise<RegisteredApplication>;
|
|
28
36
|
//# sourceMappingURL=register-application.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register-application.d.ts","sourceRoot":"","sources":["../src/register-application.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"register-application.d.ts","sourceRoot":"","sources":["../src/register-application.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,0BAA0B;IACzC,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,aAAa,GAAG,SAAS,GAAG,YAAY,CAAC;IACtD,WAAW,EAAE,eAAe,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB;;;;;;OAMG;IACH,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAkC7G"}
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import { Client } from "pg";
|
|
2
|
+
import { generateApplicationApiKey, hashApplicationApiKey } from "./api-key.js";
|
|
2
3
|
/**
|
|
3
4
|
* Inserts an application and its permission catalog directly against
|
|
4
5
|
* Postgres. kontrolia.applications/permissions have no insert policy for
|
|
5
6
|
* regular users (see migrations/0010_rls_policies.sql — writes are meant to
|
|
6
|
-
* go through a platform-admin path)
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* and
|
|
7
|
+
* go through a platform-admin path), so this is that path: a direct,
|
|
8
|
+
* service-role-equivalent write, the same way migrate() bypasses RLS to
|
|
9
|
+
* apply schema changes. Safe to re-run — the slug and permission key are
|
|
10
|
+
* both upserted, and re-running never rotates an existing api_key_hash.
|
|
10
11
|
*/
|
|
11
12
|
export async function registerApplication(options) {
|
|
12
13
|
const client = new Client({ connectionString: options.connectionString });
|
|
13
14
|
await client.connect();
|
|
14
15
|
try {
|
|
15
|
-
const
|
|
16
|
-
|
|
16
|
+
const candidateApiKey = generateApplicationApiKey();
|
|
17
|
+
const { rows: [application], } = await client.query(`insert into kontrolia.applications (name, slug, environment, api_key_hash)
|
|
18
|
+
values ($1, $2, $3, $4)
|
|
17
19
|
on conflict (slug) do update set name = excluded.name, environment = excluded.environment
|
|
18
|
-
returning id`, [options.name, options.slug, options.environment]);
|
|
20
|
+
returning id, (xmax = 0) as inserted`, [options.name, options.slug, options.environment, hashApplicationApiKey(candidateApiKey)]);
|
|
19
21
|
if (!application)
|
|
20
22
|
throw new Error(`Failed to upsert application "${options.slug}"`);
|
|
21
23
|
const permissionKeys = [];
|
|
@@ -26,7 +28,7 @@ export async function registerApplication(options) {
|
|
|
26
28
|
on conflict (key) do update set description = excluded.description`, [application.id, permission.resource, permission.action, key, permission.description ?? null]);
|
|
27
29
|
permissionKeys.push(key);
|
|
28
30
|
}
|
|
29
|
-
return { applicationId: application.id, permissionKeys };
|
|
31
|
+
return { applicationId: application.id, permissionKeys, apiKey: application.inserted ? candidateApiKey : null };
|
|
30
32
|
}
|
|
31
33
|
finally {
|
|
32
34
|
await client.end();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-- log_device_revoked() (0013_audit_log_triggers.sql) falls back to the
|
|
2
|
+
-- device's own user_id as the actor when there's no session context, e.g.
|
|
3
|
+
-- an admin calling revoke_session() on someone else's device. That fallback
|
|
4
|
+
-- breaks when the DELETE on kontrolia.devices is itself the product of
|
|
5
|
+
-- `delete from auth.users ... on delete cascade`: by the time this AFTER
|
|
6
|
+
-- DELETE trigger's INSERT runs, the user's row is already gone from
|
|
7
|
+
-- auth.users (deleted earlier in the same transaction, and command-counter
|
|
8
|
+
-- visibility makes that immediate), so falling back to old.user_id trips
|
|
9
|
+
-- audit_logs_actor_user_id_fkey and the whole `delete from auth.users`
|
|
10
|
+
-- fails. Only use the fallback if that user still exists; otherwise log the
|
|
11
|
+
-- device revocation with a null actor, same as any other audit row for an
|
|
12
|
+
-- actor we can't identify.
|
|
13
|
+
create or replace function kontrolia.log_device_revoked()
|
|
14
|
+
returns trigger
|
|
15
|
+
language plpgsql
|
|
16
|
+
security definer
|
|
17
|
+
set search_path = ''
|
|
18
|
+
as $$
|
|
19
|
+
declare
|
|
20
|
+
fallback_actor uuid;
|
|
21
|
+
begin
|
|
22
|
+
select id into fallback_actor from auth.users where id = old.user_id;
|
|
23
|
+
|
|
24
|
+
insert into kontrolia.audit_logs (organization_id, actor_user_id, action, target_type, target_id, metadata)
|
|
25
|
+
values (null, coalesce(auth.uid(), fallback_actor), 'device.revoked', 'device', old.session_id::text, jsonb_build_object('label', old.label));
|
|
26
|
+
return old;
|
|
27
|
+
end;
|
|
28
|
+
$$;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
-- Lets an already-registered application authenticate its own catalog-sync
|
|
2
|
+
-- calls (POST /api/applications/sync on auth-server) without needing direct
|
|
3
|
+
-- database access — the same "declare your own permissions" pattern Auth0's
|
|
4
|
+
-- Resource Servers / Stripe's API keys use. Only a hash is ever stored; the
|
|
5
|
+
-- plaintext key is generated once, shown to the operator, and never
|
|
6
|
+
-- persisted anywhere.
|
|
7
|
+
|
|
8
|
+
alter table kontrolia.applications add column api_key_hash text;
|
|
9
|
+
|
|
10
|
+
comment on column kontrolia.applications.api_key_hash is
|
|
11
|
+
'sha256 hex digest of the application''s sync API key. Null for applications registered before this column existed, or that opted out — those can only be updated by re-running the CLI/registerApplication() directly against the database.';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontrolia/db",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "KontrolIA Auth database layer: SQL migrations for the `kontrolia` schema (organizations, RBAC, Custom Access Token Hook) and a connection-string-agnostic migration runner.",
|
|
6
6
|
"keywords": [
|