@hatk/hatk 0.0.1-alpha.84 → 0.0.1-alpha.86

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.
Files changed (94) hide show
  1. package/dist/adapter.d.ts.map +1 -1
  2. package/dist/adapter.js +2 -0
  3. package/dist/cli.js +5 -1
  4. package/dist/config.d.ts +47 -1
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +18 -1
  7. package/dist/database/db.d.ts +11 -0
  8. package/dist/database/db.d.ts.map +1 -1
  9. package/dist/database/db.js +356 -160
  10. package/dist/database/dialect.js +2 -2
  11. package/dist/database/fts.d.ts.map +1 -1
  12. package/dist/database/fts.js +6 -2
  13. package/dist/database/index.d.ts +1 -1
  14. package/dist/database/index.d.ts.map +1 -1
  15. package/dist/database/index.js +1 -1
  16. package/dist/database/schema.d.ts +2 -0
  17. package/dist/database/schema.d.ts.map +1 -1
  18. package/dist/database/schema.js +46 -4
  19. package/dist/dev-entry.d.ts.map +1 -1
  20. package/dist/dev-entry.js +2 -2
  21. package/dist/feeds.d.ts +14 -0
  22. package/dist/feeds.d.ts.map +1 -1
  23. package/dist/feeds.js +24 -13
  24. package/dist/hooks.d.ts +17 -0
  25. package/dist/hooks.d.ts.map +1 -1
  26. package/dist/hooks.js +26 -3
  27. package/dist/hydrate.d.ts +42 -0
  28. package/dist/hydrate.d.ts.map +1 -1
  29. package/dist/hydrate.js +18 -5
  30. package/dist/indexer.d.ts +4 -3
  31. package/dist/indexer.d.ts.map +1 -1
  32. package/dist/indexer.js +81 -64
  33. package/dist/lex-types.d.ts +2 -0
  34. package/dist/lex-types.d.ts.map +1 -1
  35. package/dist/main.js +26 -2
  36. package/dist/oauth/db.d.ts +9 -0
  37. package/dist/oauth/db.d.ts.map +1 -1
  38. package/dist/oauth/db.js +12 -0
  39. package/dist/opengraph.d.ts.map +1 -1
  40. package/dist/opengraph.js +16 -4
  41. package/dist/private-collections.d.ts +7 -2
  42. package/dist/private-collections.d.ts.map +1 -1
  43. package/dist/private-collections.js +9 -4
  44. package/dist/server.d.ts +8 -0
  45. package/dist/server.d.ts.map +1 -1
  46. package/dist/server.js +167 -15
  47. package/dist/spaces/blob.d.ts +51 -0
  48. package/dist/spaces/blob.d.ts.map +1 -0
  49. package/dist/spaces/blob.js +111 -0
  50. package/dist/spaces/credential.d.ts +62 -0
  51. package/dist/spaces/credential.d.ts.map +1 -0
  52. package/dist/spaces/credential.js +184 -0
  53. package/dist/spaces/engine.d.ts +97 -0
  54. package/dist/spaces/engine.d.ts.map +1 -0
  55. package/dist/spaces/engine.js +552 -0
  56. package/dist/spaces/guard.d.ts +41 -0
  57. package/dist/spaces/guard.d.ts.map +1 -0
  58. package/dist/spaces/guard.js +79 -0
  59. package/dist/spaces/identity.d.ts +37 -0
  60. package/dist/spaces/identity.d.ts.map +1 -0
  61. package/dist/spaces/identity.js +88 -0
  62. package/dist/spaces/index.d.ts +55 -0
  63. package/dist/spaces/index.d.ts.map +1 -0
  64. package/dist/spaces/index.js +133 -0
  65. package/dist/spaces/notify.d.ts +51 -0
  66. package/dist/spaces/notify.d.ts.map +1 -0
  67. package/dist/spaces/notify.js +121 -0
  68. package/dist/spaces/records.d.ts +37 -0
  69. package/dist/spaces/records.d.ts.map +1 -0
  70. package/dist/spaces/records.js +63 -0
  71. package/dist/spaces/store.d.ts +51 -0
  72. package/dist/spaces/store.d.ts.map +1 -0
  73. package/dist/spaces/store.js +99 -0
  74. package/dist/spaces/uri.d.ts +60 -0
  75. package/dist/spaces/uri.d.ts.map +1 -0
  76. package/dist/spaces/uri.js +97 -0
  77. package/dist/spaces/verify.d.ts +39 -0
  78. package/dist/spaces/verify.d.ts.map +1 -0
  79. package/dist/spaces/verify.js +87 -0
  80. package/dist/spaces/viewer.d.ts +51 -0
  81. package/dist/spaces/viewer.d.ts.map +1 -0
  82. package/dist/spaces/viewer.js +167 -0
  83. package/dist/spaces/visibility.d.ts +57 -0
  84. package/dist/spaces/visibility.d.ts.map +1 -0
  85. package/dist/spaces/visibility.js +81 -0
  86. package/dist/templates/feed.tpl +4 -0
  87. package/dist/templates/xrpc.tpl +13 -1
  88. package/dist/test.d.ts +2 -0
  89. package/dist/test.d.ts.map +1 -1
  90. package/dist/test.js +8 -5
  91. package/dist/xrpc.d.ts +19 -3
  92. package/dist/xrpc.d.ts.map +1 -1
  93. package/dist/xrpc.js +62 -32
  94. package/package.json +8 -4
@@ -0,0 +1 @@
1
+ {"version":3,"file":"visibility.d.ts","sourceRoot":"","sources":["../../src/spaces/visibility.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAaH;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAE9E;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,IAAI,CAElE;AAED,+DAA+D;AAC/D,wBAAgB,cAAc,IAAI,WAAW,CAAC,MAAM,CAAC,CAEpD;AAED,6DAA6D;AAC7D,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAGzE;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAUlH"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Which permissioned spaces the read in flight is allowed to serve.
3
+ *
4
+ * Space records are not public data. A space's authority decides who may read
5
+ * it, and it decides that one caller at a time — so a row indexed out of a
6
+ * space carries an audience, and serving it to a request that has not proven
7
+ * it belongs to that audience discloses private data no matter how the query
8
+ * was written.
9
+ *
10
+ * hatk has no other per-row visibility concept. The takedown join is
11
+ * viewer-independent and `privateCollections` is all-or-nothing, so neither
12
+ * could carry this. The rule here is instead:
13
+ *
14
+ * a row with `space IS NULL` is public repo data and always readable;
15
+ * a row with a space is readable only inside a scope that names that space.
16
+ *
17
+ * The default is an empty scope, which serves no space rows at all. That is
18
+ * deliberate: a read path nobody remembered to scope fails closed and shows a
19
+ * member nothing, rather than failing open and showing a stranger everything.
20
+ * Every query over a record table applies `spaceFilterSql`; the scope is
21
+ * established per request by whoever established the viewer.
22
+ */
23
+ import { AsyncLocalStorage } from 'node:async_hooks';
24
+ const EMPTY = new Set();
25
+ /**
26
+ * Async-local rather than a module global: a server answers requests
27
+ * concurrently, and one viewer's readable set must never be observable from
28
+ * another's request while an await is outstanding.
29
+ */
30
+ const store = new AsyncLocalStorage();
31
+ /**
32
+ * Run `fn` with exactly these spaces readable.
33
+ *
34
+ * Replaces rather than extends any enclosing scope: widening what a nested read
35
+ * may see is never what a caller means, and a scope that only ever narrows is
36
+ * one that can be reasoned about locally.
37
+ */
38
+ export function withReadableSpaces(spaces, fn) {
39
+ return store.run(new Set(spaces), fn);
40
+ }
41
+ /**
42
+ * Set the readable spaces for the rest of the current async context.
43
+ *
44
+ * `withReadableSpaces` wants a callback, and a request handler is a long body
45
+ * rather than something to wrap — restructuring it around a closure to hold a
46
+ * scope would be the tail wagging the dog. A server handler already runs in its
47
+ * own async context per request, which is the case `enterWith` exists for.
48
+ */
49
+ export function enterReadableSpaces(spaces) {
50
+ store.enterWith(new Set(spaces));
51
+ }
52
+ /** The spaces this read may serve. Empty outside any scope. */
53
+ export function readableSpaces() {
54
+ return store.getStore() ?? EMPTY;
55
+ }
56
+ /** Whether a space would be served to the read in flight. */
57
+ export function isSpaceReadable(space) {
58
+ if (space == null)
59
+ return true;
60
+ return readableSpaces().has(space);
61
+ }
62
+ /**
63
+ * The space gate as a SQL predicate, for a table aliased `alias` (pass '' for
64
+ * an unaliased table).
65
+ *
66
+ * `startIdx` is the next free `$N` placeholder; the returned `nextIdx` is the
67
+ * next one after this predicate's own. With nothing readable the predicate
68
+ * binds no parameters at all, which is the shape almost every request takes.
69
+ */
70
+ export function spaceFilterSql(alias, startIdx) {
71
+ const col = alias ? `${alias}.space` : 'space';
72
+ const spaces = [...readableSpaces()];
73
+ if (spaces.length === 0)
74
+ return { sql: `${col} IS NULL`, params: [], nextIdx: startIdx };
75
+ const placeholders = spaces.map((_, i) => `$${startIdx + i}`).join(', ');
76
+ return {
77
+ sql: `(${col} IS NULL OR ${col} IN (${placeholders}))`,
78
+ params: spaces,
79
+ nextIdx: startIdx + spaces.length,
80
+ };
81
+ }
@@ -5,6 +5,10 @@ export default defineFeed({
5
5
  label: '{{Name}}',
6
6
 
7
7
  async generate(ctx) {
8
+ // ctx.paginate applies the permissioned-space gate for you, so this page
9
+ // shows a member the spaces they may read and everyone else the public
10
+ // rows alone. A feed that builds its own SQL over a space-backed table is
11
+ // refused with an error naming the table; see ctx.spaceFilter.
8
12
  const { rows, cursor } = await ctx.paginate<{ uri: string }>(
9
13
  `SELECT uri, cid, indexed_at FROM "your.collection.here"`,
10
14
  )
@@ -1,7 +1,7 @@
1
1
  import { defineQuery } from '$hatk'
2
2
 
3
3
  export default defineQuery('{{name}}', async (ctx) => {
4
- const { ok, db, params, packCursor, unpackCursor } = ctx
4
+ const { ok, db, params, packCursor, unpackCursor, spaceFilter } = ctx
5
5
  const limit = params.limit ?? 30
6
6
  const cursor = params.cursor
7
7
 
@@ -9,6 +9,18 @@ export default defineQuery('{{name}}', async (ctx) => {
9
9
  const sqlParams: (string | number)[] = []
10
10
  let paramIdx = 1
11
11
 
12
+ // Which permissioned spaces this viewer may be shown. Typed helpers like
13
+ // ctx.lookup and ctx.getRecords apply this already; hand-written SQL cannot,
14
+ // because nothing can inject a predicate into a string you wrote. Drop it on
15
+ // a collection a space writes into and hatk refuses the query by name.
16
+ //
17
+ // Outside a viewer's scope it is `s.space IS NULL` and binds nothing, so on an
18
+ // instance that indexes no space it costs one test on an indexed column.
19
+ const spaces = spaceFilter('s', paramIdx)
20
+ conditions.push(spaces.sql)
21
+ sqlParams.push(...spaces.params)
22
+ paramIdx = spaces.nextIdx
23
+
12
24
  if (cursor) {
13
25
  const parsed = unpackCursor(cursor)
14
26
  if (parsed) {
package/dist/test.d.ts CHANGED
@@ -5,6 +5,8 @@ export interface TestContext {
5
5
  db: {
6
6
  query: (sql: string, params?: any[]) => Promise<any[]>;
7
7
  run: (sql: string, params?: any[]) => Promise<void>;
8
+ /** Raw SQL with the permissioned-space guard off, as the runtime contexts have it. */
9
+ unfiltered: (sql: string, params?: any[]) => Promise<any[]>;
8
10
  };
9
11
  loadFixtures: (dir?: string) => Promise<void>;
10
12
  loadFeed: (name: string) => {
@@ -1 +1 @@
1
- {"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../src/test.ts"],"names":[],"mappings":"AAIA,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAkBzD,OAAO,EAAE,IAAI,IAAI,iBAAiB,EAAE,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAA;AAEpE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE;QACF,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;QACtD,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;KACpD,CAAA;IACD,YAAY,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7C,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;QAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,CAAA;KAAE,CAAA;IAC5E,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;QAAE,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,CAAA;KAAE,CAAA;IACnE,WAAW,EAAE,CAAC,IAAI,CAAC,EAAE;QACnB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,MAAM,CAAC,EAAE;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAA;QAC/B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAChC,KAAK,WAAW,CAAA;IACjB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1B,gBAAgB,CAAC,OAAO,EAAE,UAAU,CAAA;IACpC,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,CAAA;CACxC;AAED,MAAM,WAAW,UAAW,SAAQ,WAAW;IAC7C,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC9D,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC7E,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,QAAQ,KAAK,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAA;IAC/D,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAClE;AAYD;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,WAAW,CAAC,CAwL9D;AA8BD,wBAAsB,eAAe,IAAI,OAAO,CAAC,UAAU,CAAC,CAiD3D"}
1
+ {"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../src/test.ts"],"names":[],"mappings":"AAIA,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAmBzD,OAAO,EAAE,IAAI,IAAI,iBAAiB,EAAE,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAA;AAIpE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE;QACF,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;QACtD,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;QACnD,sFAAsF;QACtF,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;KAC5D,CAAA;IACD,YAAY,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7C,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;QAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,CAAA;KAAE,CAAA;IAC5E,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;QAAE,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,CAAA;KAAE,CAAA;IACnE,WAAW,EAAE,CAAC,IAAI,CAAC,EAAE;QACnB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,MAAM,CAAC,EAAE;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAA;QAC/B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAChC,KAAK,WAAW,CAAA;IACjB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1B,gBAAgB,CAAC,OAAO,EAAE,UAAU,CAAA;IACpC,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,CAAA;CACxC;AAED,MAAM,WAAW,UAAW,SAAQ,WAAW;IAC7C,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC9D,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC7E,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,QAAQ,KAAK,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAA;IAC/D,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAClE;AAYD;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,WAAW,CAAC,CAyL9D;AA8BD,wBAAsB,eAAe,IAAI,OAAO,CAAC,UAAU,CAAC,CAiD3D"}
package/dist/test.js CHANGED
@@ -3,7 +3,7 @@ import { readdirSync, readFileSync } from 'node:fs';
3
3
  import { registerHatkResolveHook } from "./resolve-hatk.js";
4
4
  import YAML from 'yaml';
5
5
  import { loadConfig } from "./config.js";
6
- import { loadLexicons, storeLexicons, discoverCollections, generateTableSchema, generateCreateTableSQL, } from "./database/schema.js";
6
+ import { validatableLexicons, loadLexicons, storeLexicons, discoverCollections, generateTableSchema, generateCreateTableSQL, } from "./database/schema.js";
7
7
  import { initDatabase, querySQL, runSQL, insertRecord, closeDatabase } from "./database/db.js";
8
8
  import { createAdapter } from "./database/adapter-factory.js";
9
9
  import { SQLITE_DIALECT } from "./database/dialect.js";
@@ -16,6 +16,8 @@ import { validateLexicons } from '@bigmoves/lexicon';
16
16
  import { packCursor, unpackCursor, isTakendownDid, filterTakendownDids } from "./database/db.js";
17
17
  import { seed as createSeedHelpers } from "./seed.js";
18
18
  import { setPrivateCollections } from "./private-collections.js";
19
+ import { spaceFilterSql } from "./spaces/visibility.js";
20
+ import { guardedQuerySQL, unfilteredQuerySQL } from "./spaces/guard.js";
19
21
  /**
20
22
  * Find the project's hatk.config.ts by walking up from cwd.
21
23
  * Returns the resolved config path, or falls back to 'hatk.config.ts'.
@@ -44,7 +46,7 @@ export async function createTestContext() {
44
46
  configureCdn(config.cdn);
45
47
  // Load and validate lexicons
46
48
  const lexicons = loadLexicons(resolve(configDir, 'lexicons'));
47
- const lexiconErrors = validateLexicons([...lexicons.values()]);
49
+ const lexiconErrors = validateLexicons(validatableLexicons(lexicons));
48
50
  if (lexiconErrors) {
49
51
  const messages = Object.entries(lexiconErrors).flatMap(([nsid, errs]) => errs.map((e) => `${nsid}: ${e}`));
50
52
  throw new Error(`Invalid lexicons:\n${messages.join('\n')}`);
@@ -84,7 +86,7 @@ export async function createTestContext() {
84
86
  // Discover feeds, xrpc, labels, hooks, og from server/ directory (skip setup scripts in tests)
85
87
  await initServer(resolve(configDir, 'server'), { skipSetup: true });
86
88
  return {
87
- db: { query: querySQL, run: runSQL },
89
+ db: { query: guardedQuerySQL, run: runSQL, unfiltered: unfilteredQuerySQL },
88
90
  _config: config,
89
91
  _collections: collections,
90
92
  loadFixtures: async (dir) => {
@@ -184,14 +186,14 @@ export async function createTestContext() {
184
186
  },
185
187
  feedContext: (opts) => {
186
188
  const paginateDeps = {
187
- db: { query: querySQL },
189
+ db: { query: guardedQuerySQL, unfiltered: unfilteredQuerySQL },
188
190
  cursor: opts?.cursor,
189
191
  limit: opts?.limit || 30,
190
192
  packCursor,
191
193
  unpackCursor,
192
194
  };
193
195
  return {
194
- db: { query: querySQL },
196
+ db: { query: guardedQuerySQL, unfiltered: unfilteredQuerySQL },
195
197
  params: opts?.params || {},
196
198
  cursor: opts?.cursor,
197
199
  limit: opts?.limit || 30,
@@ -201,6 +203,7 @@ export async function createTestContext() {
201
203
  isTakendown: isTakendownDid,
202
204
  filterTakendownDids,
203
205
  paginate: createPaginate(paginateDeps),
206
+ spaceFilter: spaceFilterSql,
204
207
  };
205
208
  },
206
209
  close: async () => {
package/dist/xrpc.d.ts CHANGED
@@ -29,6 +29,7 @@ export interface XrpcContext<P = Record<string, string>, Records extends Record<
29
29
  db: {
30
30
  query: (sql: string, params?: unknown[]) => Promise<unknown[]>;
31
31
  run: (sql: string, params?: unknown[]) => Promise<void>;
32
+ unfiltered: (sql: string, params?: unknown[]) => Promise<unknown[]>;
32
33
  };
33
34
  params: P;
34
35
  input: I;
@@ -93,7 +94,7 @@ export declare function configureCdn(cdn: CdnConfig | null): void;
93
94
  */
94
95
  export declare function blobUrl(did: string, ref: unknown, preset?: string): string | undefined;
95
96
  /** Build a full XrpcContext from request parameters. Reuses buildBaseContext for shared fields. */
96
- export declare function buildXrpcContext(params: Record<string, string>, cursor: string | undefined, limit: number, viewer: {
97
+ export declare function buildXrpcContext(params: XrpcParams, cursor: string | undefined, limit: number, viewer: {
97
98
  did: string;
98
99
  handle?: string;
99
100
  } | null, input?: unknown): XrpcContext;
@@ -107,8 +108,23 @@ export declare function initXrpc(xrpcDir: string): Promise<void>;
107
108
  export declare function registerXrpcHandler(nsid: string, handlerModule: {
108
109
  handler: (ctx: any) => Promise<any>;
109
110
  }): void;
111
+ /**
112
+ * What the query string gives a handler: one value per key, or every value
113
+ * when the key was repeated (`?dids=a&dids=b`).
114
+ */
115
+ export type XrpcParams = Record<string, any>;
116
+ /** Collect a query string into params, keeping every value of a repeated key. */
117
+ export declare function paramsFromSearch(search: URLSearchParams): XrpcParams;
118
+ /**
119
+ * Apply the lexicon's parameter schema before a handler runs: defaults,
120
+ * integer coercion, and arrays. A query string cannot say whether
121
+ * `dids=x` is one string or a one-element list, so the schema decides: a
122
+ * parameter declared `array` always reaches the handler as one, or a handler
123
+ * that spreads it would spread the characters of a DID instead.
124
+ */
125
+ export declare function coerceParams(params: XrpcParams, properties: Record<string, any>, required: string[]): void;
110
126
  /** Execute a registered XRPC handler by name. Returns null if no handler matches. */
111
- export declare function executeXrpc(name: string, params: Record<string, string>, cursor: string | undefined, limit: number, viewer?: {
127
+ export declare function executeXrpc(name: string, params: XrpcParams, cursor: string | undefined, limit: number, viewer?: {
112
128
  did: string;
113
129
  } | null, input?: unknown): Promise<any | null>;
114
130
  /** Call a registered XRPC handler directly (no HTTP). For use in SSR renderers. */
@@ -117,7 +133,7 @@ export declare function callXrpc(nsid: string, params?: Record<string, any>, inp
117
133
  * Register a core XRPC handler directly (no XrpcContext wrapping).
118
134
  * Used for built-in dev.hatk.* handlers that manage their own dependencies.
119
135
  */
120
- export declare function registerCoreXrpcHandler(nsid: string, fn: (params: Record<string, string>, cursor: string | undefined, limit: number, viewer: {
136
+ export declare function registerCoreXrpcHandler(nsid: string, fn: (params: XrpcParams, cursor: string | undefined, limit: number, viewer: {
121
137
  did: string;
122
138
  handle?: string;
123
139
  } | null, input?: unknown) => Promise<any>): void;
@@ -1 +1 @@
1
- {"version":3,"file":"xrpc.d.ts","sourceRoot":"","sources":["../src/xrpc.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"xrpc.d.ts","sourceRoot":"","sources":["../src/xrpc.ts"],"names":[],"mappings":"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"}
package/dist/xrpc.js CHANGED
@@ -26,10 +26,11 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
26
26
  * })
27
27
  * ```
28
28
  */
29
+ import { guardedQuerySQL, unfilteredQuerySQL } from "./spaces/guard.js";
29
30
  import { resolve, relative } from 'node:path';
30
31
  import { readdirSync, statSync } from 'node:fs';
31
32
  import { log, emit, timer } from "./logger.js";
32
- import { querySQL, runSQL, packCursor, unpackCursor, isTakendownDid, filterTakendownDids, searchRecords, findUriByFields, } from "./database/db.js";
33
+ import { runSQL, packCursor, unpackCursor, isTakendownDid, filterTakendownDids, searchRecords, findUriByFields, } from "./database/db.js";
33
34
  import { resolveRecords, buildBaseContext } from "./hydrate.js";
34
35
  import { getLexicon } from "./database/schema.js";
35
36
  import { createHmac } from 'node:crypto';
@@ -62,7 +63,7 @@ let _cdn = null;
62
63
  export function isLocalRelay() {
63
64
  try {
64
65
  const host = new URL(_relayUrl).hostname;
65
- return host === 'localhost' || host === '127.0.0.1' || host === '[::1]' || host === '::1' || host.endsWith('.localhost');
66
+ return (host === 'localhost' || host === '127.0.0.1' || host === '[::1]' || host === '::1' || host.endsWith('.localhost'));
66
67
  }
67
68
  catch {
68
69
  return false;
@@ -119,7 +120,7 @@ export function buildXrpcContext(params, cursor, limit, viewer, input) {
119
120
  const base = buildBaseContext(viewer);
120
121
  return {
121
122
  ...base,
122
- db: { query: querySQL, run: runSQL },
123
+ db: { query: guardedQuerySQL, run: runSQL, unfiltered: unfilteredQuerySQL },
123
124
  params,
124
125
  input: input || {},
125
126
  cursor,
@@ -212,20 +213,7 @@ export async function initXrpc(xrpcDir) {
212
213
  handlers.set(name, {
213
214
  name,
214
215
  execute: async (params, cursor, limit, viewer, input) => {
215
- // Apply defaults and coerce types from lexicon
216
- for (const [key, def] of Object.entries(paramProperties)) {
217
- if (params[key] == null && def.default != null) {
218
- params[key] = String(def.default);
219
- }
220
- if (params[key] != null && def.type === 'integer') {
221
- params[key] = Number(params[key]);
222
- }
223
- }
224
- for (const param of requiredParams) {
225
- if (!params[param]) {
226
- throw new InvalidRequestError(`Missing required parameter: ${param}`, 'InvalidRequest');
227
- }
228
- }
216
+ coerceParams(params, paramProperties, requiredParams);
229
217
  const ctx = buildXrpcContext(params, cursor, limit, viewer, input);
230
218
  return handler.handler(ctx);
231
219
  },
@@ -242,24 +230,64 @@ export function registerXrpcHandler(nsid, handlerModule) {
242
230
  handlers.set(nsid, {
243
231
  name: nsid,
244
232
  execute: async (params, cursor, limit, viewer, input) => {
245
- for (const [key, def] of Object.entries(paramProperties)) {
246
- if (params[key] == null && def.default != null) {
247
- params[key] = String(def.default);
248
- }
249
- if (params[key] != null && def.type === 'integer') {
250
- params[key] = Number(params[key]);
251
- }
252
- }
253
- for (const param of requiredParams) {
254
- if (!params[param]) {
255
- throw new InvalidRequestError(`Missing required parameter: ${param}`, 'InvalidRequest');
256
- }
257
- }
233
+ coerceParams(params, paramProperties, requiredParams);
258
234
  const ctx = buildXrpcContext(params, cursor, limit, viewer, input);
259
235
  return handlerModule.handler(ctx);
260
236
  },
261
237
  });
262
238
  }
239
+ /** Collect a query string into params, keeping every value of a repeated key. */
240
+ export function paramsFromSearch(search) {
241
+ const params = {};
242
+ for (const [key, value] of search) {
243
+ const prior = params[key];
244
+ if (prior === undefined)
245
+ params[key] = value;
246
+ else if (Array.isArray(prior))
247
+ prior.push(value);
248
+ else
249
+ params[key] = [prior, value];
250
+ }
251
+ return params;
252
+ }
253
+ function coerceScalar(value, def) {
254
+ // Only integers, as before: a boolean still reaches the handler as the
255
+ // string it was sent as, and handlers written against that keep working.
256
+ return def?.type === 'integer' ? Number(value) : value;
257
+ }
258
+ /**
259
+ * Apply the lexicon's parameter schema before a handler runs: defaults,
260
+ * integer coercion, and arrays. A query string cannot say whether
261
+ * `dids=x` is one string or a one-element list, so the schema decides: a
262
+ * parameter declared `array` always reaches the handler as one, or a handler
263
+ * that spreads it would spread the characters of a DID instead.
264
+ */
265
+ export function coerceParams(params, properties, required) {
266
+ for (const [key, def] of Object.entries(properties)) {
267
+ if (params[key] == null && def.default != null) {
268
+ params[key] = def.type === 'array' ? [...def.default] : def.type === 'string' ? String(def.default) : def.default;
269
+ }
270
+ if (params[key] == null)
271
+ continue;
272
+ if (def.type === 'array') {
273
+ const list = Array.isArray(params[key]) ? params[key] : [params[key]];
274
+ params[key] = list.map((v) => coerceScalar(v, def.items));
275
+ }
276
+ else if (Array.isArray(params[key])) {
277
+ // A scalar given twice: the last one wins, as a form submission would.
278
+ params[key] = coerceScalar(params[key][params[key].length - 1], def);
279
+ }
280
+ else {
281
+ params[key] = coerceScalar(params[key], def);
282
+ }
283
+ }
284
+ for (const param of required) {
285
+ const v = params[param];
286
+ if (v == null || v === '' || (Array.isArray(v) && v.length === 0)) {
287
+ throw new InvalidRequestError(`Missing required parameter: ${param}`, 'InvalidRequest');
288
+ }
289
+ }
290
+ }
263
291
  /** Execute a registered XRPC handler by name. Returns null if no handler matches. */
264
292
  export async function executeXrpc(name, params, cursor, limit, viewer, input) {
265
293
  const handler = handlers.get(name);
@@ -286,8 +314,10 @@ export async function callXrpc(nsid, params = {}, input) {
286
314
  }
287
315
  const stringParams = {};
288
316
  for (const [k, v] of Object.entries(params)) {
289
- if (v != null)
290
- stringParams[k] = String(v);
317
+ if (v == null)
318
+ continue;
319
+ // A list stays a list, as the query string would have carried it.
320
+ stringParams[k] = Array.isArray(v) ? v.map(String) : String(v);
291
321
  }
292
322
  const limit = params.limit ? Number(params.limit) : 20;
293
323
  const cursor = params.cursor ?? undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hatk/hatk",
3
- "version": "0.0.1-alpha.84",
3
+ "version": "0.0.1-alpha.86",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "hatk": "dist/cli.js"
@@ -25,18 +25,21 @@
25
25
  "./test": "./dist/test.js",
26
26
  "./config": "./dist/config.js",
27
27
  "./vite-plugin": "./dist/vite-plugin.js",
28
- "./renderer": "./dist/renderer.js"
28
+ "./renderer": "./dist/renderer.js",
29
+ "./spaces": "./dist/spaces/index.js"
29
30
  },
30
31
  "scripts": {
31
32
  "clean": "rm -rf dist",
32
33
  "build": "npm run clean && tsc -p tsconfig.build.json && cp -r src/templates dist/templates && cp -r src/lexicons dist/lexicons",
33
34
  "prepublishOnly": "npm run build",
34
- "test": "vitest run"
35
+ "test": "vitest run --coverage",
36
+ "test:watch": "vitest"
35
37
  },
36
38
  "dependencies": {
37
- "@bigmoves/lexicon": "^0.2.2",
39
+ "@bigmoves/lexicon": "^0.2.4",
38
40
  "@duckdb/node-api": "^1.4.4-r.1",
39
41
  "@hatk/oauth-client": "*",
42
+ "@noble/curves": "^2.4.0",
40
43
  "@resvg/resvg-js": "^2.6.2",
41
44
  "better-sqlite3": "^12.6.2",
42
45
  "satori": "^0.28.1",
@@ -46,6 +49,7 @@
46
49
  "devDependencies": {
47
50
  "@types/better-sqlite3": "^7.6.13",
48
51
  "@types/react": "^19.2.14",
52
+ "@vitest/coverage-v8": "^4.1.11",
49
53
  "vite": "^8.0.0"
50
54
  },
51
55
  "peerDependencies": {