was-teaching-server 0.9.1 → 0.11.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 (76) hide show
  1. package/CHANGELOG.md +118 -1
  2. package/README.md +23 -11
  3. package/dist/backends/filesystem.d.ts +247 -4
  4. package/dist/backends/filesystem.d.ts.map +1 -1
  5. package/dist/backends/filesystem.js +711 -81
  6. package/dist/backends/filesystem.js.map +1 -1
  7. package/dist/backends/postgres.d.ts +240 -2
  8. package/dist/backends/postgres.d.ts.map +1 -1
  9. package/dist/backends/postgres.js +800 -42
  10. package/dist/backends/postgres.js.map +1 -1
  11. package/dist/backends/postgresSchema.d.ts.map +1 -1
  12. package/dist/backends/postgresSchema.js +69 -57
  13. package/dist/backends/postgresSchema.js.map +1 -1
  14. package/dist/errors.d.ts +26 -9
  15. package/dist/errors.d.ts.map +1 -1
  16. package/dist/errors.js +40 -12
  17. package/dist/errors.js.map +1 -1
  18. package/dist/lib/equalityIndex.d.ts +334 -0
  19. package/dist/lib/equalityIndex.d.ts.map +1 -0
  20. package/dist/lib/equalityIndex.js +552 -0
  21. package/dist/lib/equalityIndex.js.map +1 -0
  22. package/dist/lib/exportManifest.d.ts +9 -0
  23. package/dist/lib/exportManifest.d.ts.map +1 -1
  24. package/dist/lib/exportManifest.js +9 -0
  25. package/dist/lib/exportManifest.js.map +1 -1
  26. package/dist/lib/importTar.d.ts +18 -2
  27. package/dist/lib/importTar.d.ts.map +1 -1
  28. package/dist/lib/importTar.js +77 -4
  29. package/dist/lib/importTar.js.map +1 -1
  30. package/dist/lib/paths.d.ts +33 -0
  31. package/dist/lib/paths.d.ts.map +1 -1
  32. package/dist/lib/paths.js +28 -0
  33. package/dist/lib/paths.js.map +1 -1
  34. package/dist/lib/resourceFileName.d.ts +43 -0
  35. package/dist/lib/resourceFileName.d.ts.map +1 -1
  36. package/dist/lib/resourceFileName.js +63 -0
  37. package/dist/lib/resourceFileName.js.map +1 -1
  38. package/dist/plugin.d.ts.map +1 -1
  39. package/dist/plugin.js +5 -2
  40. package/dist/plugin.js.map +1 -1
  41. package/dist/requests/ChunkRequest.d.ts +104 -0
  42. package/dist/requests/ChunkRequest.d.ts.map +1 -0
  43. package/dist/requests/ChunkRequest.js +396 -0
  44. package/dist/requests/ChunkRequest.js.map +1 -0
  45. package/dist/requests/CollectionRequest.d.ts +22 -5
  46. package/dist/requests/CollectionRequest.d.ts.map +1 -1
  47. package/dist/requests/CollectionRequest.js +192 -11
  48. package/dist/requests/CollectionRequest.js.map +1 -1
  49. package/dist/requests/ResourceRequest.d.ts.map +1 -1
  50. package/dist/requests/ResourceRequest.js +20 -0
  51. package/dist/requests/ResourceRequest.js.map +1 -1
  52. package/dist/requests/SpaceRequest.d.ts +1 -0
  53. package/dist/requests/SpaceRequest.d.ts.map +1 -1
  54. package/dist/requests/SpaceRequest.js +21 -1
  55. package/dist/requests/SpaceRequest.js.map +1 -1
  56. package/dist/routes.d.ts.map +1 -1
  57. package/dist/routes.js +21 -0
  58. package/dist/routes.js.map +1 -1
  59. package/dist/types.d.ts +165 -2
  60. package/dist/types.d.ts.map +1 -1
  61. package/package.json +19 -18
  62. package/src/backends/filesystem.ts +924 -75
  63. package/src/backends/postgres.ts +1114 -32
  64. package/src/backends/postgresSchema.ts +69 -57
  65. package/src/errors.ts +44 -12
  66. package/src/lib/equalityIndex.ts +769 -0
  67. package/src/lib/importTar.ts +107 -4
  68. package/src/lib/paths.ts +48 -0
  69. package/src/lib/resourceFileName.ts +69 -0
  70. package/src/plugin.ts +5 -2
  71. package/src/requests/ChunkRequest.ts +478 -0
  72. package/src/requests/CollectionRequest.ts +221 -12
  73. package/src/requests/ResourceRequest.ts +20 -0
  74. package/src/requests/SpaceRequest.ts +25 -1
  75. package/src/routes.ts +46 -0
  76. package/src/types.ts +175 -1
@@ -21,7 +21,7 @@ import type pg from 'pg'
21
21
  * never edit an applied entry.
22
22
  */
23
23
  export const MIGRATIONS: string[] = [
24
- // v1: the full WAS + WebKMS surface.
24
+ // v1: the full WAS + WebKMS + chunked-storage surface.
25
25
  `
26
26
  -- The Spaces tree. 'description' is NULL for a placeholder row created by a
27
27
  -- write below the Space level (a resource/policy write to a Space whose
@@ -29,17 +29,29 @@ export const MIGRATIONS: string[] = [
29
29
  -- without a '.space.' file; getSpaceDescription treats it as absent.
30
30
  -- 'usage_bytes' is the transactional quota counter (spec "Quotas"),
31
31
  -- maintained in the same transaction as every content write and delete.
32
+ -- 'controller' denormalizes the Space controller (also present as
33
+ -- description->>'controller') onto its own indexed column, keeping the
34
+ -- per-controller COUNT(*) for the Spaces count quota cheap and lockable;
35
+ -- writeSpace maintains it on every insert and update.
32
36
  CREATE TABLE spaces (
33
37
  space_id text COLLATE "C" PRIMARY KEY,
34
38
  description jsonb,
35
- usage_bytes bigint NOT NULL DEFAULT 0
39
+ usage_bytes bigint NOT NULL DEFAULT 0,
40
+ controller text
36
41
  );
42
+ CREATE INDEX spaces_controller_idx ON spaces (controller);
37
43
 
44
+ -- 'description_version' is the monotonic Collection Description version --
45
+ -- the ETag validator behind conditional (If-Match) Collection Description
46
+ -- writes, so concurrent recipient edits compare-and-swap instead of
47
+ -- clobbering. Kept out of the stored 'description' jsonb (it travels only
48
+ -- as the ETag header); writeCollection bumps it on every write.
38
49
  CREATE TABLE collections (
39
- space_id text COLLATE "C" NOT NULL
40
- REFERENCES spaces ON DELETE CASCADE,
41
- collection_id text COLLATE "C" NOT NULL,
42
- description jsonb,
50
+ space_id text COLLATE "C" NOT NULL
51
+ REFERENCES spaces ON DELETE CASCADE,
52
+ collection_id text COLLATE "C" NOT NULL,
53
+ description jsonb,
54
+ description_version integer NOT NULL DEFAULT 1,
43
55
  PRIMARY KEY (space_id, collection_id)
44
56
  );
45
57
 
@@ -49,6 +61,15 @@ export const MIGRATIONS: string[] = [
49
61
  -- 'content_type' records the last-known type on a tombstone. 'version' /
50
62
  -- 'meta_version' are the two ETag validators; 'custom' is the user-writable
51
63
  -- metadata (or the opaque encryption envelope on an encrypted Collection).
64
+ -- 'created_by' is the Resource's creator -- the DID of the invoker of its
65
+ -- FIRST content write, set once and preserved verbatim thereafter (the
66
+ -- spec's OPTIONAL 'createdBy'); NULL for a row written by a caller with no
67
+ -- resolved invoker. 'epoch' is the client-declared key epoch the content
68
+ -- was encrypted under (multi-recipient encrypted Collections), stored
69
+ -- opaquely (the server never computes or verifies it); NULL when no epoch
70
+ -- was declared (a plaintext Collection, or an encrypted write that omitted
71
+ -- the stamp). Neither is COLLATE "C": unlike 'created_at' / 'updated_at',
72
+ -- they never participate in an ORDER BY or keyset comparison.
52
73
  CREATE TABLE resources (
53
74
  space_id text COLLATE "C" NOT NULL,
54
75
  collection_id text COLLATE "C" NOT NULL,
@@ -63,6 +84,8 @@ export const MIGRATIONS: string[] = [
63
84
  deleted boolean NOT NULL DEFAULT false,
64
85
  created_at text COLLATE "C" NOT NULL,
65
86
  updated_at text COLLATE "C" NOT NULL,
87
+ created_by text,
88
+ epoch text,
66
89
  PRIMARY KEY (space_id, collection_id, resource_id),
67
90
  FOREIGN KEY (space_id, collection_id)
68
91
  REFERENCES collections ON DELETE CASCADE
@@ -73,6 +96,30 @@ export const MIGRATIONS: string[] = [
73
96
  CREATE INDEX resources_changes_idx
74
97
  ON resources (space_id, collection_id, updated_at, resource_id);
75
98
 
99
+ -- Chunk storage for chunked Resources. One row per addressed chunk
100
+ -- (space, collection, resource, index); 'bytes' is the opaque chunk
101
+ -- representation (stored exactly like a binary Resource's content, never
102
+ -- parsed), 'size' its byte length (the quota-counter input), and 'version'
103
+ -- the chunk's own monotonic ETag validator (independent of the parent
104
+ -- Resource's). The foreign key to 'resources' gives chunk rows the same
105
+ -- ON DELETE CASCADE the Space/Collection tree already uses, so a HARD
106
+ -- delete of the parent Resource (or its Collection or Space) removes its
107
+ -- chunks with it; a SOFT delete (the tombstone UPDATE in deleteResource)
108
+ -- removes them explicitly in the same transaction instead.
109
+ CREATE TABLE chunks (
110
+ space_id text COLLATE "C" NOT NULL,
111
+ collection_id text COLLATE "C" NOT NULL,
112
+ resource_id text COLLATE "C" NOT NULL,
113
+ chunk_index integer NOT NULL,
114
+ content_type text NOT NULL,
115
+ bytes bytea NOT NULL,
116
+ size bigint NOT NULL DEFAULT 0,
117
+ version integer NOT NULL,
118
+ PRIMARY KEY (space_id, collection_id, resource_id, chunk_index),
119
+ FOREIGN KEY (space_id, collection_id, resource_id)
120
+ REFERENCES resources ON DELETE CASCADE
121
+ );
122
+
76
123
  -- Policies for all three levels in one table; '' sentinel columns keep the
77
124
  -- primary key total (Postgres PKs reject NULL). Space policy: ('', '');
78
125
  -- collection policy: (cid, ''); resource policy: (cid, rid).
@@ -94,6 +141,22 @@ export const MIGRATIONS: string[] = [
94
141
  PRIMARY KEY (space_id, backend_id)
95
142
  );
96
143
 
144
+ -- Space-scoped zcap revocations -- the WAS route families' sibling of the
145
+ -- keystore-scoped 'revocations' table below. A separate table (rather than
146
+ -- a nullable-FK union over one table) lets each keep its own
147
+ -- ON DELETE CASCADE to its parent and its own composite primary key;
148
+ -- deleting a Space here deletes its revocations. Columns mirror
149
+ -- 'revocations', with 'space_id' referencing the spaces tree in place of
150
+ -- 'keystore_id'.
151
+ CREATE TABLE space_revocations (
152
+ space_id text COLLATE "C" NOT NULL REFERENCES spaces ON DELETE CASCADE,
153
+ delegator text COLLATE "C" NOT NULL,
154
+ capability_id text COLLATE "C" NOT NULL,
155
+ record jsonb NOT NULL,
156
+ expires text COLLATE "C",
157
+ PRIMARY KEY (space_id, delegator, capability_id)
158
+ );
159
+
97
160
  -- WebKMS facet: a sibling tree to spaces, exactly as on the filesystem.
98
161
  -- 'controller' / 'sequence' / 'kms_module' are denormalized from the
99
162
  -- verbatim config for the list filter and the update gates.
@@ -126,57 +189,6 @@ export const MIGRATIONS: string[] = [
126
189
  expires text COLLATE "C",
127
190
  PRIMARY KEY (keystore_id, delegator, capability_id)
128
191
  );
129
- `,
130
- // v2: denormalize a Space's controller onto its own column for the Spaces
131
- // count quota (spec "Quotas"). The controller also lives inside 'description'
132
- // (description->>'controller'), but a dedicated, indexed column keeps the
133
- // per-controller COUNT(*) cheap and lockable. Backfill from existing rows;
134
- // writeSpace maintains it on every insert and update.
135
- `
136
- ALTER TABLE spaces ADD COLUMN controller text;
137
- UPDATE spaces SET controller = description->>'controller';
138
- CREATE INDEX spaces_controller_idx ON spaces (controller);
139
- `,
140
- // v3: record a Resource's creator -- the DID of the invoker of its FIRST
141
- // content write, set once and preserved verbatim thereafter (the spec's
142
- // OPTIONAL `createdBy`). NULL for a pre-existing row and for one written by
143
- // a caller with no resolved invoker. Not `COLLATE "C"`: unlike `created_at` /
144
- // `updated_at`, this column never participates in an `ORDER BY` or keyset
145
- // comparison.
146
- `
147
- ALTER TABLE resources ADD COLUMN created_by text;
148
- `,
149
- // v4: Space-scoped zcap revocations -- the WAS route families' sibling of the
150
- // keystore-scoped `revocations` table above. A separate table (rather than a
151
- // nullable-FK union over one table) lets each keep its own `ON DELETE CASCADE`
152
- // to its parent and its own composite primary key; deleting a Space here
153
- // deletes its revocations. Columns mirror `revocations`, with 'space_id'
154
- // referencing the spaces tree in place of 'keystore_id'.
155
- `
156
- CREATE TABLE space_revocations (
157
- space_id text COLLATE "C" NOT NULL REFERENCES spaces ON DELETE CASCADE,
158
- delegator text COLLATE "C" NOT NULL,
159
- capability_id text COLLATE "C" NOT NULL,
160
- record jsonb NOT NULL,
161
- expires text COLLATE "C",
162
- PRIMARY KEY (space_id, delegator, capability_id)
163
- );
164
- `,
165
- // v5: the client-declared key epoch a Resource's content was encrypted under
166
- // (multi-recipient encrypted Collections). Stored opaquely: the server never
167
- // computes or verifies it. NULL when no epoch was declared (a plaintext
168
- // Collection, or an encrypted write that omitted the stamp). Not `COLLATE "C"`
169
- // -- it never participates in an `ORDER BY` or keyset comparison.
170
- `
171
- ALTER TABLE resources ADD COLUMN epoch text;
172
- `,
173
- // v6: the monotonic Collection Description version -- the ETag validator behind
174
- // conditional (`If-Match`) Collection Description writes, so concurrent
175
- // recipient edits compare-and-swap instead of clobbering. Kept out of the
176
- // stored 'description' jsonb (it travels only as the `ETag` header). Backfill
177
- // existing rows to 1; writeCollection bumps it on every write.
178
- `
179
- ALTER TABLE collections ADD COLUMN description_version integer NOT NULL DEFAULT 1;
180
192
  `
181
193
  ]
182
194
 
package/src/errors.ts CHANGED
@@ -124,6 +124,25 @@ export class InvalidResourceIdError extends ProblemError {
124
124
  }
125
125
  }
126
126
 
127
+ /**
128
+ * 400 — the `:chunkIndex` path param of a chunk operation (the
129
+ * `chunked-streams` feature) is not a canonical non-negative integer. The
130
+ * canonical-form requirement (no leading zeros, no sign) keeps every chunk
131
+ * addressable at exactly one URL.
132
+ * @param options {object}
133
+ * @param [options.requestName] {string} request name used in the error title
134
+ */
135
+ export class InvalidChunkIndexError extends ProblemError {
136
+ constructor({ requestName }: { requestName?: string } = {}) {
137
+ super({
138
+ type: ProblemTypes.INVALID_ID,
139
+ title: `Invalid ${requestName || 'Chunk'} request`,
140
+ detail: 'Invalid chunk index (must be a canonical non-negative integer).',
141
+ statusCode: 400
142
+ })
143
+ }
144
+ }
145
+
127
146
  /**
128
147
  * 409 — a `POST` create operation supplied an `id` that already exists.
129
148
  * Create-or-replace at a client-chosen id is the idempotent `PUT` path, which
@@ -146,26 +165,39 @@ export class IdConflictError extends ProblemError {
146
165
  }
147
166
 
148
167
  /**
149
- * 409 — a Resource write carries an `indexed` blinded attribute marked
150
- * `unique: true` whose (HMAC key id, name, value) triple is already claimed by
151
- * another live Resource in the same Collection (the EDV unique-attribute
152
- * invariant, enforced by backends carrying the `blinded-index-query` feature).
153
- * Reuses the `id-conflict` problem type (like the WebKMS conflicts): a unique
154
- * blinded attribute is a client-chosen identifier-like claim, and the registry
155
- * defines no more specific kind. Only ever observable by a caller already
156
- * authorized to write the target.
168
+ * 409 — a write claims a `unique: true` indexed attribute already claimed by
169
+ * another live Resource in the same Collection. Two invariants share it: the
170
+ * EDV blinded one (an `indexed` blinded attribute's (HMAC key id, name, value)
171
+ * triple; the `blinded-index-query` feature, `variant: 'blinded'`, the
172
+ * default) and the plaintext equality one (a `unique`-declared `indexes`
173
+ * attribute's (name, value) pair; the `equality-query` feature,
174
+ * `variant: 'equality'`). Reuses the `id-conflict` problem type (like the
175
+ * WebKMS conflicts): a unique attribute is a client-chosen identifier-like
176
+ * claim, and the registry defines no more specific kind. Only ever observable
177
+ * by a caller already authorized to write the target.
157
178
  */
158
179
  export class UniqueAttributeConflictError extends ProblemError {
159
- constructor() {
180
+ constructor({
181
+ variant = 'blinded'
182
+ }: { variant?: 'blinded' | 'equality' } = {}) {
183
+ const attribute =
184
+ variant === 'equality'
185
+ ? 'unique indexed attribute'
186
+ : 'unique blinded index attribute'
160
187
  const detail =
161
- 'Could not write document; a unique blinded index attribute is already' +
188
+ `Could not write document; a ${attribute} is already` +
162
189
  ' in use by another Resource in this Collection.'
163
190
  super({
164
191
  type: ProblemTypes.ID_CONFLICT,
165
- title: 'A unique blinded index attribute is already in use.',
192
+ title: `A ${attribute} is already in use.`,
166
193
  detail,
167
194
  statusCode: 409,
168
- problems: [{ detail, pointer: '#/indexed' }]
195
+ problems: [
196
+ {
197
+ detail,
198
+ pointer: variant === 'equality' ? '#/indexes' : '#/indexed'
199
+ }
200
+ ]
169
201
  })
170
202
  }
171
203
  }