@ingram-tech/nk-auth 0.19.4 → 0.19.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.
@@ -1 +1 @@
1
- {"version":3,"file":"chain.d.ts","sourceRoot":"","sources":["../src/chain.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAGrF,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAE/B;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,QAAO,MAOtC,CAAC;AAEH,wEAAwE;AACxE,eAAO,MAAM,qBAAqB,wBAAwB,CAAC;AAS3D;;;;;;GAMG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;IAClD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,YAAY,OAAO,EAAE,SAAS,MAAM,EAAE,EAMrC;CACD;AAED,MAAM,WAAW,qBAAqB;IACrC,sFAAsF;IACtF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,qDAAqD;IACrD,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,sBAAsB,SAC5B,IAAI,YACD,qBAAqB,KAC5B,OAAO,CAAC,eAAe,GAAG,IAAI,CA0BhC,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,aAChB,OAAO,YACR,qBAAqB,KAC5B,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAOtB,CAAC"}
1
+ {"version":3,"file":"chain.d.ts","sourceRoot":"","sources":["../src/chain.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAGrF,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAG/B;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,QAAO,MAOtC,CAAC;AAEH,wEAAwE;AACxE,eAAO,MAAM,qBAAqB,wBAAwB,CAAC;AAS3D;;;;;;GAMG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;IAClD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,YAAY,OAAO,EAAE,SAAS,MAAM,EAAE,EAMrC;CACD;AAED,MAAM,WAAW,qBAAqB;IACrC,sFAAsF;IACtF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,qDAAqD;IACrD,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B;AAsDD;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,sBAAsB,SAC5B,IAAI,YACD,qBAAqB,KAC5B,OAAO,CAAC,eAAe,GAAG,IAAI,CAahC,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,aAChB,OAAO,YACR,qBAAqB,KAC5B,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAOtB,CAAC"}
package/dist/chain.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { inspectMigrations } from "@ingram-tech/nk-db/migrate";
2
2
  import { createRequire } from "node:module";
3
3
  import { dirname } from "node:path";
4
+ import { SHIPPED_AUTH_CHAIN } from "./shipped-chain.js";
4
5
  /**
5
6
  * The migration chain this package ships: `migrations/` in the installed
6
7
  * package. Resolved through the package manifest, at call time, so a bundler
@@ -30,6 +31,43 @@ export class AuthChainNotAppliedError extends Error {
30
31
  this.pending = pending;
31
32
  }
32
33
  }
34
+ const journalOf = (options) => ({
35
+ schema: options.migrationsSchema ?? "drizzle",
36
+ table: options.migrationsTable ?? AUTH_MIGRATIONS_TABLE,
37
+ });
38
+ /**
39
+ * Whether this role can read the journal table. Looked up in pg_catalog, which
40
+ * every role can read: `to_regclass` raises "permission denied" instead of
41
+ * returning null when the role lacks USAGE on the schema, and that broke every
42
+ * session read on a site whose auth role has no business in `drizzle`.
43
+ */
44
+ const journalReadable = async (pool, schema, table) => {
45
+ const { rows } = await pool.query(`select has_schema_privilege(n.oid, 'USAGE')
46
+ and has_table_privilege(c.oid, 'SELECT') as readable
47
+ from pg_catalog.pg_class c
48
+ join pg_catalog.pg_namespace n on n.oid = c.relnamespace
49
+ where n.nspname = $1 and c.relname = $2`, [schema, table]);
50
+ return rows[0]?.readable === true;
51
+ };
52
+ const quoteIdent = (name) => `"${name.replaceAll('"', '""')}"`;
53
+ /**
54
+ * The same verdict as {@link assertAuthChainApplied}, decided against
55
+ * {@link SHIPPED_AUTH_CHAIN} instead of the `migrations/` folder: a shipped
56
+ * file is pending when its hash is not recorded (the set difference nk-db's
57
+ * runner uses). It never touches the filesystem, so it is safe inside a
58
+ * site's server bundle, where it runs on the first session read.
59
+ */
60
+ const assertShippedChainRecorded = async (pool, options) => {
61
+ const { schema, table } = journalOf(options);
62
+ if (!(await journalReadable(pool, schema, table)))
63
+ return;
64
+ const { rows } = await pool.query(`select hash from ${quoteIdent(schema)}.${quoteIdent(table)}`);
65
+ const recorded = new Set(rows.map((row) => row.hash));
66
+ const pending = SHIPPED_AUTH_CHAIN.filter((file) => !recorded.has(file.hash));
67
+ if (pending.length > 0) {
68
+ throw new AuthChainNotAppliedError(pending.map((file) => file.tag));
69
+ }
70
+ };
33
71
  /**
34
72
  * Verify the shipped auth chain is fully applied to the database behind `pool`.
35
73
  *
@@ -47,18 +85,8 @@ export class AuthChainNotAppliedError extends Error {
47
85
  * `instrumentation.ts` or a health check to fail a deploy earlier.
48
86
  */
49
87
  export const assertAuthChainApplied = async (pool, options = {}) => {
50
- const schema = options.migrationsSchema ?? "drizzle";
51
- const table = options.migrationsTable ?? AUTH_MIGRATIONS_TABLE;
52
- // Look the journal up in pg_catalog, which every role can read:
53
- // `to_regclass` raises "permission denied" instead of returning null when the
54
- // role lacks USAGE on the schema, and that broke every session read on a
55
- // site whose auth role has no business in `drizzle`.
56
- const { rows } = await pool.query(`select has_schema_privilege(n.oid, 'USAGE')
57
- and has_table_privilege(c.oid, 'SELECT') as readable
58
- from pg_catalog.pg_class c
59
- join pg_catalog.pg_namespace n on n.oid = c.relnamespace
60
- where n.nspname = $1 and c.relname = $2`, [schema, table]);
61
- if (rows[0]?.readable !== true)
88
+ const { schema, table } = journalOf(options);
89
+ if (!(await journalReadable(pool, schema, table)))
62
90
  return null;
63
91
  const status = await inspectMigrations({
64
92
  pool,
@@ -83,7 +111,7 @@ export const authChainCheck = (database, options = {}) => {
83
111
  return async () => { };
84
112
  let checked;
85
113
  return () => {
86
- checked ??= assertAuthChainApplied(database, options).then(() => undefined);
114
+ checked ??= assertShippedChainRecorded(database, options);
87
115
  return checked;
88
116
  };
89
117
  };
package/dist/chain.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"chain.js","sourceRoot":"","sources":["../src/chain.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAwB,MAAM,4BAA4B,CAAC;AACrF,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAW,EAAE,CAChD,OAAO,CACN,OAAO,CACN,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,CACrC,oDAAoD,CACpD,CACD,CACD,CAAC;AAEH,wEAAwE;AACxE,MAAM,CAAC,MAAM,qBAAqB,GAAG,qBAAqB,CAAC;AAE3D,mEAAmE;AACnE,MAAM,MAAM,GAAG,CAAC,KAAc,EAAiB,EAAE,CAChD,OAAO,KAAK,KAAK,QAAQ;IACzB,KAAK,KAAK,IAAI;IACd,OAAQ,KAA6B,CAAC,KAAK,KAAK,UAAU;IAC1D,OAAQ,KAA+B,CAAC,OAAO,KAAK,UAAU,CAAC;AAEhE;;;;;;GAMG;AACH,MAAM,OAAO,wBAAyB,SAAQ,KAAK;IACzC,OAAO,CAAoB;IACpC,YAAY,OAA0B;QACrC,KAAK,CACJ,yBAAyB,OAAO,CAAC,MAAM,6EAA6E,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,0GAA0G,qBAAqB,kCAAkC,CACvS,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAC;QACvC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACxB,CAAC;CACD;AASD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,KAAK,EAC1C,IAAU,EACV,OAAO,GAA0B,EAAE,EACD,EAAE;IACpC,MAAM,MAAM,GAAG,OAAO,CAAC,gBAAgB,IAAI,SAAS,CAAC;IACrD,MAAM,KAAK,GAAG,OAAO,CAAC,eAAe,IAAI,qBAAqB,CAAC;IAC/D,gEAAgE;IAChE,8EAA8E;IAC9E,yEAAyE;IACzE,qDAAqD;IACrD,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,KAAK,CAChC;;;;0CAIwC,EACxC,CAAC,MAAM,EAAE,KAAK,CAAC,CACf,CAAC;IACF,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,QAAQ,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC5C,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC;QACtC,IAAI;QACJ,gBAAgB,EAAE,oBAAoB,EAAE;QACxC,gBAAgB,EAAE,MAAM;QACxB,eAAe,EAAE,KAAK;KACtB,CAAC,CAAC;IACH,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,wBAAwB,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5E,CAAC;IACD,OAAO,MAAM,CAAC;AACf,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAC7B,QAAiB,EACjB,OAAO,GAA0B,EAAE,EACX,EAAE;IAC1B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC;QAAE,OAAO,KAAK,IAAI,EAAE,GAAE,CAAC,CAAC;IAC7C,IAAI,OAAkC,CAAC;IACvC,OAAO,GAAG,EAAE;QACX,OAAO,KAAK,sBAAsB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC5E,OAAO,OAAO,CAAC;IAChB,CAAC,CAAC;AACH,CAAC,CAAC"}
1
+ {"version":3,"file":"chain.js","sourceRoot":"","sources":["../src/chain.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The migration chain this package ships, as data: each `migrations/*.sql`
3
+ * file's tag and the sha256 nk-db's runner records for it.
4
+ *
5
+ * The per-request chain check runs inside the site's server bundle, where the
6
+ * package's files are not reliably on disk: Next rewrites `require.resolve`
7
+ * to a numeric module id, so resolving `migrations/` there made every session
8
+ * read throw. Carrying the chain in code keeps the check off the filesystem.
9
+ * `chain.test.ts` fails if this list drifts from `migrations/`, so a new
10
+ * migration must be added here in the same change.
11
+ */
12
+ export declare const SHIPPED_AUTH_CHAIN: ReadonlyArray<{
13
+ tag: string;
14
+ hash: string;
15
+ }>;
16
+ //# sourceMappingURL=shipped-chain.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shipped-chain.d.ts","sourceRoot":"","sources":["../src/shipped-chain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB,EAAE,aAAa,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAa3E,CAAC"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The migration chain this package ships, as data: each `migrations/*.sql`
3
+ * file's tag and the sha256 nk-db's runner records for it.
4
+ *
5
+ * The per-request chain check runs inside the site's server bundle, where the
6
+ * package's files are not reliably on disk: Next rewrites `require.resolve`
7
+ * to a numeric module id, so resolving `migrations/` there made every session
8
+ * read throw. Carrying the chain in code keeps the check off the filesystem.
9
+ * `chain.test.ts` fails if this list drifts from `migrations/`, so a new
10
+ * migration must be added here in the same change.
11
+ */
12
+ export const SHIPPED_AUTH_CHAIN = [
13
+ {
14
+ tag: "0001_better_auth",
15
+ hash: "5f44f3294805908dc7a946ac4fb2032d868da5f04e98f496ddfe76e8c0f8cddc",
16
+ },
17
+ {
18
+ tag: "0002_better_auth_1_7",
19
+ hash: "fa64a63e86ea862aff393fef6ecc705fb478d10c7bfc9e390ed475fa990aedea",
20
+ },
21
+ {
22
+ tag: "0003_better_auth_1_7_3",
23
+ hash: "45152bfe9401ace12ac860ccfc4ae49b6a3ab69cf2460947a964c5f231aa4919",
24
+ },
25
+ ];
26
+ //# sourceMappingURL=shipped-chain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shipped-chain.js","sourceRoot":"","sources":["../src/shipped-chain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAiD;IAC/E;QACC,GAAG,EAAE,kBAAkB;QACvB,IAAI,EAAE,kEAAkE;KACxE;IACD;QACC,GAAG,EAAE,sBAAsB;QAC3B,IAAI,EAAE,kEAAkE;KACxE;IACD;QACC,GAAG,EAAE,wBAAwB;QAC7B,IAAI,EAAE,kEAAkE;KACxE;CACD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingram-tech/nk-auth",
3
- "version": "0.19.4",
3
+ "version": "0.19.5",
4
4
  "description": "The Ingram Better Auth foundation: composable presets (org, dual-shape JWT, active-org hooks, pg pool) for Next.js sites.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/chain.ts CHANGED
@@ -2,6 +2,7 @@ import { inspectMigrations, type MigrationStatus } from "@ingram-tech/nk-db/migr
2
2
  import { createRequire } from "node:module";
3
3
  import { dirname } from "node:path";
4
4
  import type { Pool } from "pg";
5
+ import { SHIPPED_AUTH_CHAIN } from "./shipped-chain.js";
5
6
 
6
7
  /**
7
8
  * The migration chain this package ships: `migrations/` in the installed
@@ -53,6 +54,58 @@ export interface AuthChainCheckOptions {
53
54
  migrationsSchema?: string;
54
55
  }
55
56
 
57
+ const journalOf = (options: AuthChainCheckOptions) => ({
58
+ schema: options.migrationsSchema ?? "drizzle",
59
+ table: options.migrationsTable ?? AUTH_MIGRATIONS_TABLE,
60
+ });
61
+
62
+ /**
63
+ * Whether this role can read the journal table. Looked up in pg_catalog, which
64
+ * every role can read: `to_regclass` raises "permission denied" instead of
65
+ * returning null when the role lacks USAGE on the schema, and that broke every
66
+ * session read on a site whose auth role has no business in `drizzle`.
67
+ */
68
+ const journalReadable = async (
69
+ pool: Pool,
70
+ schema: string,
71
+ table: string,
72
+ ): Promise<boolean> => {
73
+ const { rows } = await pool.query<{ readable: boolean }>(
74
+ `select has_schema_privilege(n.oid, 'USAGE')
75
+ and has_table_privilege(c.oid, 'SELECT') as readable
76
+ from pg_catalog.pg_class c
77
+ join pg_catalog.pg_namespace n on n.oid = c.relnamespace
78
+ where n.nspname = $1 and c.relname = $2`,
79
+ [schema, table],
80
+ );
81
+ return rows[0]?.readable === true;
82
+ };
83
+
84
+ const quoteIdent = (name: string): string => `"${name.replaceAll('"', '""')}"`;
85
+
86
+ /**
87
+ * The same verdict as {@link assertAuthChainApplied}, decided against
88
+ * {@link SHIPPED_AUTH_CHAIN} instead of the `migrations/` folder: a shipped
89
+ * file is pending when its hash is not recorded (the set difference nk-db's
90
+ * runner uses). It never touches the filesystem, so it is safe inside a
91
+ * site's server bundle, where it runs on the first session read.
92
+ */
93
+ const assertShippedChainRecorded = async (
94
+ pool: Pool,
95
+ options: AuthChainCheckOptions,
96
+ ): Promise<void> => {
97
+ const { schema, table } = journalOf(options);
98
+ if (!(await journalReadable(pool, schema, table))) return;
99
+ const { rows } = await pool.query<{ hash: string }>(
100
+ `select hash from ${quoteIdent(schema)}.${quoteIdent(table)}`,
101
+ );
102
+ const recorded = new Set(rows.map((row) => row.hash));
103
+ const pending = SHIPPED_AUTH_CHAIN.filter((file) => !recorded.has(file.hash));
104
+ if (pending.length > 0) {
105
+ throw new AuthChainNotAppliedError(pending.map((file) => file.tag));
106
+ }
107
+ };
108
+
56
109
  /**
57
110
  * Verify the shipped auth chain is fully applied to the database behind `pool`.
58
111
  *
@@ -73,21 +126,8 @@ export const assertAuthChainApplied = async (
73
126
  pool: Pool,
74
127
  options: AuthChainCheckOptions = {},
75
128
  ): Promise<MigrationStatus | null> => {
76
- const schema = options.migrationsSchema ?? "drizzle";
77
- const table = options.migrationsTable ?? AUTH_MIGRATIONS_TABLE;
78
- // Look the journal up in pg_catalog, which every role can read:
79
- // `to_regclass` raises "permission denied" instead of returning null when the
80
- // role lacks USAGE on the schema, and that broke every session read on a
81
- // site whose auth role has no business in `drizzle`.
82
- const { rows } = await pool.query<{ readable: boolean }>(
83
- `select has_schema_privilege(n.oid, 'USAGE')
84
- and has_table_privilege(c.oid, 'SELECT') as readable
85
- from pg_catalog.pg_class c
86
- join pg_catalog.pg_namespace n on n.oid = c.relnamespace
87
- where n.nspname = $1 and c.relname = $2`,
88
- [schema, table],
89
- );
90
- if (rows[0]?.readable !== true) return null;
129
+ const { schema, table } = journalOf(options);
130
+ if (!(await journalReadable(pool, schema, table))) return null;
91
131
  const status = await inspectMigrations({
92
132
  pool,
93
133
  migrationsFolder: authMigrationsFolder(),
@@ -114,7 +154,7 @@ export const authChainCheck = (
114
154
  if (!isPool(database)) return async () => {};
115
155
  let checked: Promise<void> | undefined;
116
156
  return () => {
117
- checked ??= assertAuthChainApplied(database, options).then(() => undefined);
157
+ checked ??= assertShippedChainRecorded(database, options);
118
158
  return checked;
119
159
  };
120
160
  };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The migration chain this package ships, as data: each `migrations/*.sql`
3
+ * file's tag and the sha256 nk-db's runner records for it.
4
+ *
5
+ * The per-request chain check runs inside the site's server bundle, where the
6
+ * package's files are not reliably on disk: Next rewrites `require.resolve`
7
+ * to a numeric module id, so resolving `migrations/` there made every session
8
+ * read throw. Carrying the chain in code keeps the check off the filesystem.
9
+ * `chain.test.ts` fails if this list drifts from `migrations/`, so a new
10
+ * migration must be added here in the same change.
11
+ */
12
+ export const SHIPPED_AUTH_CHAIN: ReadonlyArray<{ tag: string; hash: string }> = [
13
+ {
14
+ tag: "0001_better_auth",
15
+ hash: "5f44f3294805908dc7a946ac4fb2032d868da5f04e98f496ddfe76e8c0f8cddc",
16
+ },
17
+ {
18
+ tag: "0002_better_auth_1_7",
19
+ hash: "fa64a63e86ea862aff393fef6ecc705fb478d10c7bfc9e390ed475fa990aedea",
20
+ },
21
+ {
22
+ tag: "0003_better_auth_1_7_3",
23
+ hash: "45152bfe9401ace12ac860ccfc4ae49b6a3ab69cf2460947a964c5f231aa4919",
24
+ },
25
+ ];