@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
@@ -16,7 +16,7 @@ export const DUCKDB_DIALECT = {
16
16
  upsertPrefix: 'INSERT OR REPLACE INTO',
17
17
  jsonExtractString: (col, path) => `json_extract_string(${col}, '${path}')`,
18
18
  jsonArrayStringAgg: (col, path) => `list_string_agg(json_extract_string(${col}, '${path}'))`,
19
- listTablesQuery: `SELECT table_name FROM information_schema.tables WHERE table_schema = 'main' AND table_name NOT LIKE '\\_%' ESCAPE '\\\\'`,
19
+ listTablesQuery: `SELECT table_name FROM information_schema.tables WHERE table_schema = 'main' AND table_name NOT LIKE '\\_%' ESCAPE '\\'`,
20
20
  checkpointSQL: 'CHECKPOINT',
21
21
  currentTimestamp: 'CURRENT_TIMESTAMP',
22
22
  ilike: 'ILIKE',
@@ -48,7 +48,7 @@ export const SQLITE_DIALECT = {
48
48
  jsonArrayStringAgg: (col, path) => {
49
49
  return `(SELECT group_concat(je.value, ' ') FROM json_each(${col}, '${path}') je)`;
50
50
  },
51
- listTablesQuery: `SELECT name AS table_name FROM sqlite_master WHERE type='table' AND name NOT LIKE '\\_%' ESCAPE '\\\\'`,
51
+ listTablesQuery: `SELECT name AS table_name FROM sqlite_master WHERE type='table' AND name NOT LIKE '\\_%' ESCAPE '\\'`,
52
52
  checkpointSQL: null,
53
53
  currentTimestamp: 'CURRENT_TIMESTAMP',
54
54
  ilike: 'LIKE',
@@ -1 +1 @@
1
- {"version":3,"file":"fts.d.ts","sourceRoot":"","sources":["../../src/database/fts.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAuE5C,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,GAAG,IAAI,CAE3D;AAED,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAED,wBAAgB,aAAa,IAAI,UAAU,GAAG,IAAI,CAEjD;AAWD,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAE7D;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAElE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEvD;AAgFD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBrE;AAED,wBAAsB,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAehH;AAED,wBAAsB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAepF;AAED,wBAAsB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAYpF;AAokBD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIpD;AAED,wBAAsB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA4B5E"}
1
+ {"version":3,"file":"fts.d.ts","sourceRoot":"","sources":["../../src/database/fts.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAuE5C,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,GAAG,IAAI,CAE3D;AAED,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAED,wBAAgB,aAAa,IAAI,UAAU,GAAG,IAAI,CAEjD;AAWD,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAE7D;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAElE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEvD;AAqFD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBrE;AAED,wBAAsB,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAehH;AAED,wBAAsB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAepF;AAED,wBAAsB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAYpF;AAokBD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIpD;AAED,wBAAsB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA4B5E"}
@@ -102,14 +102,18 @@ function computeFtsSchema(collection) {
102
102
  const selectExprs = ['t.uri', 't.cid', 't.did', 't.indexed_at'];
103
103
  const searchColNames = [];
104
104
  for (const col of schema.columns) {
105
+ // A blob column holds {ref, mimeType, size} metadata — no searchable text.
106
+ // This has to be decided on isJson, not on sqlType: SQLite's jsonType is
107
+ // TEXT, so a blob column would otherwise fall into the plain-TEXT branch
108
+ // below and be indexed as raw JSON.
109
+ if (col.isJson && record?.properties?.[col.originalName]?.type === 'blob')
110
+ continue;
105
111
  if (col.sqlType === 'TEXT') {
106
112
  selectExprs.push(`t.${q(col.name)}`);
107
113
  searchColNames.push(col.name);
108
114
  }
109
115
  else if (col.isJson && record?.properties) {
110
116
  const prop = record.properties[col.originalName];
111
- if (prop?.type === 'blob')
112
- continue; // skip blobs
113
117
  if (prop && lexicon) {
114
118
  const derived = jsonSearchColumns(`t.${q(col.name)}`, prop, lexicon, dialect);
115
119
  if (derived.length > 0) {
@@ -3,5 +3,5 @@ export type { SqlDialect } from './dialect.ts';
3
3
  export { getDialect, DUCKDB_DIALECT, SQLITE_DIALECT } from './dialect.ts';
4
4
  export { createAdapter } from './adapter-factory.ts';
5
5
  export { initDatabase, closeDatabase, querySQL, runSQL, insertRecord, deleteRecord, queryRecords, searchRecords, getRecordByUri, getCursor, setCursor, bulkInsertRecords, packCursor, unpackCursor, } from './db.ts';
6
- export { type TableSchema, type ColumnDef, type ChildTableSchema, loadLexicons, discoverCollections, buildSchemas, generateTableSchema, generateCreateTableSQL, toSnakeCase, getLexicon, getLexiconArray, getAllLexicons, storeLexicons, } from './schema.ts';
6
+ export { type TableSchema, type ColumnDef, type ChildTableSchema, loadLexicons, discoverCollections, buildSchemas, generateTableSchema, generateCreateTableSQL, toSnakeCase, getLexicon, getLexiconArray, getAllLexicons, storeLexicons, columnName, } from './schema.ts';
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/database/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AACjF,YAAY,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAGpD,OAAO,EACL,YAAY,EACZ,aAAa,EACb,QAAQ,EACR,MAAM,EACN,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,cAAc,EACd,SAAS,EACT,SAAS,EACT,iBAAiB,EACjB,UAAU,EACV,YAAY,GACb,MAAM,SAAS,CAAA;AAGhB,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,YAAY,EACZ,mBAAmB,EACnB,YAAY,EACZ,mBAAmB,EACnB,sBAAsB,EACtB,WAAW,EACX,UAAU,EACV,eAAe,EACf,cAAc,EACd,aAAa,GACd,MAAM,aAAa,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/database/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AACjF,YAAY,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAGpD,OAAO,EACL,YAAY,EACZ,aAAa,EACb,QAAQ,EACR,MAAM,EACN,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,cAAc,EACd,SAAS,EACT,SAAS,EACT,iBAAiB,EACjB,UAAU,EACV,YAAY,GACb,MAAM,SAAS,CAAA;AAGhB,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,YAAY,EACZ,mBAAmB,EACnB,YAAY,EACZ,mBAAmB,EACnB,sBAAsB,EACtB,WAAW,EACX,UAAU,EACV,eAAe,EACf,cAAc,EACd,aAAa,EACb,UAAU,GACX,MAAM,aAAa,CAAA"}
@@ -3,4 +3,4 @@ export { createAdapter } from "./adapter-factory.js";
3
3
  // Re-export commonly used functions from db.ts
4
4
  export { initDatabase, closeDatabase, querySQL, runSQL, insertRecord, deleteRecord, queryRecords, searchRecords, getRecordByUri, getCursor, setCursor, bulkInsertRecords, packCursor, unpackCursor, } from "./db.js";
5
5
  // Re-export schema utilities
6
- export { loadLexicons, discoverCollections, buildSchemas, generateTableSchema, generateCreateTableSQL, toSnakeCase, getLexicon, getLexiconArray, getAllLexicons, storeLexicons, } from "./schema.js";
6
+ export { loadLexicons, discoverCollections, buildSchemas, generateTableSchema, generateCreateTableSQL, toSnakeCase, getLexicon, getLexiconArray, getAllLexicons, storeLexicons, columnName, } from "./schema.js";
@@ -35,8 +35,10 @@ export interface ChildTableSchema {
35
35
  columns: ColumnDef[];
36
36
  }
37
37
  export declare function toSnakeCase(str: string): string;
38
+ export declare function columnName(fieldName: string): string;
38
39
  export declare function q(name: string): string;
39
40
  export declare function loadLexicons(lexiconsDir: string): Map<string, any>;
41
+ export declare function validatableLexicons(lexicons: Map<string, any>): any[];
40
42
  /**
41
43
  * Discover collections by scanning lexicons for record-type definitions.
42
44
  */
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/database/schema.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAG9C,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;IACd,MAAM,EAAE,OAAO,CAAA;CAChB;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,SAAS,EAAE,CAAA;IACpB,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,iBAAiB,EAAE,CAAA;CAC9B;AAED,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,SAAS,EAAE,CAAA;IACpB,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,QAAQ,EAAE,gBAAgB,EAAE,CAAA;IAC5B,MAAM,EAAE,gBAAgB,EAAE,CAAA;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,gBAAgB,EAAE,MAAM,CAAA;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,SAAS,EAAE,CAAA;CACrB;AAGD,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE/C;AAGD,wBAAgB,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEtC;AA+CD,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CASlE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,EAAE,CASxE;AAID,wBAAgB,aAAa,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAI9D;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,GAAG,SAAS,CAExD;AAED,wBAAgB,cAAc,IAAI,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,CAAC,CAEtE;AAED,iFAAiF;AACjF,wBAAgB,eAAe,IAAI,GAAG,EAAE,CAEvC;AAyHD,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,GAAG,EACZ,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,OAAO,GAAE,UAA2B,GACnC,WAAW,CA+Gb;AAGD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,GAAE,UAA2B,GAAG,MAAM,CAsExG;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,EAC1B,WAAW,EAAE,MAAM,EAAE,EACrB,OAAO,GAAE,UAA2B,GACnC;IAAE,OAAO,EAAE,WAAW,EAAE,CAAC;IAAC,aAAa,EAAE,MAAM,EAAE,CAAA;CAAE,CA2BrD"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/database/schema.ts"],"names":[],"mappings":"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"}
@@ -5,6 +5,18 @@ import { DUCKDB_DIALECT } from "./dialect.js";
5
5
  export function toSnakeCase(str) {
6
6
  return str.replace(/([A-Z])/g, '_$1').toLowerCase();
7
7
  }
8
+ /**
9
+ * The columns every generated table carries before any lexicon field. A record
10
+ * property with one of these names — `fyi.opensocial.space` has a `uri` — would
11
+ * otherwise produce a CREATE TABLE with a duplicate column and fail at boot.
12
+ * It is stored under `record_<name>` instead; `originalName` still carries the
13
+ * lexicon's spelling, which is what every reshaped row is keyed by.
14
+ */
15
+ const ENVELOPE_COLUMNS = new Set(['uri', 'cid', 'did', 'space', 'indexed_at', 'handle']);
16
+ export function columnName(fieldName) {
17
+ const snake = toSnakeCase(fieldName);
18
+ return ENVELOPE_COLUMNS.has(snake) ? `record_${snake}` : snake;
19
+ }
8
20
  // Quote a column name to avoid conflicts with SQL reserved words
9
21
  export function q(name) {
10
22
  return `"${name}"`;
@@ -68,6 +80,20 @@ export function loadLexicons(lexiconsDir) {
68
80
  }
69
81
  return lexicons;
70
82
  }
83
+ /**
84
+ * Lexicons the validator cannot model but hatk legitimately keeps.
85
+ *
86
+ * A space type is how the indexer learns which collections a space holds, and
87
+ * a permission set is what an OAuth scope resolves through; both are real
88
+ * lexicons an app vendors on purpose. `validateLexicons` reports each as an
89
+ * unknown definition type, which would read as a fatal schema error at boot.
90
+ * Only these two are set aside — anything else unknown is still a mistake the
91
+ * validator should refuse.
92
+ */
93
+ const UNMODELLED_DEF_TYPES = new Set(['space', 'permission-set']);
94
+ export function validatableLexicons(lexicons) {
95
+ return [...lexicons.values()].filter((lex) => !UNMODELLED_DEF_TYPES.has(lex.defs?.main?.type));
96
+ }
71
97
  /**
72
98
  * Discover collections by scanning lexicons for record-type definitions.
73
99
  */
@@ -158,7 +184,10 @@ function resolveUnionBranch(ref, collection, fieldName, defs, lexicons, dialect)
158
184
  let arrayField;
159
185
  let wrapperField;
160
186
  let propSource = branchDef.properties;
161
- const branchRequired = new Set(branchDef.required || []);
187
+ // `required` has to travel with propSource: once the columns come from the
188
+ // item (or wrapper target) def, the outer branch's `required` describes the
189
+ // wrapper property, not these columns.
190
+ let branchRequired = new Set(branchDef.required || []);
162
191
  // Check for single-property wrapper patterns
163
192
  const propEntries = Object.entries(branchDef.properties);
164
193
  if (propEntries.length === 1) {
@@ -171,6 +200,7 @@ function resolveUnionBranch(ref, collection, fieldName, defs, lexicons, dialect)
171
200
  isArray = true;
172
201
  arrayField = onlyField;
173
202
  propSource = itemDef.properties;
203
+ branchRequired = new Set(itemDef.required || []);
174
204
  }
175
205
  }
176
206
  else if (onlyProp.type === 'ref' && onlyProp.ref) {
@@ -179,6 +209,7 @@ function resolveUnionBranch(ref, collection, fieldName, defs, lexicons, dialect)
179
209
  if (refDef?.type === 'object' && refDef.properties) {
180
210
  wrapperField = onlyField;
181
211
  propSource = refDef.properties;
212
+ branchRequired = new Set(refDef.required || []);
182
213
  }
183
214
  }
184
215
  }
@@ -229,7 +260,7 @@ export function generateTableSchema(nsid, lexicon, lexicons, dialect = DUCKDB_DI
229
260
  }
230
261
  // Still add the JSON column for the raw union value
231
262
  columns.push({
232
- name: toSnakeCase(fieldName),
263
+ name: columnName(fieldName),
233
264
  originalName: fieldName,
234
265
  sqlType: dialect.jsonType,
235
266
  notNull: required.has(fieldName),
@@ -287,7 +318,7 @@ export function generateTableSchema(nsid, lexicon, lexicons, dialect = DUCKDB_DI
287
318
  }
288
319
  else {
289
320
  columns.push({
290
- name: toSnakeCase(fieldName),
321
+ name: columnName(fieldName),
291
322
  originalName: fieldName,
292
323
  sqlType,
293
324
  notNull: required.has(fieldName),
@@ -308,10 +339,16 @@ export function generateTableSchema(nsid, lexicon, lexicons, dialect = DUCKDB_DI
308
339
  }
309
340
  // Generate CREATE TABLE SQL from a TableSchema
310
341
  export function generateCreateTableSQL(schema, dialect = DUCKDB_DIALECT) {
342
+ // `space` is the permissioned space a row came from, and NULL for everything
343
+ // off the firehose. It is the only per-row visibility marker in the schema:
344
+ // a space record is readable by whoever the space's authority admits, so no
345
+ // read may serve one without knowing who is asking. NULL means "public repo
346
+ // data", which is what every existing row is.
311
347
  const lines = [
312
348
  ' uri TEXT PRIMARY KEY',
313
349
  ' cid TEXT',
314
350
  ' did TEXT NOT NULL',
351
+ ' space TEXT',
315
352
  ` indexed_at ${dialect.timestampType} NOT NULL`,
316
353
  ];
317
354
  for (const col of schema.columns) {
@@ -323,6 +360,9 @@ export function generateCreateTableSQL(schema, dialect = DUCKDB_DIALECT) {
323
360
  const indexes = [
324
361
  `CREATE INDEX IF NOT EXISTS idx_${prefix}_indexed ON ${schema.tableName}(indexed_at DESC);`,
325
362
  `CREATE INDEX IF NOT EXISTS idx_${prefix}_author ON ${schema.tableName}(did);`,
363
+ // Every read that serves space rows filters on this column, and every read
364
+ // that does not still tests it for NULL.
365
+ `CREATE INDEX IF NOT EXISTS idx_${prefix}_space ON ${schema.tableName}(space);`,
326
366
  ];
327
367
  // Index ref columns for hydration lookups
328
368
  for (const refCol of schema.refColumns) {
@@ -381,11 +421,13 @@ export function buildSchemas(lexicons, collections, dialect = DUCKDB_DIALECT) {
381
421
  uri TEXT PRIMARY KEY,
382
422
  cid TEXT,
383
423
  did TEXT NOT NULL,
424
+ space TEXT,
384
425
  indexed_at ${dialect.timestampType} NOT NULL,
385
426
  data ${dialect.jsonType}
386
427
  );
387
428
  CREATE INDEX IF NOT EXISTS idx_${nsid.replace(/\./g, '_')}_indexed ON "${nsid}"(indexed_at DESC);
388
- CREATE INDEX IF NOT EXISTS idx_${nsid.replace(/\./g, '_')}_author ON "${nsid}"(did);`;
429
+ CREATE INDEX IF NOT EXISTS idx_${nsid.replace(/\./g, '_')}_author ON "${nsid}"(did);
430
+ CREATE INDEX IF NOT EXISTS idx_${nsid.replace(/\./g, '_')}_space ON "${nsid}"(space);`;
389
431
  schemas.push({ collection: nsid, tableName: `"${nsid}"`, columns: [], refColumns: [], children: [], unions: [] });
390
432
  ddlStatements.push(genericDDL);
391
433
  continue;
@@ -1 +1 @@
1
- {"version":3,"file":"dev-entry.d.ts","sourceRoot":"","sources":["../src/dev-entry.ts"],"names":[],"mappings":"AAyHA,eAAO,MAAM,OAAO,yCAKlB,CAAA;AAEF,yEAAyE;AACzE,wBAAsB,YAAY,kBAEjC;AAED,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AACpC,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"dev-entry.d.ts","sourceRoot":"","sources":["../src/dev-entry.ts"],"names":[],"mappings":"AA+HA,eAAO,MAAM,OAAO,yCAKlB,CAAA;AAEF,yEAAyE;AACzE,wBAAsB,YAAY,kBAEjC;AAED,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AACpC,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA"}
package/dist/dev-entry.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * Boots hatk infrastructure and exports the fetch handler.
4
4
  */
5
5
  import { loadConfig } from "./config.js";
6
- import { loadLexicons, storeLexicons, discoverCollections, buildSchemas } from "./database/schema.js";
6
+ import { validatableLexicons, loadLexicons, storeLexicons, discoverCollections, buildSchemas, } from "./database/schema.js";
7
7
  import { discoverViews } from "./views.js";
8
8
  import { initDatabase, migrateSchema, getSchemaDump } from "./database/db.js";
9
9
  import { createAdapter } from "./database/adapter-factory.js";
@@ -32,7 +32,7 @@ configureRelay(config.relay);
32
32
  configurePlc(config.plc);
33
33
  configureCdn(config.cdn);
34
34
  const lexicons = loadLexicons(resolve(configDir, 'lexicons'));
35
- const lexiconErrors = validateLexicons([...lexicons.values()]);
35
+ const lexiconErrors = validateLexicons(validatableLexicons(lexicons));
36
36
  if (lexiconErrors) {
37
37
  for (const [nsid, errors] of Object.entries(lexiconErrors)) {
38
38
  for (const err of errors)
package/dist/feeds.d.ts CHANGED
@@ -17,6 +17,8 @@ export interface PaginateResult<T> {
17
17
  export interface FeedContext {
18
18
  db: {
19
19
  query: (sql: string, params?: unknown[]) => Promise<unknown[]>;
20
+ /** Raw SQL with the permissioned-space guard off. See `spaces/guard.ts`. */
21
+ unfiltered: (sql: string, params?: unknown[]) => Promise<unknown[]>;
20
22
  };
21
23
  params: Record<string, string>;
22
24
  cursor?: string;
@@ -34,6 +36,18 @@ export interface FeedContext {
34
36
  paginate: <T extends {
35
37
  uri: string;
36
38
  }>(sql: string, opts?: PaginateOpts) => Promise<PaginateResult<T>>;
39
+ /**
40
+ * The permissioned-space gate, for SQL this feed builds without `paginate`.
41
+ *
42
+ * `paginate` applies it already. This is for a feed that runs its own query
43
+ * — a raw `ctx.db.query` over a record table has to carry it, or it serves
44
+ * space rows to whoever asks. See `spaceFilterSql`.
45
+ */
46
+ spaceFilter: (alias: string, startIdx: number) => {
47
+ sql: string;
48
+ params: string[];
49
+ nextIdx: number;
50
+ };
37
51
  }
38
52
  type FeedGenerate = (ctx: FeedContext & {
39
53
  ok: (value: FeedResult) => Checked<FeedResult>;
@@ -1 +1 @@
1
- {"version":3,"file":"feeds.d.ts","sourceRoot":"","sources":["../src/feeds.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,cAAc,CAAA;AACpD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAE7C,YAAY,EAAE,WAAW,EAAE,GAAG,EAAE,CAAA;AAEhC,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAA;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B,IAAI,EAAE,CAAC,EAAE,CAAA;IACT,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;CAC3B;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE;QAAE,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;KAAE,CAAA;IACtE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IAC9B,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,MAAM,CAAA;IAC7D,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IACzE,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC9C,mBAAmB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAA;IAC7D,QAAQ,EAAE,CAAC,CAAC,SAAS;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,YAAY,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAA;CACtG;AAkBD,KAAK,YAAY,GAAG,CAClB,GAAG,EAAE,WAAW,GAAG;IAAE,EAAE,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,UAAU,CAAC,CAAA;CAAE,KAClE,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAA;AAEjC,KAAK,QAAQ,GACT;IACE,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,YAAY,CAAA;IACtB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,GAAG,CAAC,GAAG,CAAC,EAAE,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;CACtE,GACD;IACE,UAAU,CAAC,EAAE,KAAK,CAAA;IAClB,IAAI,CAAC,EAAE,KAAK,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,YAAY,CAAA;IACtB,OAAO,EAAE,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,GAAG,CAAC,GAAG,CAAC,EAAE,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;CACrE,CAAA;AAEL,wBAAgB,cAAc,CAAC,IAAI,EAAE;IACnC,EAAE,EAAE;QAAE,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;KAAE,CAAA;IACtE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,MAAM,CAAA;IAC7D,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;CAC1E,IACe,CAAC,SAAS;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,EAAE,KAAK,MAAM,EAAE,OAAO,YAAY,KAAG,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAmDvG;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,QAAQ;oBACoB,GAAG;gBA3E9C,MAAM;WACX,MAAM;WACN,MAAM;cAEH,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,GAAG,CAAC,GAAG,CAAC,EAAE,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC;;;oBAuEd,GAAG;iBApE7C,KAAK;WACX,KAAK;WACL,MAAM;aAEJ,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,GAAG,CAAC,GAAG,CAAC,EAAE,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC;;EAiEzE;AAED,4DAA4D;AAC5D,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,GAAG,IAAI,CAuCzF;AAID,wBAAsB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA8D/D;AAED,8DAA8D;AAC9D,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GAC9B,OAAO,CAAC;IAAE,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAoBzE;AAED,wBAAgB,SAAS,IAAI;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAAE,CAE7D;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD"}
1
+ {"version":3,"file":"feeds.d.ts","sourceRoot":"","sources":["../src/feeds.ts"],"names":[],"mappings":"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"}
package/dist/feeds.js CHANGED
@@ -9,8 +9,10 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
9
9
  import { resolve } from 'node:path';
10
10
  import { readdirSync } from 'node:fs';
11
11
  import { log } from "./logger.js";
12
- import { querySQL, packCursor, unpackCursor, isTakendownDid, filterTakendownDids, resolveHandleToDid, } from "./database/db.js";
12
+ import { packCursor, unpackCursor, isTakendownDid, filterTakendownDids, resolveHandleToDid, } from "./database/db.js";
13
13
  import { resolveRecords, buildBaseContext } from "./hydrate.js";
14
+ import { spaceFilterSql } from "./spaces/visibility.js";
15
+ import { guardedQuerySQL, unfilteredQuerySQL } from "./spaces/guard.js";
14
16
  export function createPaginate(deps) {
15
17
  return async (sql, opts) => {
16
18
  const { db, cursor, limit, packCursor: pack, unpackCursor: unpack } = deps;
@@ -25,6 +27,16 @@ export function createPaginate(deps) {
25
27
  const op = order === 'ASC' ? '>' : '<';
26
28
  let paramIdx = userParams.length + 1;
27
29
  const sqlParams = [...userParams];
30
+ // The permissioned-space gate, applied to whatever table the ordering
31
+ // names — the same table `cid` is read from. Feed SQL is hand-written and
32
+ // nothing can inject a predicate into a string somebody else wrote, so
33
+ // building it here is the only way every feed gets it without every feed
34
+ // author remembering to. Outside a viewer's scope it is `space IS NULL`
35
+ // and binds nothing, which is what it compiles to on an instance that
36
+ // indexes no space.
37
+ const gate = spaceFilterSql(prefix.replace(/\.$/, ''), paramIdx);
38
+ sqlParams.push(...gate.params);
39
+ paramIdx = gate.nextIdx;
28
40
  // Build cursor condition
29
41
  let cursorCondition = '';
30
42
  if (cursor) {
@@ -35,15 +47,12 @@ export function createPaginate(deps) {
35
47
  paramIdx += 3;
36
48
  }
37
49
  }
38
- // Detect existing WHERE and append cursor condition
50
+ // Detect existing WHERE and append the gate and the cursor condition
39
51
  let fullSql = sql;
40
- if (cursorCondition) {
41
- if (/\bWHERE\b/i.test(sql)) {
42
- fullSql += ` AND ${cursorCondition}`;
43
- }
44
- else {
45
- fullSql += ` WHERE ${cursorCondition}`;
46
- }
52
+ const appended = [gate.sql, ...(cursorCondition ? [cursorCondition] : [])];
53
+ if (appended.length) {
54
+ const joiner = /\bWHERE\b/i.test(sql) ? 'AND' : 'WHERE';
55
+ fullSql += ` ${joiner} ${appended.join(' AND ')}`;
47
56
  }
48
57
  fullSql += ` ORDER BY ${orderBy} ${order}, ${cidCol} ${order} LIMIT $${paramIdx}`;
49
58
  sqlParams.push(limit + 1);
@@ -70,14 +79,14 @@ export function registerFeed(name, generator) {
70
79
  view: generator.view,
71
80
  generate: async (params, cursor, limit, viewer) => {
72
81
  const paginateDeps = {
73
- db: { query: querySQL },
82
+ db: { query: guardedQuerySQL, unfiltered: unfilteredQuerySQL },
74
83
  cursor,
75
84
  limit,
76
85
  packCursor,
77
86
  unpackCursor,
78
87
  };
79
88
  const ctx = {
80
- db: { query: querySQL },
89
+ db: { query: guardedQuerySQL, unfiltered: unfilteredQuerySQL },
81
90
  params,
82
91
  cursor,
83
92
  limit,
@@ -87,6 +96,7 @@ export function registerFeed(name, generator) {
87
96
  isTakendown: isTakendownDid,
88
97
  filterTakendownDids,
89
98
  paginate: createPaginate(paginateDeps),
99
+ spaceFilter: spaceFilterSql,
90
100
  };
91
101
  const result = await generator.generate(ctx);
92
102
  if (Array.isArray(result)) {
@@ -123,14 +133,14 @@ export async function initFeeds(feedsDir) {
123
133
  view: generator.view,
124
134
  generate: async (params, cursor, limit, viewer) => {
125
135
  const paginateDeps = {
126
- db: { query: querySQL },
136
+ db: { query: guardedQuerySQL, unfiltered: unfilteredQuerySQL },
127
137
  cursor,
128
138
  limit,
129
139
  packCursor,
130
140
  unpackCursor,
131
141
  };
132
142
  const ctx = {
133
- db: { query: querySQL },
143
+ db: { query: guardedQuerySQL, unfiltered: unfilteredQuerySQL },
134
144
  params,
135
145
  cursor,
136
146
  limit,
@@ -140,6 +150,7 @@ export async function initFeeds(feedsDir) {
140
150
  isTakendown: isTakendownDid,
141
151
  filterTakendownDids,
142
152
  paginate: createPaginate(paginateDeps),
153
+ spaceFilter: spaceFilterSql,
143
154
  };
144
155
  const result = await generator.generate(ctx);
145
156
  if (Array.isArray(result)) {
package/dist/hooks.d.ts CHANGED
@@ -9,6 +9,13 @@ export type OnLoginCtx = Omit<BaseContext, 'db'> & {
9
9
  db: {
10
10
  query: (sql: string, params?: unknown[]) => Promise<unknown[]>;
11
11
  run: (sql: string, params?: unknown[]) => Promise<void>;
12
+ /**
13
+ * Raw SQL with the permissioned-space guard off. A hook is not serving a
14
+ * viewer, and discovering what a space contains — which spaces a
15
+ * community lists, say — is exactly a read over space rows that no scope
16
+ * is open for. See `spaces/guard.ts`.
17
+ */
18
+ unfiltered: (sql: string, params?: unknown[]) => Promise<unknown[]>;
12
19
  };
13
20
  /** Trigger a backfill for a DID and wait for it to complete. */
14
21
  ensureRepo: (did: string) => Promise<void>;
@@ -43,6 +50,8 @@ export type OnCommitCtx = {
43
50
  db: {
44
51
  query: (sql: string, params?: unknown[]) => Promise<unknown[]>;
45
52
  run: (sql: string, params?: unknown[]) => Promise<void>;
53
+ /** Raw SQL with the permissioned-space guard off. See `spaces/guard.ts`. */
54
+ unfiltered: (sql: string, params?: unknown[]) => Promise<unknown[]>;
46
55
  };
47
56
  /** Typed record lookup (same as BaseContext). */
48
57
  lookup: BaseContext['lookup'];
@@ -65,6 +74,14 @@ export declare function defineHook(event: 'on-commit', options: {
65
74
  /**
66
75
  * Discover and load the on-login hook from the project's `hooks/` directory.
67
76
  * Looks for `on-login.ts` or `on-login.js`. Safe to call if no hook exists.
77
+ *
78
+ * The documented contract is `export default defineHook('on-login', handler)`,
79
+ * which is an object carrying the handler — the same shape the `server/`
80
+ * scanner recognizes by its `__type` tag. A bare async function is still
81
+ * accepted, because that is what the legacy `hooks/` directory used before
82
+ * `defineHook` existed. Anything else is reported rather than installed: a
83
+ * non-callable hook would otherwise fail silently on every login, since
84
+ * `fireOnLoginHook` swallows what the hook throws.
68
85
  */
69
86
  export declare function loadOnLoginHook(hooksDir: string): Promise<void>;
70
87
  /** Register a hook from a scanned server/ module. */
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"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"}
package/dist/hooks.js CHANGED
@@ -63,6 +63,14 @@ const onCommitHooks = [];
63
63
  /**
64
64
  * Discover and load the on-login hook from the project's `hooks/` directory.
65
65
  * Looks for `on-login.ts` or `on-login.js`. Safe to call if no hook exists.
66
+ *
67
+ * The documented contract is `export default defineHook('on-login', handler)`,
68
+ * which is an object carrying the handler — the same shape the `server/`
69
+ * scanner recognizes by its `__type` tag. A bare async function is still
70
+ * accepted, because that is what the legacy `hooks/` directory used before
71
+ * `defineHook` existed. Anything else is reported rather than installed: a
72
+ * non-callable hook would otherwise fail silently on every login, since
73
+ * `fireOnLoginHook` swallows what the hook throws.
66
74
  */
67
75
  export async function loadOnLoginHook(hooksDir) {
68
76
  const tsPath = resolve(hooksDir, 'on-login.ts');
@@ -71,9 +79,24 @@ export async function loadOnLoginHook(hooksDir) {
71
79
  if (!path)
72
80
  return;
73
81
  const mod = await import(__rewriteRelativeImportExtension(/* @vite-ignore */ `${path}?t=${Date.now()}`));
74
- onLoginHook = mod.default;
82
+ const handler = resolveHookHandler(mod.default);
83
+ if (!handler) {
84
+ emit('hooks', 'on_login_load_error', {
85
+ path,
86
+ error: 'default export is not a function or a defineHook("on-login", ...) result',
87
+ });
88
+ return;
89
+ }
90
+ onLoginHook = handler;
75
91
  log('[hooks] on-login hook loaded');
76
92
  }
93
+ /** Unwrap a hook module's default export into the callable handler, if it has one. */
94
+ function resolveHookHandler(exported) {
95
+ if (typeof exported === 'function')
96
+ return exported;
97
+ const handler = exported?.handler;
98
+ return typeof handler === 'function' ? handler : null;
99
+ }
77
100
  /** Mark a DID as pending, trigger auto-backfill, and wait for completion. */
78
101
  async function ensureRepo(did) {
79
102
  await setRepoStatus(did, 'pending');
@@ -102,7 +125,7 @@ export async function fireOnLoginHook(did, oauthConfig) {
102
125
  const hookPromise = onLoginHook({
103
126
  ...base,
104
127
  did,
105
- db: { query: base.db.query, run: runSQL },
128
+ db: { query: base.db.query, run: runSQL, unfiltered: base.db.unfiltered },
106
129
  ensureRepo,
107
130
  createRecord: async (collection, record, opts) => {
108
131
  if (!oauthConfig)
@@ -147,7 +170,7 @@ export function fireOnCommitHooks(items) {
147
170
  record: item.record,
148
171
  repo: item.authorDid,
149
172
  uri: item.uri,
150
- db: { query: base.db.query, run: runSQL },
173
+ db: { query: base.db.query, run: runSQL, unfiltered: base.db.unfiltered },
151
174
  lookup: base.lookup,
152
175
  push,
153
176
  })
package/dist/hydrate.d.ts CHANGED
@@ -7,12 +7,54 @@ export interface BaseContext {
7
7
  } | null;
8
8
  db: {
9
9
  query: (sql: string, params?: unknown[]) => Promise<unknown[]>;
10
+ /**
11
+ * Raw SQL with the permissioned-space guard off. Named so it can be found
12
+ * with grep: for results never served to a viewer, such as an admin
13
+ * rollup. See `spaces/guard.ts`.
14
+ */
15
+ unfiltered: (sql: string, params?: unknown[]) => Promise<unknown[]>;
10
16
  };
11
17
  getRecords: <R = unknown>(collection: string, uris: string[]) => Promise<Map<string, Row<R>>>;
12
18
  lookup: <R = unknown>(collection: string, field: string, values: string[]) => Promise<Map<string, Row<R>>>;
13
19
  count: (collection: string, field: string, values: string[]) => Promise<Map<string, number>>;
14
20
  labels: (uris: string[]) => Promise<Map<string, unknown[]>>;
15
21
  blobUrl: (did: string, ref: unknown, preset?: string) => string | undefined;
22
+ /**
23
+ * The permissioned-space gate, for hand-written SQL.
24
+ *
25
+ * Every helper on this context applies it already. Raw SQL through
26
+ * `ctx.db.query` cannot — nothing can inject a predicate into a string
27
+ * somebody else wrote — so a query that selects from a collection a space
28
+ * writes into has to apply it itself:
29
+ *
30
+ * ```ts
31
+ * const gate = ctx.spaceFilter('t', 2)
32
+ * ctx.db.query(
33
+ * `SELECT t.* FROM "app.example.post" t WHERE t.did = $1 AND ${gate.sql}`,
34
+ * ['did:plc:someone', ...gate.params],
35
+ * )
36
+ * ```
37
+ *
38
+ * Outside a viewer's scope this is `t.space IS NULL` and binds nothing, so on
39
+ * an instance that indexes no space it costs one predicate on an indexed
40
+ * column and changes no result.
41
+ */
42
+ spaceFilter: (alias: string, startIdx: number) => {
43
+ sql: string;
44
+ params: string[];
45
+ nextIdx: number;
46
+ };
47
+ /**
48
+ * Every record of a collection in a space the viewer may read. Empty for a
49
+ * space outside their scope; see `requireSpace` for the explicit refusal.
50
+ */
51
+ spaceRecords: <R = unknown>(collection: string, space: string) => Promise<Row<R>[]>;
52
+ /** Every record where `field` is one of `values` — the many-rows counterpart of `lookup`. */
53
+ records: <R = unknown>(collection: string, field: string, values: string[]) => Promise<Row<R>[]>;
54
+ /** Throw `NotAuthorized` unless the viewer may read this space. */
55
+ requireSpace: (space: string) => void;
56
+ /** The URL this appview serves a space blob at, for the viewer asking. */
57
+ spaceBlobUrl: (space: string, repo: string, cid: string) => string;
16
58
  }
17
59
  /** Fetch records for URIs, reshape them, and filter out taken-down DIDs. */
18
60
  export declare function resolveRecords(uris: string[]): Promise<Row<unknown>[]>;
@@ -1 +1 @@
1
- {"version":3,"file":"hydrate.d.ts","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAA;AAEzC,YAAY,EAAE,GAAG,EAAE,CAAA;AAInB,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IAC/C,EAAE,EAAE;QAAE,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;KAAE,CAAA;IACtE,UAAU,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC7F,MAAM,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC1G,KAAK,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAC5F,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,CAAA;IAC3D,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;CAC5E;AAID,4EAA4E;AAC5E,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAoC5E;AAID,yCAAyC;AACzC,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GAAG,WAAW,CAkB7F"}
1
+ {"version":3,"file":"hydrate.d.ts","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAA;AAEzC,YAAY,EAAE,GAAG,EAAE,CAAA;AAInB,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IAC/C,EAAE,EAAE;QACF,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;QAC9D;;;;WAIG;QACH,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;KACpE,CAAA;IACD,UAAU,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC7F,MAAM,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC1G,KAAK,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAC5F,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,CAAA;IAC3D,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IAC3E;;;;;;;;;;;;;;;;;;;OAmBG;IACH,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;IACpG;;;OAGG;IACH,YAAY,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IACnF,6FAA6F;IAC7F,OAAO,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IAChG,mEAAmE;IACnE,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACrC,0EAA0E;IAC1E,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,MAAM,CAAA;CACnE;AAID,4EAA4E;AAC5E,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAsC5E;AAID,yCAAyC;AACzC,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GAAG,WAAW,CAuB7F"}
package/dist/hydrate.js CHANGED
@@ -1,15 +1,23 @@
1
- import { getRecordsMap, countByFieldBatch, lookupByFieldBatch, querySQL, queryLabelsForUris, filterTakendownDids, getRecordsByUris, reshapeRow, } from "./database/db.js";
1
+ import { getRecordsMap, countByFieldBatch, lookupByFieldBatch, queryLabelsForUris, filterTakendownDids, getRecordsByUris, reshapeRow, } from "./database/db.js";
2
2
  import { blobUrl } from "./xrpc.js";
3
+ import { collectionFromRecordUri } from "./spaces/uri.js";
4
+ import { spaceFilterSql } from "./spaces/visibility.js";
5
+ import { guardedQuerySQL, unfilteredQuerySQL } from "./spaces/guard.js";
6
+ import { records, requireSpace, spaceRecords } from "./spaces/records.js";
7
+ import { spaceBlobUrl } from "./spaces/blob.js";
3
8
  // --- Record Resolution ---
4
9
  /** Fetch records for URIs, reshape them, and filter out taken-down DIDs. */
5
10
  export async function resolveRecords(uris) {
6
11
  if (uris.length === 0)
7
12
  return [];
8
- // Group URIs by collection for batch fetching
13
+ // Group URIs by collection for batch fetching. A space record's collection
14
+ // sits in a different segment than a repo record's, so the position is read
15
+ // by a parser that knows both shapes rather than inline.
9
16
  const byCollection = new Map();
10
17
  for (const uri of uris) {
11
- const parts = uri.replace('at://', '').split('/');
12
- const col = parts[1];
18
+ const col = collectionFromRecordUri(uri);
19
+ if (!col)
20
+ continue;
13
21
  if (!byCollection.has(col))
14
22
  byCollection.set(col, []);
15
23
  byCollection.get(col).push(uri);
@@ -43,7 +51,7 @@ export async function resolveRecords(uris) {
43
51
  export function buildBaseContext(viewer) {
44
52
  return {
45
53
  viewer,
46
- db: { query: querySQL },
54
+ db: { query: guardedQuerySQL, unfiltered: unfilteredQuerySQL },
47
55
  getRecords: getRecordsMap,
48
56
  lookup: async (collection, field, values) => {
49
57
  if (values.length === 0)
@@ -59,5 +67,10 @@ export function buildBaseContext(viewer) {
59
67
  },
60
68
  labels: queryLabelsForUris,
61
69
  blobUrl,
70
+ spaceFilter: spaceFilterSql,
71
+ spaceRecords,
72
+ records,
73
+ requireSpace,
74
+ spaceBlobUrl,
62
75
  };
63
76
  }