@haverstack/server 0.8.0 → 0.9.1

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 (197) hide show
  1. package/dist/app.d.ts +8 -0
  2. package/dist/app.d.ts.map +1 -0
  3. package/dist/app.js +70 -0
  4. package/dist/app.js.map +1 -0
  5. package/dist/config.d.ts +21 -0
  6. package/dist/config.d.ts.map +1 -0
  7. package/dist/config.js +115 -0
  8. package/dist/config.js.map +1 -0
  9. package/dist/index.d.ts +16 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +12 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/lib/changeStreams.d.ts +17 -0
  14. package/dist/lib/changeStreams.d.ts.map +1 -0
  15. package/{src/lib/changeStreams.ts → dist/lib/changeStreams.js} +12 -12
  16. package/dist/lib/changeStreams.js.map +1 -0
  17. package/dist/lib/frameGate.d.ts +26 -0
  18. package/dist/lib/frameGate.d.ts.map +1 -0
  19. package/dist/lib/frameGate.js +44 -0
  20. package/dist/lib/frameGate.js.map +1 -0
  21. package/dist/lib/json.d.ts +10 -0
  22. package/dist/lib/json.d.ts.map +1 -0
  23. package/dist/lib/json.js +27 -0
  24. package/dist/lib/json.js.map +1 -0
  25. package/dist/lib/nonceStore.d.ts +50 -0
  26. package/dist/lib/nonceStore.d.ts.map +1 -0
  27. package/dist/lib/nonceStore.js +96 -0
  28. package/dist/lib/nonceStore.js.map +1 -0
  29. package/dist/lib/queryLimit.d.ts +4 -0
  30. package/dist/lib/queryLimit.d.ts.map +1 -0
  31. package/{src/lib/queryLimit.ts → dist/lib/queryLimit.js} +5 -6
  32. package/dist/lib/queryLimit.js.map +1 -0
  33. package/dist/lib/queryWorker/pool.d.ts +60 -0
  34. package/dist/lib/queryWorker/pool.d.ts.map +1 -0
  35. package/dist/lib/queryWorker/pool.js +278 -0
  36. package/dist/lib/queryWorker/pool.js.map +1 -0
  37. package/{src/lib/queryWorker/protocol.ts → dist/lib/queryWorker/protocol.d.ts} +21 -18
  38. package/dist/lib/queryWorker/protocol.d.ts.map +1 -0
  39. package/dist/lib/queryWorker/protocol.js +2 -0
  40. package/dist/lib/queryWorker/protocol.js.map +1 -0
  41. package/dist/lib/queryWorker/worker.d.ts +2 -0
  42. package/dist/lib/queryWorker/worker.d.ts.map +1 -0
  43. package/{src/lib/queryWorker/worker.ts → dist/lib/queryWorker/worker.js} +19 -22
  44. package/dist/lib/queryWorker/worker.js.map +1 -0
  45. package/dist/lib/resumeBuffer.d.ts +143 -0
  46. package/dist/lib/resumeBuffer.d.ts.map +1 -0
  47. package/dist/lib/resumeBuffer.js +216 -0
  48. package/dist/lib/resumeBuffer.js.map +1 -0
  49. package/dist/lib/resumeCursor.d.ts +16 -0
  50. package/dist/lib/resumeCursor.d.ts.map +1 -0
  51. package/{src/lib/resumeCursor.ts → dist/lib/resumeCursor.js} +22 -24
  52. package/dist/lib/resumeCursor.js.map +1 -0
  53. package/dist/lib/tokenSweep.d.ts +17 -0
  54. package/dist/lib/tokenSweep.d.ts.map +1 -0
  55. package/dist/lib/tokenSweep.js +40 -0
  56. package/dist/lib/tokenSweep.js.map +1 -0
  57. package/dist/main.d.ts +2 -0
  58. package/dist/main.d.ts.map +1 -0
  59. package/dist/main.js +46 -0
  60. package/dist/main.js.map +1 -0
  61. package/dist/middleware/auth.d.ts +25 -0
  62. package/dist/middleware/auth.d.ts.map +1 -0
  63. package/dist/middleware/auth.js +74 -0
  64. package/dist/middleware/auth.js.map +1 -0
  65. package/dist/middleware/errors.d.ts +17 -0
  66. package/dist/middleware/errors.d.ts.map +1 -0
  67. package/dist/middleware/errors.js +64 -0
  68. package/dist/middleware/errors.js.map +1 -0
  69. package/dist/routes/attachments.d.ts +5 -0
  70. package/dist/routes/attachments.d.ts.map +1 -0
  71. package/dist/routes/attachments.js +108 -0
  72. package/dist/routes/attachments.js.map +1 -0
  73. package/dist/routes/auth.d.ts +6 -0
  74. package/dist/routes/auth.d.ts.map +1 -0
  75. package/dist/routes/auth.js +110 -0
  76. package/dist/routes/auth.js.map +1 -0
  77. package/dist/routes/changes.d.ts +22 -0
  78. package/dist/routes/changes.d.ts.map +1 -0
  79. package/dist/routes/changes.js +233 -0
  80. package/dist/routes/changes.js.map +1 -0
  81. package/dist/routes/entity.d.ts +5 -0
  82. package/dist/routes/entity.d.ts.map +1 -0
  83. package/dist/routes/entity.js +30 -0
  84. package/dist/routes/entity.js.map +1 -0
  85. package/dist/routes/health.d.ts +4 -0
  86. package/dist/routes/health.d.ts.map +1 -0
  87. package/dist/routes/health.js +7 -0
  88. package/dist/routes/health.js.map +1 -0
  89. package/dist/routes/records.d.ts +5 -0
  90. package/dist/routes/records.d.ts.map +1 -0
  91. package/dist/routes/records.js +231 -0
  92. package/dist/routes/records.js.map +1 -0
  93. package/dist/routes/tokens.d.ts +5 -0
  94. package/dist/routes/tokens.d.ts.map +1 -0
  95. package/dist/routes/tokens.js +65 -0
  96. package/dist/routes/tokens.js.map +1 -0
  97. package/dist/routes/types.d.ts +5 -0
  98. package/dist/routes/types.d.ts.map +1 -0
  99. package/dist/routes/types.js +46 -0
  100. package/dist/routes/types.js.map +1 -0
  101. package/dist/routes/wellknown.d.ts +6 -0
  102. package/dist/routes/wellknown.d.ts.map +1 -0
  103. package/dist/routes/wellknown.js +45 -0
  104. package/dist/routes/wellknown.js.map +1 -0
  105. package/dist/shutdown.d.ts +23 -0
  106. package/dist/shutdown.d.ts.map +1 -0
  107. package/dist/shutdown.js +32 -0
  108. package/dist/shutdown.js.map +1 -0
  109. package/dist/stack.d.ts +20 -0
  110. package/dist/stack.d.ts.map +1 -0
  111. package/dist/stack.js +50 -0
  112. package/dist/stack.js.map +1 -0
  113. package/dist/testing.d.ts +49 -0
  114. package/dist/testing.d.ts.map +1 -0
  115. package/dist/testing.js +130 -0
  116. package/dist/testing.js.map +1 -0
  117. package/{src/types.ts → dist/types.d.ts} +5 -5
  118. package/dist/types.d.ts.map +1 -0
  119. package/dist/types.js +2 -0
  120. package/dist/types.js.map +1 -0
  121. package/{src/wireError.ts → dist/wireError.d.ts} +2 -9
  122. package/dist/wireError.d.ts.map +1 -0
  123. package/dist/wireError.js +13 -0
  124. package/dist/wireError.js.map +1 -0
  125. package/package.json +42 -17
  126. package/.claude/settings.local.json +0 -22
  127. package/.dockerignore +0 -10
  128. package/.env.example +0 -90
  129. package/.github/pull_request_template.md +0 -23
  130. package/.github/workflows/ci.yml +0 -55
  131. package/.github/workflows/release.yml +0 -99
  132. package/.prettierignore +0 -3
  133. package/.prettierrc +0 -7
  134. package/AGENTS.md +0 -91
  135. package/CONTRIBUTING.md +0 -234
  136. package/Dockerfile +0 -30
  137. package/SECURITY.md +0 -54
  138. package/docs/api.md +0 -277
  139. package/docs/deployment.md +0 -212
  140. package/eslint.config.mjs +0 -18
  141. package/pnpm-workspace.yaml +0 -13
  142. package/src/app.ts +0 -86
  143. package/src/config.ts +0 -176
  144. package/src/index.ts +0 -54
  145. package/src/lib/frameGate.ts +0 -42
  146. package/src/lib/json.ts +0 -27
  147. package/src/lib/nonceStore.ts +0 -103
  148. package/src/lib/queryWorker/pool.ts +0 -324
  149. package/src/lib/resumeBuffer.ts +0 -268
  150. package/src/lib/tokenSweep.ts +0 -43
  151. package/src/middleware/auth.ts +0 -77
  152. package/src/middleware/errors.ts +0 -73
  153. package/src/routes/attachments.ts +0 -126
  154. package/src/routes/auth.ts +0 -137
  155. package/src/routes/changes.ts +0 -279
  156. package/src/routes/entity.ts +0 -33
  157. package/src/routes/health.ts +0 -8
  158. package/src/routes/records.ts +0 -259
  159. package/src/routes/tokens.ts +0 -85
  160. package/src/routes/types.ts +0 -57
  161. package/src/routes/wellknown.ts +0 -55
  162. package/src/shutdown.ts +0 -56
  163. package/src/stack.ts +0 -96
  164. package/tests/app.test.ts +0 -86
  165. package/tests/changeFeedClient.ts +0 -156
  166. package/tests/config.test.ts +0 -112
  167. package/tests/conformance.test.ts +0 -2322
  168. package/tests/fixtures/blockingWorker.ts +0 -27
  169. package/tests/fixtures/brokenWorker.ts +0 -7
  170. package/tests/lib/changeFeedClient.test.ts +0 -126
  171. package/tests/lib/frameGate.test.ts +0 -93
  172. package/tests/lib/queryLimit.test.ts +0 -33
  173. package/tests/lib/queryWorker.test.ts +0 -60
  174. package/tests/lib/queryWorkerPool.test.ts +0 -129
  175. package/tests/lib/resumeBuffer.test.ts +0 -341
  176. package/tests/lib/resumeCursor.test.ts +0 -46
  177. package/tests/middleware/errors.test.ts +0 -176
  178. package/tests/routes/associations.test.ts +0 -170
  179. package/tests/routes/attachments.test.ts +0 -621
  180. package/tests/routes/auth.test.ts +0 -448
  181. package/tests/routes/changes.test.ts +0 -567
  182. package/tests/routes/contentQuery.test.ts +0 -68
  183. package/tests/routes/entity.test.ts +0 -170
  184. package/tests/routes/groups.test.ts +0 -69
  185. package/tests/routes/records.test.ts +0 -1965
  186. package/tests/routes/relatedToQuery.test.ts +0 -182
  187. package/tests/routes/tokens.test.ts +0 -157
  188. package/tests/routes/tombstones.test.ts +0 -93
  189. package/tests/routes/types.test.ts +0 -129
  190. package/tests/routes/versions.test.ts +0 -177
  191. package/tests/routes/wellknown.test.ts +0 -71
  192. package/tests/setup.ts +0 -154
  193. package/tests/shutdown.test.ts +0 -121
  194. package/tests/stack.test.ts +0 -198
  195. package/tsconfig.build.json +0 -9
  196. package/tsconfig.json +0 -19
  197. package/vitest.config.ts +0 -12
@@ -1,170 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import {
3
- buildTestApp,
4
- req,
5
- TEST_TOKEN,
6
- TEST_ENTITY_ID,
7
- OTHER_ENTITY_ID,
8
- type TestApp,
9
- } from '../setup.js';
10
- import type { StackRecord } from '@haverstack/core';
11
-
12
- async function seedEntityRecord(ctx: TestApp['ctx']): Promise<StackRecord> {
13
- return ctx.adapter.createRecord({
14
- id: TEST_ENTITY_ID,
15
- typeId: '_entity@1',
16
- content: { did: TEST_ENTITY_ID, name: 'Test Entity' },
17
- createdAt: new Date(),
18
- updatedAt: new Date(),
19
- version: 1,
20
- entityId: TEST_ENTITY_ID,
21
- });
22
- }
23
-
24
- // _entity@1 records get an auto-generated id — even the owner's own card
25
- // (see Stack.create()'s ownerProfile bootstrap) — with the binding held in
26
- // content.did rather than the record id. This is the realistic shape.
27
- async function seedEntityRecordWithGeneratedId(ctx: TestApp['ctx']): Promise<StackRecord> {
28
- return ctx.stack.create('_entity@1', { did: TEST_ENTITY_ID, name: 'Test Entity' });
29
- }
30
-
31
- describe('GET /entity', () => {
32
- let t: TestApp;
33
- beforeEach(async () => {
34
- t = await buildTestApp();
35
- });
36
- afterEach(async () => {
37
- await t.cleanup();
38
- });
39
-
40
- it('returns the owner entity record when authenticated as owner', async () => {
41
- await seedEntityRecord(t.ctx);
42
- const { status, data } = await req(t.app, 'GET', '/entity', { token: TEST_TOKEN });
43
- expect(status).toBe(200);
44
- expect((data as Record<string, unknown>).id).toBe(TEST_ENTITY_ID);
45
- expect(((data as Record<string, unknown>).content as Record<string, unknown>).name).toBe(
46
- 'Test Entity',
47
- );
48
- });
49
-
50
- it('returns 404 when the entity record does not exist', async () => {
51
- const { status } = await req(t.app, 'GET', '/entity', { token: TEST_TOKEN });
52
- expect(status).toBe(404);
53
- });
54
-
55
- it('finds the owner record by content.did even when its id differs from ownerEntityId', async () => {
56
- await seedEntityRecordWithGeneratedId(t.ctx);
57
- const { status, data } = await req(t.app, 'GET', '/entity', { token: TEST_TOKEN });
58
- expect(status).toBe(200);
59
- const content = (data as Record<string, unknown>).content as Record<string, unknown>;
60
- expect(content.did).toBe(TEST_ENTITY_ID);
61
- expect(content.name).toBe('Test Entity');
62
- });
63
-
64
- it('returns 404, not 403, for a non-owner authenticated entity (anti-oracle rule)', async () => {
65
- await seedEntityRecord(t.ctx);
66
- const { token } = await t.ctx.adapter.createToken(OTHER_ENTITY_ID);
67
- const { status } = await req(t.app, 'GET', '/entity', { token });
68
- expect(status).toBe(404);
69
- });
70
-
71
- it('returns 401 for an unauthenticated request', async () => {
72
- const { status } = await req(t.app, 'GET', '/entity');
73
- expect(status).toBe(401);
74
- });
75
-
76
- it('resolves the current owner record on each request, including after delete and recreate', async () => {
77
- const first = await seedEntityRecordWithGeneratedId(t.ctx);
78
-
79
- const populate = await req(t.app, 'GET', '/entity', { token: TEST_TOKEN });
80
- expect(populate.status).toBe(200);
81
-
82
- await t.ctx.adapter.deleteRecord(first.id, { hard: true });
83
- const afterDelete = await req(t.app, 'GET', '/entity', { token: TEST_TOKEN });
84
- expect(afterDelete.status).toBe(404);
85
-
86
- const second = await seedEntityRecordWithGeneratedId(t.ctx);
87
- expect(second.id).not.toBe(first.id);
88
- const afterRecreate = await req(t.app, 'GET', '/entity', { token: TEST_TOKEN });
89
- expect(afterRecreate.status).toBe(200);
90
- expect((afterRecreate.data as Record<string, unknown>).id).toBe(second.id);
91
- });
92
- });
93
-
94
- describe('PATCH /entity', () => {
95
- let t: TestApp;
96
- beforeEach(async () => {
97
- t = await buildTestApp();
98
- });
99
- afterEach(async () => {
100
- await t.cleanup();
101
- });
102
-
103
- it('merges content and returns the updated record', async () => {
104
- await seedEntityRecord(t.ctx);
105
- const { status, data } = await req(t.app, 'PATCH', '/entity', {
106
- token: TEST_TOKEN,
107
- body: { content: { name: 'Updated Entity', handle: 'updated' } },
108
- });
109
- expect(status).toBe(200);
110
- const content = (data as Record<string, unknown>).content as Record<string, unknown>;
111
- expect(content.name).toBe('Updated Entity');
112
- expect(content.handle).toBe('updated');
113
- });
114
-
115
- it('returns 404 when the entity record does not exist', async () => {
116
- const { status } = await req(t.app, 'PATCH', '/entity', {
117
- token: TEST_TOKEN,
118
- body: { content: { name: 'X' } },
119
- });
120
- expect(status).toBe(404);
121
- });
122
-
123
- it('updates the owner record found by content.did even when its id differs from ownerEntityId', async () => {
124
- await seedEntityRecordWithGeneratedId(t.ctx);
125
- const { status, data } = await req(t.app, 'PATCH', '/entity', {
126
- token: TEST_TOKEN,
127
- body: { content: { name: 'Renamed' } },
128
- });
129
- expect(status).toBe(200);
130
- const content = (data as Record<string, unknown>).content as Record<string, unknown>;
131
- expect(content.name).toBe('Renamed');
132
- expect(content.did).toBe(TEST_ENTITY_ID);
133
- });
134
-
135
- it('returns 403 for a non-owner authenticated entity', async () => {
136
- await seedEntityRecord(t.ctx);
137
- const { token } = await t.ctx.adapter.createToken(OTHER_ENTITY_ID);
138
- const { status } = await req(t.app, 'PATCH', '/entity', {
139
- token,
140
- body: { content: { name: 'Hacked' } },
141
- });
142
- expect(status).toBe(403);
143
- });
144
-
145
- it('returns 401 for an unauthenticated request', async () => {
146
- const { status } = await req(t.app, 'PATCH', '/entity', {
147
- body: { content: { name: 'X' } },
148
- });
149
- expect(status).toBe(401);
150
- });
151
-
152
- it('resolves the current owner record on each PATCH request after a since-deleted record', async () => {
153
- const record = await seedEntityRecordWithGeneratedId(t.ctx);
154
- await req(t.app, 'GET', '/entity', { token: TEST_TOKEN });
155
-
156
- await t.ctx.adapter.deleteRecord(record.id, { hard: true });
157
- const stale = await req(t.app, 'PATCH', '/entity', {
158
- token: TEST_TOKEN,
159
- body: { content: { name: 'X' } },
160
- });
161
- expect(stale.status).toBe(404);
162
-
163
- await seedEntityRecordWithGeneratedId(t.ctx);
164
- const recovered = await req(t.app, 'PATCH', '/entity', {
165
- token: TEST_TOKEN,
166
- body: { content: { name: 'Recovered' } },
167
- });
168
- expect(recovered.status).toBe(200);
169
- });
170
- });
@@ -1,69 +0,0 @@
1
- /**
2
- * _group-scoped permissions: a permission's groupId resolves only against
3
- * a real _group Record's roster. Core's rule, pinned at this server's HTTP
4
- * surface rather than assumed to arrive intact.
5
- */
6
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
7
- import { SYSTEM_TYPES } from '@haverstack/core';
8
- import { buildTestApp, req, OTHER_ENTITY_ID, type TestApp } from '../setup.js';
9
-
10
- const NOTE_TYPE = 'com.example.test/note@1';
11
- const GROUP_TYPE = `${SYSTEM_TYPES.GROUP}@1`;
12
-
13
- describe('_group ACL', () => {
14
- let t: TestApp;
15
- beforeEach(async () => {
16
- t = await buildTestApp();
17
- await t.ctx.stack.defineType(NOTE_TYPE, 'Note', { title: { kind: 'string' } });
18
- });
19
- afterEach(async () => {
20
- await t.cleanup();
21
- });
22
-
23
- it('an entity added to a real _group roster gains group-scoped read access', async () => {
24
- const group = await t.ctx.stack.create(GROUP_TYPE, { name: 'Test Group' });
25
- await t.ctx.stack.associate(group.id, {
26
- kind: 'relationship',
27
- label: 'member',
28
- target: { scope: 'entity', entityId: OTHER_ENTITY_ID },
29
- });
30
- const record = await t.ctx.stack.create(
31
- NOTE_TYPE,
32
- { title: 'group-visible' },
33
- { permissions: [{ access: 'group', groupId: group.id, read: true, write: false }] },
34
- );
35
- const { token } = await t.ctx.adapter.createToken(OTHER_ENTITY_ID);
36
- const { status } = await req(t.app, 'GET', `/records/${record.id}`, { token });
37
- expect(status).toBe(200);
38
- });
39
-
40
- it('a non-_group record with matching relationship associations is never treated as a roster', async () => {
41
- // Same association shape a real group roster uses (kind: relationship,
42
- // label: member, target: entity) — but on an ordinary Note, not a
43
- // _group Record. Before the 0.20 fix, any record's relationship
44
- // associations could serve as an ACL; this pins that it can't.
45
- const notAGroup = await t.ctx.stack.create(
46
- NOTE_TYPE,
47
- { title: 'not a real group' },
48
- {
49
- associations: [
50
- {
51
- kind: 'relationship',
52
- label: 'member',
53
- target: { scope: 'entity', entityId: OTHER_ENTITY_ID },
54
- },
55
- ],
56
- },
57
- );
58
- const record = await t.ctx.stack.create(
59
- NOTE_TYPE,
60
- { title: 'should stay private' },
61
- { permissions: [{ access: 'group', groupId: notAGroup.id, read: true, write: false }] },
62
- );
63
- const { token } = await t.ctx.adapter.createToken(OTHER_ENTITY_ID);
64
- const { status } = await req(t.app, 'GET', `/records/${record.id}`, { token });
65
- // Anti-oracle: unreadable is 404, same as any other record this entity
66
- // can't reach — never a 403 that would confirm the record exists.
67
- expect(status).toBe(404);
68
- });
69
- });