@heychunky/core 0.19.0 → 0.20.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.
@@ -0,0 +1,23 @@
1
+ import type { Register } from "./master.ts";
2
+ import type { App } from "./apps.ts";
3
+ export declare const APP_KEY_PREFIX = "hc_app_";
4
+ /** Mint a key for an app, revoking any it had. The value is shown once. */
5
+ export declare function mintAppKey(appId: string, register: Register): Promise<string>;
6
+ /** The app a key belongs to, or nothing — a revoked key is nothing too. */
7
+ export declare function appForKey(key: string | undefined, register: Register): Promise<App | undefined>;
8
+ /**
9
+ * Whether an email may see an app's dev and next: a member of the app's
10
+ * organisation, or on its tester list.
11
+ */
12
+ export declare function viewerAllowed(appId: string, email: string, register: Register): Promise<boolean>;
13
+ export declare function addViewer(appId: string, email: string, addedBy: string, register: Register): Promise<void>;
14
+ export declare function removeViewer(appId: string, email: string, register: Register): Promise<void>;
15
+ export declare function viewersFor(appId: string, register: Register): Promise<{
16
+ email: string;
17
+ added_at: string;
18
+ }[]>;
19
+ /** A token the console hands an allowed viewer to carry to the app. Minutes, once. */
20
+ export declare function mintViewerToken(appId: string, email: string, register: Register): Promise<string>;
21
+ /** Redeem a viewer token for the app the key names: the email, once, in time. */
22
+ export declare function redeemViewerToken(appId: string, token: string, register: Register): Promise<string | undefined>;
23
+ //# sourceMappingURL=appkeys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"appkeys.d.ts","sourceRoot":"","sources":["../../src/admin/appkeys.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAErC,eAAO,MAAM,cAAc,YAAY,CAAC;AAKxC,2EAA2E;AAC3E,wBAAsB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CASnF;AAED,2EAA2E;AAC3E,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,GAAG,GAAG,SAAS,CAAC,CAMrG;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAUtG;AAED,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhH;AAED,wBAAsB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAElG;AAED,wBAAsB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EAAE,CAAC,CAIlH;AAED,sFAAsF;AACtF,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAOvG;AAED,iFAAiF;AACjF,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAMrH"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * An app's own credential, and who may see it before it is live.
3
+ *
4
+ * From phase 5 on the app talks back to Chunky — who may see `dev` and
5
+ * `next`, later comments and feedback — and needs a credential of its own:
6
+ * not a person's key, which names a person and an organisation, but the
7
+ * app's. One rung down from a platform key. Minted at hosting, set in the
8
+ * app's environment, compared only as a hash.
9
+ *
10
+ * Viewing is two lists: the organisation's members, who may always see
11
+ * their own app, and the testers the app has invited by email. The sign-in
12
+ * is heychunky's own; what crosses to the app is a short-lived token the
13
+ * console mints for an allowed viewer and the app redeems once.
14
+ */
15
+ import { createHash, randomBytes } from "node:crypto";
16
+ export const APP_KEY_PREFIX = "hc_app_";
17
+ const VIEWER_MINUTES = 5;
18
+ const hash = (value) => createHash("sha256").update(value).digest("hex");
19
+ /** Mint a key for an app, revoking any it had. The value is shown once. */
20
+ export async function mintAppKey(appId, register) {
21
+ const key = APP_KEY_PREFIX + randomBytes(24).toString("base64url");
22
+ await register.write("update app_keys set revoked_at = now() where app_id = $1 and revoked_at is null", [appId]);
23
+ await register.write("insert into app_keys (app_id, key_hash, prefix) values ($1, $2, $3)", [appId, hash(key), key.slice(0, APP_KEY_PREFIX.length + 4) + "…"]);
24
+ return key;
25
+ }
26
+ /** The app a key belongs to, or nothing — a revoked key is nothing too. */
27
+ export async function appForKey(key, register) {
28
+ if (!key?.startsWith(APP_KEY_PREFIX))
29
+ return undefined;
30
+ const { rows } = await register.query(`select a.* from app_keys k join apps a on a.id = k.app_id
31
+ where k.key_hash = $1 and k.revoked_at is null`, [hash(key)]);
32
+ return rows[0];
33
+ }
34
+ /**
35
+ * Whether an email may see an app's dev and next: a member of the app's
36
+ * organisation, or on its tester list.
37
+ */
38
+ export async function viewerAllowed(appId, email, register) {
39
+ const { rows } = await register.query(`select 1 from apps a
40
+ where a.id = $1 and (
41
+ exists (select 1 from org_members m join users u on u.id = m.user_id
42
+ where m.org_id = a.org_id and lower(u.email) = lower($2))
43
+ or exists (select 1 from app_viewers v where v.app_id = a.id and lower(v.email) = lower($2)))
44
+ limit 1`, [appId, email]);
45
+ return rows.length > 0;
46
+ }
47
+ export async function addViewer(appId, email, addedBy, register) {
48
+ await register.write(`insert into app_viewers (app_id, email, added_by) values ($1, $2, $3)
49
+ on conflict (app_id, lower(email)) do nothing`, [appId, email.trim(), addedBy]);
50
+ }
51
+ export async function removeViewer(appId, email, register) {
52
+ await register.write("delete from app_viewers where app_id = $1 and lower(email) = lower($2)", [appId, email]);
53
+ }
54
+ export async function viewersFor(appId, register) {
55
+ const { rows } = await register.query("select email, added_at from app_viewers where app_id = $1 order by added_at", [appId]);
56
+ return rows;
57
+ }
58
+ /** A token the console hands an allowed viewer to carry to the app. Minutes, once. */
59
+ export async function mintViewerToken(appId, email, register) {
60
+ const token = randomBytes(24).toString("base64url");
61
+ await register.write(`insert into app_viewer_tokens (token_hash, app_id, email, expires_at)
62
+ values ($1, $2, $3, now() + interval '${VIEWER_MINUTES} minutes')`, [hash(token), appId, email]);
63
+ return token;
64
+ }
65
+ /** Redeem a viewer token for the app the key names: the email, once, in time. */
66
+ export async function redeemViewerToken(appId, token, register) {
67
+ const { rows } = await register.write(`update app_viewer_tokens set redeemed_at = now()
68
+ where token_hash = $1 and app_id = $2 and redeemed_at is null and expires_at > now()
69
+ returning email`, [hash(token), appId]);
70
+ return rows[0]?.["email"];
71
+ }
72
+ //# sourceMappingURL=appkeys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"appkeys.js","sourceRoot":"","sources":["../../src/admin/appkeys.ts"],"names":[],"mappings":"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"}
@@ -24,6 +24,7 @@ export * from "./decommission.ts";
24
24
  export * from "./migrations.ts";
25
25
  export * from "./promote.ts";
26
26
  export * from "./release.ts";
27
+ export * from "./appkeys.ts";
27
28
  export * from "./runs.ts";
28
29
  export * from "./entrypoints.ts";
29
30
  export * from "./chunks.ts";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/admin/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,cAAc,wBAAwB,CAAC;AACvC,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAG5B,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/admin/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,cAAc,wBAAwB,CAAC;AACvC,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAG5B,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC"}
@@ -24,6 +24,7 @@ export * from "./decommission.js";
24
24
  export * from "./migrations.js";
25
25
  export * from "./promote.js";
26
26
  export * from "./release.js";
27
+ export * from "./appkeys.js";
27
28
  export * from "./runs.js";
28
29
  export * from "./entrypoints.js";
29
30
  export * from "./chunks.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/admin/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,cAAc,wBAAwB,CAAC;AACvC,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAE5B,+CAA+C;AAC/C,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/admin/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,cAAc,wBAAwB,CAAC;AACvC,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAE5B,+CAA+C;AAC/C,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heychunky/core",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "The engine the Chunky CLI, worker and apps run on.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,15 @@
1
+ -- The credential an app talks back to Chunky with: who may see dev and
2
+ -- next, later comments and feedback. Minted at hosting, set in the app's
3
+ -- environment, and only ever compared as a hash. One rung down from a
4
+ -- platform key; an app has at most one that is not revoked.
5
+ create table app_keys (
6
+ id uuid default gen_random_uuid() not null,
7
+ app_id uuid not null,
8
+ key_hash text not null,
9
+ prefix text not null,
10
+ created_at timestamptz default now() not null,
11
+ revoked_at timestamptz,
12
+ constraint app_keys_pkey primary key (id),
13
+ constraint app_keys_key_hash_key unique (key_hash),
14
+ constraint app_keys_app_id_fkey foreign key (app_id) references apps(id) on delete cascade
15
+ );
@@ -0,0 +1 @@
1
+ create index app_keys_app_idx on app_keys (app_id) where revoked_at is null;
@@ -0,0 +1,12 @@
1
+ -- Who may see an app's dev and next besides its organisation's members:
2
+ -- the testers it has invited, by email. A row and nothing more; the
3
+ -- sign-in is heychunky's own.
4
+ create table app_viewers (
5
+ app_id uuid not null,
6
+ email text not null,
7
+ added_by uuid,
8
+ added_at timestamptz default now() not null,
9
+ constraint app_viewers_pkey primary key (app_id, email),
10
+ constraint app_viewers_app_id_fkey foreign key (app_id) references apps(id) on delete cascade,
11
+ constraint app_viewers_added_by_fkey foreign key (added_by) references users(id) on delete set null
12
+ );
@@ -0,0 +1 @@
1
+ create unique index app_viewers_email_key on app_viewers (app_id, lower(email));
@@ -0,0 +1,12 @@
1
+ -- The hand-off from the console's sign-in to the app: a short-lived token
2
+ -- the console mints for a signed-in, allowed viewer and the app redeems
3
+ -- once with its app key. Nothing here outlives its expiry.
4
+ create table app_viewer_tokens (
5
+ token_hash text not null,
6
+ app_id uuid not null,
7
+ email text not null,
8
+ expires_at timestamptz not null,
9
+ redeemed_at timestamptz,
10
+ constraint app_viewer_tokens_pkey primary key (token_hash),
11
+ constraint app_viewer_tokens_app_id_fkey foreign key (app_id) references apps(id) on delete cascade
12
+ );