@freqhole/haruspex 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/dist/flows/add-peer/machine.d.ts +36 -0
  2. package/dist/flows/add-peer/machine.d.ts.map +1 -0
  3. package/dist/flows/add-peer/machine.js +500 -0
  4. package/dist/flows/add-peer/machine.test.d.ts +2 -0
  5. package/dist/flows/add-peer/machine.test.d.ts.map +1 -0
  6. package/dist/flows/add-peer/machine.test.js +421 -0
  7. package/dist/flows/add-peer/runner.d.ts +16 -0
  8. package/dist/flows/add-peer/runner.d.ts.map +1 -0
  9. package/dist/flows/add-peer/runner.js +349 -0
  10. package/dist/flows/add-peer/runner.test.d.ts +2 -0
  11. package/dist/flows/add-peer/runner.test.d.ts.map +1 -0
  12. package/dist/flows/add-peer/runner.test.js +205 -0
  13. package/dist/flows/add-peer/types.d.ts +271 -0
  14. package/dist/flows/add-peer/types.d.ts.map +1 -0
  15. package/dist/flows/add-peer/types.js +10 -0
  16. package/dist/flows/index.d.ts +6 -0
  17. package/dist/flows/index.d.ts.map +1 -0
  18. package/dist/flows/index.js +6 -0
  19. package/dist/identity/idbStore.d.ts +51 -0
  20. package/dist/identity/idbStore.d.ts.map +1 -0
  21. package/dist/identity/idbStore.js +183 -0
  22. package/dist/identity/idbStore.test.d.ts +2 -0
  23. package/dist/identity/idbStore.test.d.ts.map +1 -0
  24. package/dist/identity/idbStore.test.js +103 -0
  25. package/dist/identity/index.d.ts +8 -0
  26. package/dist/identity/index.d.ts.map +1 -0
  27. package/dist/identity/index.js +3 -0
  28. package/dist/identity/resolve.d.ts +31 -0
  29. package/dist/identity/resolve.d.ts.map +1 -0
  30. package/dist/identity/resolve.js +37 -0
  31. package/dist/identity/resolve.test.d.ts +2 -0
  32. package/dist/identity/resolve.test.d.ts.map +1 -0
  33. package/dist/identity/resolve.test.js +105 -0
  34. package/dist/identity/types.d.ts +21 -0
  35. package/dist/identity/types.d.ts.map +1 -0
  36. package/dist/identity/types.js +1 -0
  37. package/dist/identity/webLocks.d.ts +22 -0
  38. package/dist/identity/webLocks.d.ts.map +1 -0
  39. package/dist/identity/webLocks.js +67 -0
  40. package/dist/identity/webLocks.test.d.ts +2 -0
  41. package/dist/identity/webLocks.test.d.ts.map +1 -0
  42. package/dist/identity/webLocks.test.js +125 -0
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +1 -0
  46. package/dist/index.test.d.ts +2 -0
  47. package/dist/index.test.d.ts.map +1 -0
  48. package/dist/index.test.js +7 -0
  49. package/dist/knock/index.d.ts +10 -0
  50. package/dist/knock/index.d.ts.map +1 -0
  51. package/dist/knock/index.js +5 -0
  52. package/dist/knock/pending-checker.d.ts +19 -0
  53. package/dist/knock/pending-checker.d.ts.map +1 -0
  54. package/dist/knock/pending-checker.js +27 -0
  55. package/dist/knock/pending-checker.test.d.ts +2 -0
  56. package/dist/knock/pending-checker.test.d.ts.map +1 -0
  57. package/dist/knock/pending-checker.test.js +93 -0
  58. package/dist/knock/requester.d.ts +26 -0
  59. package/dist/knock/requester.d.ts.map +1 -0
  60. package/dist/knock/requester.js +72 -0
  61. package/dist/knock/requester.test.d.ts +2 -0
  62. package/dist/knock/requester.test.d.ts.map +1 -0
  63. package/dist/knock/requester.test.js +108 -0
  64. package/dist/knock/responder.d.ts +13 -0
  65. package/dist/knock/responder.d.ts.map +1 -0
  66. package/dist/knock/responder.js +40 -0
  67. package/dist/knock/responder.test.d.ts +2 -0
  68. package/dist/knock/responder.test.d.ts.map +1 -0
  69. package/dist/knock/responder.test.js +89 -0
  70. package/dist/knock/store.d.ts +11 -0
  71. package/dist/knock/store.d.ts.map +1 -0
  72. package/dist/knock/store.js +211 -0
  73. package/dist/knock/store.test.d.ts +2 -0
  74. package/dist/knock/store.test.d.ts.map +1 -0
  75. package/dist/knock/store.test.js +195 -0
  76. package/dist/knock/types.d.ts +144 -0
  77. package/dist/knock/types.d.ts.map +1 -0
  78. package/dist/knock/types.js +26 -0
  79. package/dist/permissions/access.d.ts +54 -0
  80. package/dist/permissions/access.d.ts.map +1 -0
  81. package/dist/permissions/access.js +56 -0
  82. package/dist/permissions/access.test.d.ts +2 -0
  83. package/dist/permissions/access.test.d.ts.map +1 -0
  84. package/dist/permissions/access.test.js +72 -0
  85. package/dist/permissions/index.d.ts +5 -0
  86. package/dist/permissions/index.d.ts.map +1 -0
  87. package/dist/permissions/index.js +2 -0
  88. package/dist/permissions/role-table.d.ts +11 -0
  89. package/dist/permissions/role-table.d.ts.map +1 -0
  90. package/dist/permissions/role-table.js +16 -0
  91. package/dist/protocol/client.d.ts +67 -0
  92. package/dist/protocol/client.d.ts.map +1 -0
  93. package/dist/protocol/client.js +195 -0
  94. package/dist/protocol/client.test.d.ts +2 -0
  95. package/dist/protocol/client.test.d.ts.map +1 -0
  96. package/dist/protocol/client.test.js +282 -0
  97. package/dist/protocol/codec.d.ts +45 -0
  98. package/dist/protocol/codec.d.ts.map +1 -0
  99. package/dist/protocol/codec.js +111 -0
  100. package/dist/protocol/codec.test.d.ts +2 -0
  101. package/dist/protocol/codec.test.d.ts.map +1 -0
  102. package/dist/protocol/codec.test.js +93 -0
  103. package/dist/protocol/extensions.d.ts +33 -0
  104. package/dist/protocol/extensions.d.ts.map +1 -0
  105. package/dist/protocol/extensions.js +36 -0
  106. package/dist/protocol/extensions.test.d.ts +2 -0
  107. package/dist/protocol/extensions.test.d.ts.map +1 -0
  108. package/dist/protocol/extensions.test.js +65 -0
  109. package/dist/protocol/fixtures.test.d.ts +2 -0
  110. package/dist/protocol/fixtures.test.d.ts.map +1 -0
  111. package/dist/protocol/fixtures.test.js +51 -0
  112. package/dist/protocol/index.d.ts +9 -0
  113. package/dist/protocol/index.d.ts.map +1 -0
  114. package/dist/protocol/index.js +4 -0
  115. package/dist/protocol/messages.d.ts +480 -0
  116. package/dist/protocol/messages.d.ts.map +1 -0
  117. package/dist/protocol/messages.js +255 -0
  118. package/dist/protocol/messages.test.d.ts +2 -0
  119. package/dist/protocol/messages.test.d.ts.map +1 -0
  120. package/dist/protocol/messages.test.js +75 -0
  121. package/dist/share/codec.d.ts +58 -0
  122. package/dist/share/codec.d.ts.map +1 -0
  123. package/dist/share/codec.js +222 -0
  124. package/dist/share/codec.test.d.ts +2 -0
  125. package/dist/share/codec.test.d.ts.map +1 -0
  126. package/dist/share/codec.test.js +150 -0
  127. package/dist/share/detect.d.ts +24 -0
  128. package/dist/share/detect.d.ts.map +1 -0
  129. package/dist/share/detect.js +25 -0
  130. package/dist/share/detect.test.d.ts +2 -0
  131. package/dist/share/detect.test.d.ts.map +1 -0
  132. package/dist/share/detect.test.js +31 -0
  133. package/dist/share/index.d.ts +7 -0
  134. package/dist/share/index.d.ts.map +1 -0
  135. package/dist/share/index.js +3 -0
  136. package/dist/share/peer-addr.d.ts +32 -0
  137. package/dist/share/peer-addr.d.ts.map +1 -0
  138. package/dist/share/peer-addr.js +72 -0
  139. package/dist/share/peer-addr.test.d.ts +2 -0
  140. package/dist/share/peer-addr.test.d.ts.map +1 -0
  141. package/dist/share/peer-addr.test.js +58 -0
  142. package/dist/solid/auth-form.d.ts +43 -0
  143. package/dist/solid/auth-form.d.ts.map +1 -0
  144. package/dist/solid/auth-form.js +55 -0
  145. package/dist/solid/auth-form.test.d.ts +2 -0
  146. package/dist/solid/auth-form.test.d.ts.map +1 -0
  147. package/dist/solid/auth-form.test.js +131 -0
  148. package/dist/solid/auth-status.d.ts +21 -0
  149. package/dist/solid/auth-status.d.ts.map +1 -0
  150. package/dist/solid/auth-status.js +58 -0
  151. package/dist/solid/auth-status.test.d.ts +2 -0
  152. package/dist/solid/auth-status.test.d.ts.map +1 -0
  153. package/dist/solid/auth-status.test.js +60 -0
  154. package/dist/solid/index.d.ts +5 -0
  155. package/dist/solid/index.d.ts.map +1 -0
  156. package/dist/solid/index.js +10 -0
  157. package/dist/solid/knock-inbox.d.ts +61 -0
  158. package/dist/solid/knock-inbox.d.ts.map +1 -0
  159. package/dist/solid/knock-inbox.js +123 -0
  160. package/dist/solid/knock-inbox.test.d.ts +2 -0
  161. package/dist/solid/knock-inbox.test.d.ts.map +1 -0
  162. package/dist/solid/knock-inbox.test.js +153 -0
  163. package/dist/solid/reauth-flow.d.ts +26 -0
  164. package/dist/solid/reauth-flow.d.ts.map +1 -0
  165. package/dist/solid/reauth-flow.js +34 -0
  166. package/dist/solid/reauth-flow.test.d.ts +2 -0
  167. package/dist/solid/reauth-flow.test.d.ts.map +1 -0
  168. package/dist/solid/reauth-flow.test.js +91 -0
  169. package/dist/state/endpoint-control.d.ts +22 -0
  170. package/dist/state/endpoint-control.d.ts.map +1 -0
  171. package/dist/state/endpoint-control.js +34 -0
  172. package/dist/state/endpoint-control.test.d.ts +2 -0
  173. package/dist/state/endpoint-control.test.d.ts.map +1 -0
  174. package/dist/state/endpoint-control.test.js +63 -0
  175. package/dist/state/friend-directory.d.ts +37 -0
  176. package/dist/state/friend-directory.d.ts.map +1 -0
  177. package/dist/state/friend-directory.js +44 -0
  178. package/dist/state/friend-directory.test.d.ts +2 -0
  179. package/dist/state/friend-directory.test.d.ts.map +1 -0
  180. package/dist/state/friend-directory.test.js +63 -0
  181. package/dist/state/index.d.ts +6 -0
  182. package/dist/state/index.d.ts.map +1 -0
  183. package/dist/state/index.js +3 -0
  184. package/dist/state/peer-names.d.ts +7 -0
  185. package/dist/state/peer-names.d.ts.map +1 -0
  186. package/dist/state/peer-names.js +21 -0
  187. package/dist/state/peer-names.test.d.ts +2 -0
  188. package/dist/state/peer-names.test.d.ts.map +1 -0
  189. package/dist/state/peer-names.test.js +29 -0
  190. package/dist/testing/idb-harness.d.ts +9 -0
  191. package/dist/testing/idb-harness.d.ts.map +1 -0
  192. package/dist/testing/idb-harness.js +19 -0
  193. package/dist/testing/idb-harness.test.d.ts +2 -0
  194. package/dist/testing/idb-harness.test.d.ts.map +1 -0
  195. package/dist/testing/idb-harness.test.js +18 -0
  196. package/dist/testing/identity-fixtures.d.ts +10 -0
  197. package/dist/testing/identity-fixtures.d.ts.map +1 -0
  198. package/dist/testing/identity-fixtures.js +21 -0
  199. package/dist/testing/identity-fixtures.test.d.ts +2 -0
  200. package/dist/testing/identity-fixtures.test.d.ts.map +1 -0
  201. package/dist/testing/identity-fixtures.test.js +28 -0
  202. package/dist/testing/index.d.ts +7 -0
  203. package/dist/testing/index.d.ts.map +1 -0
  204. package/dist/testing/index.js +11 -0
  205. package/dist/testing/knock-fixtures.d.ts +13 -0
  206. package/dist/testing/knock-fixtures.d.ts.map +1 -0
  207. package/dist/testing/knock-fixtures.js +45 -0
  208. package/dist/testing/knock-fixtures.test.d.ts +2 -0
  209. package/dist/testing/knock-fixtures.test.d.ts.map +1 -0
  210. package/dist/testing/knock-fixtures.test.js +65 -0
  211. package/dist/testing/protocol-fixtures.d.ts +7 -0
  212. package/dist/testing/protocol-fixtures.d.ts.map +1 -0
  213. package/dist/testing/protocol-fixtures.js +28 -0
  214. package/dist/testing/protocol-fixtures.test.d.ts +2 -0
  215. package/dist/testing/protocol-fixtures.test.d.ts.map +1 -0
  216. package/dist/testing/protocol-fixtures.test.js +19 -0
  217. package/dist/testing/state-fixtures.d.ts +33 -0
  218. package/dist/testing/state-fixtures.d.ts.map +1 -0
  219. package/dist/testing/state-fixtures.js +67 -0
  220. package/dist/testing/state-fixtures.test.d.ts +2 -0
  221. package/dist/testing/state-fixtures.test.d.ts.map +1 -0
  222. package/dist/testing/state-fixtures.test.js +68 -0
  223. package/dist/testing/webauthn-mocks.d.ts +27 -0
  224. package/dist/testing/webauthn-mocks.d.ts.map +1 -0
  225. package/dist/testing/webauthn-mocks.js +78 -0
  226. package/dist/testing/webauthn-mocks.test.d.ts +2 -0
  227. package/dist/testing/webauthn-mocks.test.d.ts.map +1 -0
  228. package/dist/testing/webauthn-mocks.test.js +40 -0
  229. package/dist/webauthn/ceremony.d.ts +76 -0
  230. package/dist/webauthn/ceremony.d.ts.map +1 -0
  231. package/dist/webauthn/ceremony.js +136 -0
  232. package/dist/webauthn/ceremony.test.d.ts +2 -0
  233. package/dist/webauthn/ceremony.test.d.ts.map +1 -0
  234. package/dist/webauthn/ceremony.test.js +245 -0
  235. package/dist/webauthn/codec.d.ts +39 -0
  236. package/dist/webauthn/codec.d.ts.map +1 -0
  237. package/dist/webauthn/codec.js +118 -0
  238. package/dist/webauthn/codec.test.d.ts +2 -0
  239. package/dist/webauthn/codec.test.d.ts.map +1 -0
  240. package/dist/webauthn/codec.test.js +126 -0
  241. package/dist/webauthn/index.d.ts +7 -0
  242. package/dist/webauthn/index.d.ts.map +1 -0
  243. package/dist/webauthn/index.js +3 -0
  244. package/dist/webauthn/transport.d.ts +58 -0
  245. package/dist/webauthn/transport.d.ts.map +1 -0
  246. package/dist/webauthn/transport.js +6 -0
  247. package/dist/webauthn/types.d.ts +78 -0
  248. package/dist/webauthn/types.d.ts.map +1 -0
  249. package/dist/webauthn/types.js +29 -0
  250. package/package.json +83 -0
@@ -0,0 +1,211 @@
1
+ // idb-backed KnockStore.
2
+ //
3
+ // database + store names are parameterized (see KnockStoreOptions) rather
4
+ // than hardcoded, so an app's existing knock table names can be pointed at
5
+ // this store in place, without a browser-data migration - the same
6
+ // `dbName` option pattern reliquary-ts's ./blobs subpath uses.
7
+ import { KnockConflictError } from "./types.js";
8
+ const NODE_ID_INDEX = "nodeId";
9
+ const DEDUP_INDEX = "dedup";
10
+ /**
11
+ * canonical rendering of a scope, used as the dedup index key. mirrors
12
+ * rust's dedup key exactly: it is keyed on the full scope value, so e.g.
13
+ * two account-scope knocks for different `requestedUsername`s do not dedup
14
+ * against each other.
15
+ */
16
+ function scopeKey(scope) {
17
+ switch (scope.kind) {
18
+ case "account":
19
+ return `account:${scope.requestedUsername ?? ""}`;
20
+ case "browse":
21
+ return "browse";
22
+ case "resource":
23
+ return `resource:${scope.resourceId}:${scope.requestedRole ?? ""}`;
24
+ }
25
+ }
26
+ function toRow(record) {
27
+ return { ...record, scopeKey: scopeKey(record.scope) };
28
+ }
29
+ function fromRow(row) {
30
+ const { scopeKey: _scopeKey, ...record } = row;
31
+ return record;
32
+ }
33
+ function reqPromise(req) {
34
+ return new Promise((resolve, reject) => {
35
+ req.onsuccess = () => resolve(req.result);
36
+ req.onerror = () => reject(req.error);
37
+ });
38
+ }
39
+ /**
40
+ * open the database, creating the store (and its indexes) on demand if
41
+ * this is the first time this database/store pair has been opened.
42
+ */
43
+ function openDb(databaseName, storeName) {
44
+ return new Promise((resolve, reject) => {
45
+ const probe = indexedDB.open(databaseName);
46
+ probe.onsuccess = () => {
47
+ const db = probe.result;
48
+ if (db.objectStoreNames.contains(storeName)) {
49
+ resolve(db);
50
+ return;
51
+ }
52
+ const nextVersion = db.version + 1;
53
+ db.close();
54
+ const upgrade = indexedDB.open(databaseName, nextVersion);
55
+ upgrade.onupgradeneeded = () => {
56
+ const upgradeDb = upgrade.result;
57
+ if (!upgradeDb.objectStoreNames.contains(storeName)) {
58
+ const store = upgradeDb.createObjectStore(storeName, { keyPath: "id" });
59
+ store.createIndex(NODE_ID_INDEX, "nodeId", { unique: false });
60
+ store.createIndex(DEDUP_INDEX, ["nodeId", "scopeKey", "status"], {
61
+ unique: false,
62
+ });
63
+ }
64
+ };
65
+ upgrade.onsuccess = () => resolve(upgrade.result);
66
+ upgrade.onerror = () => reject(upgrade.error);
67
+ };
68
+ probe.onerror = () => reject(probe.error);
69
+ });
70
+ }
71
+ /**
72
+ * an idb-backed KnockStore. database/store names are parameterized (see
73
+ * KnockStoreOptions) so an app can point this at its own existing tables.
74
+ */
75
+ export function createIdbKnockStore(options) {
76
+ const { databaseName, storeName } = options;
77
+ return {
78
+ async createKnock(input) {
79
+ const db = await openDb(databaseName, storeName);
80
+ try {
81
+ const key = scopeKey(input.scope);
82
+ return await new Promise((resolve, reject) => {
83
+ const tx = db.transaction(storeName, "readwrite");
84
+ const store = tx.objectStore(storeName);
85
+ const dedupIndex = store.index(DEDUP_INDEX);
86
+ const range = IDBKeyRange.only([input.nodeId, key, "pending"]);
87
+ const checkReq = dedupIndex.getAllKeys(range);
88
+ checkReq.onerror = () => reject(checkReq.error);
89
+ checkReq.onsuccess = () => {
90
+ if (checkReq.result.length > 0) {
91
+ reject(new KnockConflictError(input.nodeId, input.scope));
92
+ tx.abort();
93
+ return;
94
+ }
95
+ const record = {
96
+ id: crypto.randomUUID(),
97
+ nodeId: input.nodeId,
98
+ direction: input.direction,
99
+ scope: input.scope,
100
+ message: input.message ?? "",
101
+ status: "pending",
102
+ createdAt: input.createdAt ?? Date.now(),
103
+ decisions: [],
104
+ ...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
105
+ };
106
+ const putReq = store.put(toRow(record));
107
+ putReq.onerror = () => reject(putReq.error);
108
+ putReq.onsuccess = () => resolve(record);
109
+ };
110
+ });
111
+ }
112
+ finally {
113
+ db.close();
114
+ }
115
+ },
116
+ async getKnock(id) {
117
+ const db = await openDb(databaseName, storeName);
118
+ try {
119
+ const tx = db.transaction(storeName, "readonly");
120
+ const row = (await reqPromise(tx.objectStore(storeName).get(id)));
121
+ return row ? fromRow(row) : null;
122
+ }
123
+ finally {
124
+ db.close();
125
+ }
126
+ },
127
+ async listPending() {
128
+ const db = await openDb(databaseName, storeName);
129
+ try {
130
+ const tx = db.transaction(storeName, "readonly");
131
+ const rows = (await reqPromise(tx.objectStore(storeName).getAll()));
132
+ return rows
133
+ .filter((r) => r.status === "pending")
134
+ .map(fromRow)
135
+ .sort((a, b) => b.createdAt - a.createdAt);
136
+ }
137
+ finally {
138
+ db.close();
139
+ }
140
+ },
141
+ async listAll() {
142
+ const db = await openDb(databaseName, storeName);
143
+ try {
144
+ const tx = db.transaction(storeName, "readonly");
145
+ const rows = (await reqPromise(tx.objectStore(storeName).getAll()));
146
+ return rows.map(fromRow).sort((a, b) => b.createdAt - a.createdAt);
147
+ }
148
+ finally {
149
+ db.close();
150
+ }
151
+ },
152
+ async findByNodeId(nodeId) {
153
+ const db = await openDb(databaseName, storeName);
154
+ try {
155
+ const tx = db.transaction(storeName, "readonly");
156
+ const rows = (await reqPromise(tx.objectStore(storeName).index(NODE_ID_INDEX).getAll(nodeId)));
157
+ if (rows.length === 0)
158
+ return null;
159
+ rows.sort((a, b) => b.createdAt - a.createdAt);
160
+ return fromRow(rows[0]);
161
+ }
162
+ finally {
163
+ db.close();
164
+ }
165
+ },
166
+ async recordDecision(id, decision, patch) {
167
+ const db = await openDb(databaseName, storeName);
168
+ try {
169
+ return await new Promise((resolve, reject) => {
170
+ const tx = db.transaction(storeName, "readwrite");
171
+ const store = tx.objectStore(storeName);
172
+ const getReq = store.get(id);
173
+ getReq.onerror = () => reject(getReq.error);
174
+ getReq.onsuccess = () => {
175
+ const row = getReq.result;
176
+ if (!row) {
177
+ reject(new Error(`knock ${id} not found`));
178
+ tx.abort();
179
+ return;
180
+ }
181
+ const record = fromRow(row);
182
+ const updated = {
183
+ ...record,
184
+ status: decision.outcome,
185
+ processedAt: decision.at,
186
+ processedBy: decision.byNodeId,
187
+ decisions: [...record.decisions, decision],
188
+ grantedResourceIds: patch?.grantedResourceIds ?? record.grantedResourceIds,
189
+ };
190
+ const putReq = store.put(toRow(updated));
191
+ putReq.onerror = () => reject(putReq.error);
192
+ putReq.onsuccess = () => resolve(updated);
193
+ };
194
+ });
195
+ }
196
+ finally {
197
+ db.close();
198
+ }
199
+ },
200
+ async deleteKnock(id) {
201
+ const db = await openDb(databaseName, storeName);
202
+ try {
203
+ const tx = db.transaction(storeName, "readwrite");
204
+ await reqPromise(tx.objectStore(storeName).delete(id));
205
+ }
206
+ finally {
207
+ db.close();
208
+ }
209
+ },
210
+ };
211
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=store.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.test.d.ts","sourceRoot":"","sources":["../../src/knock/store.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,195 @@
1
+ import { IDBFactory, IDBKeyRange } from "fake-indexeddb";
2
+ import { beforeEach, describe, expect, it } from "vitest";
3
+ import { createIdbKnockStore } from "./store.js";
4
+ import { KnockConflictError } from "./types.js";
5
+ beforeEach(() => {
6
+ globalThis.indexedDB = new IDBFactory();
7
+ globalThis.IDBKeyRange = IDBKeyRange;
8
+ });
9
+ const NODE_ID = "ab".repeat(32);
10
+ const OTHER_NODE_ID = "cd".repeat(32);
11
+ const browseScope = { kind: "browse" };
12
+ describe("createIdbKnockStore", () => {
13
+ it("creates and round-trips a pending knock", async () => {
14
+ const store = createIdbKnockStore({ databaseName: "app-db", storeName: "knocks" });
15
+ const record = await store.createKnock({
16
+ nodeId: NODE_ID,
17
+ direction: "outbound",
18
+ scope: browseScope,
19
+ message: "let me in",
20
+ });
21
+ expect(record.status).toBe("pending");
22
+ expect(record.decisions).toEqual([]);
23
+ expect(await store.getKnock(record.id)).toEqual(record);
24
+ });
25
+ it("has no metadata field when none is supplied - non-breaking for existing callers", async () => {
26
+ const store = createIdbKnockStore({ databaseName: "app-db", storeName: "knocks" });
27
+ const record = await store.createKnock({
28
+ nodeId: NODE_ID,
29
+ direction: "outbound",
30
+ scope: browseScope,
31
+ message: "let me in",
32
+ });
33
+ expect(record.metadata).toBeUndefined();
34
+ expect(await store.getKnock(record.id)).toEqual(record);
35
+ });
36
+ it("round-trips an app-populated metadata bag", async () => {
37
+ const store = createIdbKnockStore({ databaseName: "app-db", storeName: "knocks" });
38
+ const metadata = { name: "alice", avatarColor: "#ff00ff" };
39
+ const record = await store.createKnock({
40
+ nodeId: NODE_ID,
41
+ direction: "inbound",
42
+ scope: browseScope,
43
+ message: "let me in",
44
+ metadata,
45
+ });
46
+ expect(record.metadata).toEqual(metadata);
47
+ expect((await store.getKnock(record.id))?.metadata).toEqual(metadata);
48
+ });
49
+ it("defaults message to an empty string and createdAt to now", async () => {
50
+ const store = createIdbKnockStore({ databaseName: "app-db", storeName: "knocks" });
51
+ const before = Date.now();
52
+ const record = await store.createKnock({
53
+ nodeId: NODE_ID,
54
+ direction: "inbound",
55
+ scope: browseScope,
56
+ });
57
+ expect(record.message).toBe("");
58
+ expect(record.createdAt).toBeGreaterThanOrEqual(before);
59
+ });
60
+ it("rejects a second pending knock for the same node id + scope", async () => {
61
+ const store = createIdbKnockStore({ databaseName: "app-db", storeName: "knocks" });
62
+ await store.createKnock({ nodeId: NODE_ID, direction: "outbound", scope: browseScope });
63
+ await expect(store.createKnock({ nodeId: NODE_ID, direction: "outbound", scope: browseScope })).rejects.toThrow(KnockConflictError);
64
+ });
65
+ it("allows a second pending knock for a different scope on the same node id", async () => {
66
+ const store = createIdbKnockStore({ databaseName: "app-db", storeName: "knocks" });
67
+ await store.createKnock({ nodeId: NODE_ID, direction: "outbound", scope: browseScope });
68
+ const resourceKnock = await store.createKnock({
69
+ nodeId: NODE_ID,
70
+ direction: "outbound",
71
+ scope: { kind: "resource", resourceId: "doc-1" },
72
+ });
73
+ expect(resourceKnock.status).toBe("pending");
74
+ });
75
+ it("allows a new knock once the prior one is no longer pending", async () => {
76
+ const store = createIdbKnockStore({ databaseName: "app-db", storeName: "knocks" });
77
+ const first = await store.createKnock({
78
+ nodeId: NODE_ID,
79
+ direction: "outbound",
80
+ scope: browseScope,
81
+ });
82
+ await store.recordDecision(first.id, { byNodeId: NODE_ID, outcome: "denied", at: Date.now() });
83
+ const second = await store.createKnock({
84
+ nodeId: NODE_ID,
85
+ direction: "outbound",
86
+ scope: browseScope,
87
+ });
88
+ expect(second.status).toBe("pending");
89
+ expect(second.id).not.toBe(first.id);
90
+ });
91
+ it("allows a new knock once the prior one is deleted", async () => {
92
+ const store = createIdbKnockStore({ databaseName: "app-db", storeName: "knocks" });
93
+ const first = await store.createKnock({
94
+ nodeId: NODE_ID,
95
+ direction: "outbound",
96
+ scope: browseScope,
97
+ });
98
+ await store.deleteKnock(first.id);
99
+ const second = await store.createKnock({
100
+ nodeId: NODE_ID,
101
+ direction: "outbound",
102
+ scope: browseScope,
103
+ });
104
+ expect(second.status).toBe("pending");
105
+ expect(await store.getKnock(first.id)).toBeNull();
106
+ });
107
+ it("listPending returns only pending records, newest first", async () => {
108
+ const store = createIdbKnockStore({ databaseName: "app-db", storeName: "knocks" });
109
+ const a = await store.createKnock({
110
+ nodeId: NODE_ID,
111
+ direction: "outbound",
112
+ scope: browseScope,
113
+ createdAt: 1000,
114
+ });
115
+ const b = await store.createKnock({
116
+ nodeId: OTHER_NODE_ID,
117
+ direction: "outbound",
118
+ scope: browseScope,
119
+ createdAt: 2000,
120
+ });
121
+ await store.recordDecision(a.id, { byNodeId: NODE_ID, outcome: "accepted", at: 3000 });
122
+ const pending = await store.listPending();
123
+ expect(pending.map((r) => r.id)).toEqual([b.id]);
124
+ });
125
+ it("listAll returns every record, newest first", async () => {
126
+ const store = createIdbKnockStore({ databaseName: "app-db", storeName: "knocks" });
127
+ const a = await store.createKnock({
128
+ nodeId: NODE_ID,
129
+ direction: "outbound",
130
+ scope: browseScope,
131
+ createdAt: 1000,
132
+ });
133
+ const b = await store.createKnock({
134
+ nodeId: OTHER_NODE_ID,
135
+ direction: "outbound",
136
+ scope: browseScope,
137
+ createdAt: 2000,
138
+ });
139
+ expect((await store.listAll()).map((r) => r.id)).toEqual([b.id, a.id]);
140
+ });
141
+ it("findByNodeId returns the most recent record for that node id", async () => {
142
+ const store = createIdbKnockStore({ databaseName: "app-db", storeName: "knocks" });
143
+ const first = await store.createKnock({
144
+ nodeId: NODE_ID,
145
+ direction: "outbound",
146
+ scope: browseScope,
147
+ createdAt: 1000,
148
+ });
149
+ await store.recordDecision(first.id, { byNodeId: NODE_ID, outcome: "denied", at: 1500 });
150
+ const second = await store.createKnock({
151
+ nodeId: NODE_ID,
152
+ direction: "outbound",
153
+ scope: browseScope,
154
+ createdAt: 2000,
155
+ });
156
+ const found = await store.findByNodeId(NODE_ID);
157
+ expect(found?.id).toBe(second.id);
158
+ });
159
+ it("findByNodeId returns null for an unknown node id", async () => {
160
+ const store = createIdbKnockStore({ databaseName: "app-db", storeName: "knocks" });
161
+ expect(await store.findByNodeId(NODE_ID)).toBeNull();
162
+ });
163
+ it("recordDecision appends to the audit log and resolves status", async () => {
164
+ const store = createIdbKnockStore({ databaseName: "app-db", storeName: "knocks" });
165
+ const record = await store.createKnock({
166
+ nodeId: NODE_ID,
167
+ direction: "inbound",
168
+ scope: browseScope,
169
+ });
170
+ const updated = await store.recordDecision(record.id, { byNodeId: OTHER_NODE_ID, outcome: "accepted", grantedRole: "member", at: 5000 }, { grantedResourceIds: ["doc-1", "doc-2"] });
171
+ expect(updated.status).toBe("accepted");
172
+ expect(updated.processedAt).toBe(5000);
173
+ expect(updated.processedBy).toBe(OTHER_NODE_ID);
174
+ expect(updated.decisions).toEqual([
175
+ { byNodeId: OTHER_NODE_ID, outcome: "accepted", grantedRole: "member", at: 5000 },
176
+ ]);
177
+ expect(updated.grantedResourceIds).toEqual(["doc-1", "doc-2"]);
178
+ });
179
+ it("recordDecision throws for an unknown id", async () => {
180
+ const store = createIdbKnockStore({ databaseName: "app-db", storeName: "knocks" });
181
+ await expect(store.recordDecision("nope", { byNodeId: NODE_ID, outcome: "denied", at: 0 })).rejects.toThrow();
182
+ });
183
+ it("uses parameterized database and store names", async () => {
184
+ const store = createIdbKnockStore({
185
+ databaseName: "playlistz-db",
186
+ storeName: "knocks",
187
+ });
188
+ await store.createKnock({ nodeId: NODE_ID, direction: "inbound", scope: browseScope });
189
+ const otherStore = createIdbKnockStore({
190
+ databaseName: "different-db",
191
+ storeName: "knocks",
192
+ });
193
+ expect(await otherStore.listAll()).toEqual([]);
194
+ });
195
+ });
@@ -0,0 +1,144 @@
1
+ /** who initiated the knock: a peer knocking on us, or us knocking on a peer. */
2
+ export type KnockDirection = "inbound" | "outbound";
3
+ /** current resolution of a knock request. */
4
+ export type KnockStatus = "pending" | "accepted" | "denied";
5
+ /**
6
+ * what access is being requested. mirrors rust's `KnockScope` enum
7
+ * field-for-field so a knock raised on one side of the wire describes the
8
+ * same request shape on the other:
9
+ * - account: acceptance creates or links a user identity
10
+ * - browse: list access, no specific resource named
11
+ * - resource: access to one named resource, with an optional requested role
12
+ */
13
+ export type KnockScope = {
14
+ kind: "account";
15
+ requestedUsername?: string;
16
+ } | {
17
+ kind: "browse";
18
+ } | {
19
+ kind: "resource";
20
+ resourceId: string;
21
+ requestedRole?: string;
22
+ };
23
+ /** one responder decision, appended to a knock's audit log. */
24
+ export interface KnockDecision {
25
+ /** node id of whoever made this decision (the responder). */
26
+ byNodeId: string;
27
+ outcome: Exclude<KnockStatus, "pending">;
28
+ grantedRole?: string;
29
+ /** unix epoch millis. */
30
+ at: number;
31
+ }
32
+ export interface KnockRecord {
33
+ id: string;
34
+ nodeId: string;
35
+ direction: KnockDirection;
36
+ scope: KnockScope;
37
+ message: string;
38
+ status: KnockStatus;
39
+ /** unix epoch millis. */
40
+ createdAt: number;
41
+ /** unix epoch millis of the most recent decision, if any. */
42
+ processedAt?: number;
43
+ processedBy?: string;
44
+ /** full decision audit trail - see KnockDecision. */
45
+ decisions: KnockDecision[];
46
+ /**
47
+ * resource ids granted alongside a decision, for the case where one
48
+ * knock can cover more than the single `resourceId` named in `scope`
49
+ * (e.g. a browse-scope knock accepted with access to several resources).
50
+ * rust's per-decision shape carries a single `grantedRole` and no id
51
+ * list - this field is a ts-side addition for that many-resource case,
52
+ * not a wire concept the two sides need to agree on byte-for-byte.
53
+ * settled: multi-resource grants are never carried on the wire decision
54
+ * message itself - they're delivered as grant-store writes plus the
55
+ * protocol's own `acl-change` message. this field is a local read-model
56
+ * convenience an app can populate from those grant writes, not
57
+ * something a transport implementation needs to fill in.
58
+ */
59
+ grantedResourceIds?: string[];
60
+ /**
61
+ * an opaque, app-populated json bag for data the wire message carries
62
+ * that this record has no dedicated field for (e.g. a sender's display
63
+ * name alongside their knock message). this package never reads or
64
+ * interprets its contents - it only persists and returns whatever the
65
+ * caller supplied at creation time.
66
+ */
67
+ metadata?: Record<string, unknown>;
68
+ }
69
+ /**
70
+ * thrown by `KnockStore.createKnock` when an active (pending) knock already
71
+ * exists for this node id + scope - the dedup rule haruspex's sqlite store
72
+ * enforces with a partial unique index over (node_id, scope) where
73
+ * status = 'pending'.
74
+ */
75
+ export declare class KnockConflictError extends Error {
76
+ readonly nodeId: string;
77
+ readonly scope: KnockScope;
78
+ constructor(nodeId: string, scope: KnockScope);
79
+ }
80
+ export interface CreateKnockInput {
81
+ nodeId: string;
82
+ direction: KnockDirection;
83
+ scope: KnockScope;
84
+ message?: string;
85
+ /** unix epoch millis; defaults to Date.now(). */
86
+ createdAt?: number;
87
+ /** stored and returned as-is, never interpreted - see KnockRecord.metadata. */
88
+ metadata?: Record<string, unknown>;
89
+ }
90
+ /**
91
+ * persistence for knock records. an idb-backed implementation is provided
92
+ * (see store.ts); apps may supply their own to match existing storage.
93
+ */
94
+ export interface KnockStore {
95
+ /** enforces the dedup rule: one active (pending) knock per node id +
96
+ * scope. throws KnockConflictError if one already exists. */
97
+ createKnock(input: CreateKnockInput): Promise<KnockRecord>;
98
+ getKnock(id: string): Promise<KnockRecord | null>;
99
+ /** all pending knock records, newest first. */
100
+ listPending(): Promise<KnockRecord[]>;
101
+ /** all knock records regardless of status, newest first. */
102
+ listAll(): Promise<KnockRecord[]>;
103
+ /** the most recent knock record for `nodeId`, regardless of status, or
104
+ * null if this node id has never knocked. */
105
+ findByNodeId(nodeId: string): Promise<KnockRecord | null>;
106
+ /** appends a decision and resolves the record's status
107
+ * (last-decision-wins, matching haruspex's sqlite store). */
108
+ recordDecision(id: string, decision: KnockDecision, patch?: {
109
+ grantedResourceIds?: string[];
110
+ }): Promise<KnockRecord>;
111
+ /** permanently deletes a knock record, freeing the node id + scope to
112
+ * knock again (the dedup rule only covers pending rows). */
113
+ deleteKnock(id: string): Promise<void>;
114
+ }
115
+ /** what a knock requester sends, and what the responder answers with. */
116
+ export interface KnockRequest {
117
+ scope: KnockScope;
118
+ message?: string;
119
+ requesterName?: string;
120
+ }
121
+ export interface KnockStatusReply {
122
+ status: KnockStatus;
123
+ grantedResourceIds?: string[];
124
+ }
125
+ /**
126
+ * the wire operations `sendKnock`/`checkKnockStatus` need - injected so
127
+ * this package never owns a transport. one call opens a knock, the other
128
+ * re-checks a knock already sent (a retry/poll path, never a fresh knock).
129
+ */
130
+ export interface KnockTransport {
131
+ sendKnock(nodeId: string, request: KnockRequest): Promise<KnockStatusReply>;
132
+ checkKnockStatus(nodeId: string, request: KnockRequest): Promise<KnockStatusReply>;
133
+ }
134
+ export interface KnockPolicyResult {
135
+ grantedRole?: string;
136
+ grantedResourceIds?: string[];
137
+ }
138
+ /**
139
+ * app-injected side effect for accepting a knock - creating a user,
140
+ * granting a role, whatever the accept actually does. this package only
141
+ * manages the knock record's lifecycle, never these side effects.
142
+ */
143
+ export type KnockPolicy = (record: KnockRecord) => Promise<KnockPolicyResult> | KnockPolicyResult;
144
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/knock/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ // framework-free knock (access-request) state, mirroring haruspex's rust
2
+ // KnockScope/KnockRecord shapes so both sides of a knock describe the same
3
+ // request.
4
+ //
5
+ // this module owns the record shape and the store/transport contracts;
6
+ // sending or checking a knock over any particular wire is the caller's
7
+ // transport implementation to provide, and the accept side effect
8
+ // (creating a user, granting a role, whatever accepting actually means for
9
+ // the app) is always an injected policy callback - this package only
10
+ // tracks the knock record's lifecycle, never those side effects.
11
+ /**
12
+ * thrown by `KnockStore.createKnock` when an active (pending) knock already
13
+ * exists for this node id + scope - the dedup rule haruspex's sqlite store
14
+ * enforces with a partial unique index over (node_id, scope) where
15
+ * status = 'pending'.
16
+ */
17
+ export class KnockConflictError extends Error {
18
+ nodeId;
19
+ scope;
20
+ constructor(nodeId, scope) {
21
+ super(`an active knock already exists for node ${nodeId} and this scope`);
22
+ this.nodeId = nodeId;
23
+ this.scope = scope;
24
+ this.name = "KnockConflictError";
25
+ }
26
+ }
@@ -0,0 +1,54 @@
1
+ import type { RoleHierarchy } from "./role-table.js";
2
+ /**
3
+ * does `actualRole` have at least the privilege of `requiredRole`? lower
4
+ * hierarchy numbers are more privileged, so this holds when `actualRole`'s
5
+ * level is less than or equal to `requiredRole`'s.
6
+ */
7
+ export declare function hasAtLeastRole<TRole extends string>(hierarchy: RoleHierarchy<TRole>, actualRole: TRole, requiredRole: TRole): boolean;
8
+ /** is `userId` strictly the resource's owner? */
9
+ export declare function isOwner(userId: string, ownerId: string | null): boolean;
10
+ /**
11
+ * is `userId` the resource's owner, or does `actualRole` have at least
12
+ * `requiredRole`'s privilege? owner always passes regardless of role.
13
+ */
14
+ export declare function canAccessOwnerOr<TRole extends string>(hierarchy: RoleHierarchy<TRole>, userId: string, ownerId: string | null, actualRole: TRole, requiredRole: TRole): boolean;
15
+ /**
16
+ * a route-auth-shaped access rule: public (always allowed), authenticated
17
+ * (any signed-in caller), role (a minimum role), owner (strictly the
18
+ * resource's owner), or owner_or (owner, or a minimum role).
19
+ */
20
+ export type AccessRule<TRole extends string> = {
21
+ type: "public";
22
+ } | {
23
+ type: "authenticated";
24
+ } | {
25
+ type: "role";
26
+ role: TRole;
27
+ } | {
28
+ type: "owner";
29
+ } | {
30
+ type: "owner_or";
31
+ role: TRole;
32
+ };
33
+ /** the caller context an `AccessRule` is evaluated against. */
34
+ export interface AccessContext<TRole extends string> {
35
+ userRole: TRole | null;
36
+ userId: string | null;
37
+ ownerId: string | null;
38
+ }
39
+ /** evaluate any `AccessRule` against a caller context. */
40
+ export declare function canAccess<TRole extends string>(hierarchy: RoleHierarchy<TRole>, rule: AccessRule<TRole>, ctx: AccessContext<TRole>): boolean;
41
+ /** the bundle of role-hierarchy-bound checks `bindRoleHierarchy` produces. */
42
+ export interface RoleChecks<TRole extends string> {
43
+ hasAtLeastRole: (actualRole: TRole, requiredRole: TRole) => boolean;
44
+ canAccessOwnerOr: (userId: string, ownerId: string | null, actualRole: TRole, requiredRole: TRole) => boolean;
45
+ isOwner: (userId: string, ownerId: string | null) => boolean;
46
+ canAccess: (rule: AccessRule<TRole>, ctx: AccessContext<TRole>) => boolean;
47
+ }
48
+ /**
49
+ * bind a role hierarchy once and get back plain functions that no longer
50
+ * need it threaded through every call - convenient for a caller that only
51
+ * ever checks against one hierarchy for its whole lifetime.
52
+ */
53
+ export declare function bindRoleHierarchy<TRole extends string>(hierarchy: RoleHierarchy<TRole>): RoleChecks<TRole>;
54
+ //# sourceMappingURL=access.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../../src/permissions/access.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,SAAS,MAAM,EACjD,SAAS,EAAE,aAAa,CAAC,KAAK,CAAC,EAC/B,UAAU,EAAE,KAAK,EACjB,YAAY,EAAE,KAAK,GAClB,OAAO,CAET;AAED,iDAAiD;AACjD,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAEvE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,SAAS,MAAM,EACnD,SAAS,EAAE,aAAa,CAAC,KAAK,CAAC,EAC/B,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,UAAU,EAAE,KAAK,EACjB,YAAY,EAAE,KAAK,GAClB,OAAO,CAGT;AAED;;;;GAIG;AACH,MAAM,MAAM,UAAU,CAAC,KAAK,SAAS,MAAM,IACvC;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,eAAe,CAAA;CAAE,GACzB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAE,GAC7B;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC;AAEtC,+DAA+D;AAC/D,MAAM,WAAW,aAAa,CAAC,KAAK,SAAS,MAAM;IACjD,QAAQ,EAAE,KAAK,GAAG,IAAI,CAAC;IACvB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,0DAA0D;AAC1D,wBAAgB,SAAS,CAAC,KAAK,SAAS,MAAM,EAC5C,SAAS,EAAE,aAAa,CAAC,KAAK,CAAC,EAC/B,IAAI,EAAE,UAAU,CAAC,KAAK,CAAC,EACvB,GAAG,EAAE,aAAa,CAAC,KAAK,CAAC,GACxB,OAAO,CAiBT;AAED,8EAA8E;AAC9E,MAAM,WAAW,UAAU,CAAC,KAAK,SAAS,MAAM;IAC9C,cAAc,EAAE,CAAC,UAAU,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,KAAK,OAAO,CAAC;IACpE,gBAAgB,EAAE,CAChB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,UAAU,EAAE,KAAK,EACjB,YAAY,EAAE,KAAK,KAChB,OAAO,CAAC;IACb,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,KAAK,OAAO,CAAC;IAC7D,SAAS,EAAE,CAAC,IAAI,EAAE,UAAU,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,aAAa,CAAC,KAAK,CAAC,KAAK,OAAO,CAAC;CAC5E;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,SAAS,MAAM,EACpD,SAAS,EAAE,aAAa,CAAC,KAAK,CAAC,GAC9B,UAAU,CAAC,KAAK,CAAC,CAQnB"}