@kubun/mutation 0.17.1 → 0.18.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.
- package/lib/access-slot-only.d.ts +3 -0
- package/lib/access-slot-only.js +23 -0
- package/lib/apply.d.ts +17 -2
- package/lib/apply.js +232 -58
- package/lib/errors.d.ts +36 -20
- package/lib/errors.js +122 -53
- package/lib/index.d.ts +6 -3
- package/lib/index.js +4 -2
- package/lib/observation-winner.d.ts +19 -0
- package/lib/observation-winner.js +48 -0
- package/package.json +6 -6
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { DocumentMutation, DocumentNode } from '@kubun/protocol';
|
|
2
|
+
/** An exit notice requires a live held state and cannot carry application data. */
|
|
3
|
+
export declare function isAccessSlotOnlyMutation(mutation: DocumentMutation, previous: DocumentNode | null): boolean;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { canonicalStringify } from '@sozai/codec';
|
|
2
|
+
function withoutAccessSlots(data) {
|
|
3
|
+
const { accessPermissions: _hand, defaultAccessPermissions: _default, ...rest } = data;
|
|
4
|
+
return rest;
|
|
5
|
+
}
|
|
6
|
+
/** An exit notice requires a live held state and cannot carry application data. */ export function isAccessSlotOnlyMutation(mutation, previous) {
|
|
7
|
+
if (previous?.data == null || mutation.typ === 'tombstone') return false;
|
|
8
|
+
if (mutation.typ === 'set') {
|
|
9
|
+
return canonicalStringify(withoutAccessSlots(mutation.data)) === canonicalStringify(withoutAccessSlots(previous.data));
|
|
10
|
+
}
|
|
11
|
+
return mutation.patch.length > 0 && mutation.patch.every((patch)=>{
|
|
12
|
+
return [
|
|
13
|
+
'/accessPermissions',
|
|
14
|
+
'/accessPermissions/read',
|
|
15
|
+
'/accessPermissions/write',
|
|
16
|
+
'/defaultAccessPermissions'
|
|
17
|
+
].includes(patch.path) && [
|
|
18
|
+
'add',
|
|
19
|
+
'replace',
|
|
20
|
+
'remove'
|
|
21
|
+
].includes(patch.op) && !('from' in patch);
|
|
22
|
+
});
|
|
23
|
+
}
|
package/lib/apply.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { HLC } from '@kubun/hlc';
|
|
|
2
2
|
import type { DocumentModelID } from '@kubun/id';
|
|
3
3
|
import { DocumentID } from '@kubun/id';
|
|
4
4
|
import type { ChangeDocumentMutation, DocumentData, DocumentModel, DocumentMutation, DocumentNode, SetDocumentMutation, TombstoneDocumentMutation } from '@kubun/protocol';
|
|
5
|
-
import {
|
|
5
|
+
import type { Validator } from '@sozai/schema';
|
|
6
6
|
export type DocumentValidator = Validator<DocumentData>;
|
|
7
7
|
export type ValidatorsRecord = Record<string, Promise<DocumentValidator>>;
|
|
8
8
|
/**
|
|
@@ -28,7 +28,11 @@ export declare const DELETE_FETCH_KEY = "/fetch";
|
|
|
28
28
|
* effects (`saveDocument`, `createDocument`, `updateFieldHLCs`) are not
|
|
29
29
|
* invoked when the gate denies, so engine state is unchanged.
|
|
30
30
|
*/
|
|
31
|
-
export type
|
|
31
|
+
export type PostStateGateContext = {
|
|
32
|
+
previous: DocumentNode | null;
|
|
33
|
+
mutation: DocumentMutation;
|
|
34
|
+
};
|
|
35
|
+
export type PostStateGate = (postState: DocumentNode, context: PostStateGateContext) => Promise<boolean> | boolean;
|
|
32
36
|
/**
|
|
33
37
|
* Minimal database interface required by mutation operations.
|
|
34
38
|
* This decouples @kubun/mutation from the full KubunDB/store API,
|
|
@@ -61,6 +65,7 @@ export type MutationContext = {
|
|
|
61
65
|
* injected checker, only the owner may write (owner-only safe default).
|
|
62
66
|
*/
|
|
63
67
|
checkWriteAccess?: (doc: DocumentNode, mutation: DocumentMutation) => Promise<boolean>;
|
|
68
|
+
checkShareAccess?: (doc: DocumentNode, mutation: DocumentMutation) => Promise<boolean>;
|
|
64
69
|
hlc?: HLC;
|
|
65
70
|
maxDriftMS?: number;
|
|
66
71
|
/**
|
|
@@ -83,6 +88,9 @@ export declare function getDocumentValidator(ctx: MutationContext, id: string):
|
|
|
83
88
|
export declare const DEFAULT_MAX_DRIFT_MS = 3600000;
|
|
84
89
|
export declare function validateFetchTokenDrift(token: string, maxDriftMS: number): void;
|
|
85
90
|
export declare function getTopLevelField(path: string): string;
|
|
91
|
+
/** Assemble visible fields and overlay the retained effective read rule. */
|
|
92
|
+
export declare function assembleDocumentData(values: Record<string, unknown>): DocumentData;
|
|
93
|
+
export declare function validateSetData(validator: DocumentValidator, rawData: Record<string, unknown>, documentID: string): DocumentData;
|
|
86
94
|
/**
|
|
87
95
|
* A model's provider merge policy, or `null` for an ordinary model. Read
|
|
88
96
|
* straight from the store on every call (no cache) since it gates which
|
|
@@ -103,11 +111,18 @@ export declare function resolveModelPolicy(ctx: MutationContext, modelID: Docume
|
|
|
103
111
|
* SET classifier/register-winner logic below, which also need the frontier's
|
|
104
112
|
* own token and liveness to compare a candidate against it.
|
|
105
113
|
*/
|
|
114
|
+
export type ObservedProviderValue = {
|
|
115
|
+
fetchHLC: string;
|
|
116
|
+
valueDigest: string | null;
|
|
117
|
+
present: boolean;
|
|
118
|
+
};
|
|
106
119
|
export type FetchFrontier = {
|
|
107
120
|
hlc: string;
|
|
108
121
|
digest: string;
|
|
109
122
|
alive: boolean;
|
|
123
|
+
observed?: ObservedProviderValue;
|
|
110
124
|
};
|
|
125
|
+
export declare function providerGroupDigest(group: Record<string, unknown>): string;
|
|
111
126
|
/**
|
|
112
127
|
* Pure merge-policy liveness read: the winning side's state digest alone —
|
|
113
128
|
* the live `$setFetch` digest, or `DELETED_STATE_DIGEST` when the delete side
|
package/lib/apply.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { HLC } from '@kubun/hlc';
|
|
2
|
-
import { DocumentID } from '@kubun/id';
|
|
2
|
+
import { DocumentID, digestJSON } from '@kubun/id';
|
|
3
3
|
import { isValidAccessRule, RESERVED_DOCUMENT_FIELDS } from '@kubun/protocol';
|
|
4
|
-
import { fromB64 } from '@sozai/codec';
|
|
4
|
+
import { canonicalStringify, fromB64, toB64U } from '@sozai/codec';
|
|
5
5
|
import { applyPatches } from '@sozai/patch';
|
|
6
6
|
import { asType, createValidator } from '@sozai/schema';
|
|
7
|
-
import { DocumentIDMismatchError, HLCDriftError, IncompleteProviderGroupError, InvalidFetchTokenError, MergePolicyRemoveRequiresTombstoneError, MissingFetchTokenError, MutationSchemaValidationError, OwnerChangeError, ProviderGroupChangeRejectedError, TombstoneRequiresMergePolicyError, UnsupportedMutationTypeError, WriteAccessDeniedError } from './errors.js';
|
|
7
|
+
import { DocumentIDMismatchError, HLCDriftError, IncompleteProviderGroupError, InvalidAccessRulePatchError, InvalidDefaultAccessPermissionsError, InvalidFetchTokenError, MergePolicyRemoveRequiresTombstoneError, MissingFetchTokenError, MutationSchemaValidationError, OwnerChangeError, ProviderGroupChangeRejectedError, TombstoneRequiresMergePolicyError, UnsupportedMutationTypeError, WriteAccessDeniedError } from './errors.js';
|
|
8
8
|
import { DELETED_STATE_DIGEST, stateDigest } from './state-digest.js';
|
|
9
9
|
/**
|
|
10
10
|
* Reserved `field_hlcs` key holding the greatest delete HLC observed for the
|
|
@@ -70,6 +70,105 @@ export function getTopLevelField(path) {
|
|
|
70
70
|
}
|
|
71
71
|
const RESERVED_FIELD_SET = new Set(RESERVED_DOCUMENT_FIELDS);
|
|
72
72
|
const ACCESS_PERMISSIONS_FIELD = 'accessPermissions';
|
|
73
|
+
const DEFAULT_ACCESS_PERMISSIONS_FIELD = 'defaultAccessPermissions';
|
|
74
|
+
const EFFECTIVE_READ_KEY = '$readRule';
|
|
75
|
+
function defaultSlot(value, mutationHLC, docID) {
|
|
76
|
+
if (value == null || typeof value !== 'object') throw new InvalidDefaultAccessPermissionsError(docID);
|
|
77
|
+
const slot = value;
|
|
78
|
+
if (!isValidAccessRule(slot.rule) || typeof slot.hlc !== 'string' || HLC.tryParse(slot.hlc) == null || slot.hlc > mutationHLC) {
|
|
79
|
+
throw new InvalidDefaultAccessPermissionsError(docID);
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
rule: slot.rule,
|
|
83
|
+
hlc: slot.hlc
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function applyDefaultSlot(value, mutationHLC, docID, values, stamps) {
|
|
87
|
+
const slot = defaultSlot(value, mutationHLC, docID);
|
|
88
|
+
const path = `/${DEFAULT_ACCESS_PERMISSIONS_FIELD}`;
|
|
89
|
+
const current = stamps[path];
|
|
90
|
+
if (current != null && (slot.hlc < current || slot.hlc === current && (values[path] == null || canonicalStringify(slot) <= canonicalStringify(values[path])))) return false;
|
|
91
|
+
values[path] = slot;
|
|
92
|
+
stamps[path] = slot.hlc;
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
function applyHandPermissions(value, mutationHLC, values, stamps, touched) {
|
|
96
|
+
if (value != null && sanitizeAccessPermissions(value) == null) return false;
|
|
97
|
+
const candidate = value != null && typeof value === 'object' ? value : {};
|
|
98
|
+
const permissions = {
|
|
99
|
+
...values['/accessPermissions']
|
|
100
|
+
};
|
|
101
|
+
let changed = false;
|
|
102
|
+
for (const permission of [
|
|
103
|
+
'read',
|
|
104
|
+
'write'
|
|
105
|
+
]){
|
|
106
|
+
if (!touched.has(permission)) continue;
|
|
107
|
+
const rule = candidate[permission];
|
|
108
|
+
if (rule != null && !isValidAccessRule(rule)) continue;
|
|
109
|
+
const path = `/accessPermissions/${permission}`;
|
|
110
|
+
if (stamps[path] != null && mutationHLC <= stamps[path]) continue;
|
|
111
|
+
if (rule == null) delete permissions[permission];
|
|
112
|
+
else permissions[permission] = rule;
|
|
113
|
+
stamps[path] = mutationHLC;
|
|
114
|
+
changed = true;
|
|
115
|
+
}
|
|
116
|
+
if (changed) {
|
|
117
|
+
values['/accessPermissions'] = permissions;
|
|
118
|
+
const stamp = stamps['/accessPermissions'];
|
|
119
|
+
stamps['/accessPermissions'] = stamp == null || mutationHLC > stamp ? mutationHLC : stamp;
|
|
120
|
+
}
|
|
121
|
+
return changed;
|
|
122
|
+
}
|
|
123
|
+
function foldReadRule(values) {
|
|
124
|
+
const hand = values['/accessPermissions'];
|
|
125
|
+
const slot = values['/defaultAccessPermissions'];
|
|
126
|
+
const read = hand?.read ?? slot?.rule;
|
|
127
|
+
if (read == null) delete values[EFFECTIVE_READ_KEY];
|
|
128
|
+
else values[EFFECTIVE_READ_KEY] = read;
|
|
129
|
+
}
|
|
130
|
+
/** Assemble visible fields and overlay the retained effective read rule. */ export function assembleDocumentData(values) {
|
|
131
|
+
const data = {};
|
|
132
|
+
for (const [path, value] of Object.entries(values)){
|
|
133
|
+
if (path.startsWith('/') && path !== DELETE_KEY && path !== DELETE_FETCH_KEY) data[path.slice(1)] = value;
|
|
134
|
+
}
|
|
135
|
+
const read = values[EFFECTIVE_READ_KEY];
|
|
136
|
+
if (read != null) data.accessPermissions = {
|
|
137
|
+
...data.accessPermissions,
|
|
138
|
+
read
|
|
139
|
+
};
|
|
140
|
+
return data;
|
|
141
|
+
}
|
|
142
|
+
function setNeedsShare(data) {
|
|
143
|
+
return ACCESS_PERMISSIONS_FIELD in data || DEFAULT_ACCESS_PERMISSIONS_FIELD in data;
|
|
144
|
+
}
|
|
145
|
+
function changeNeedsShare(patch) {
|
|
146
|
+
return patch.some((item)=>[
|
|
147
|
+
'/accessPermissions',
|
|
148
|
+
'/defaultAccessPermissions'
|
|
149
|
+
].includes(getTopLevelField(item.path)));
|
|
150
|
+
}
|
|
151
|
+
function validateAccessRulePatches(mutation) {
|
|
152
|
+
for (const patch of mutation.patch){
|
|
153
|
+
const field = getTopLevelField(patch.path);
|
|
154
|
+
const targetsSlot = field === '/accessPermissions' || field === '/defaultAccessPermissions';
|
|
155
|
+
const allowedPath = [
|
|
156
|
+
'/accessPermissions',
|
|
157
|
+
'/accessPermissions/read',
|
|
158
|
+
'/accessPermissions/write',
|
|
159
|
+
'/defaultAccessPermissions'
|
|
160
|
+
].includes(patch.path);
|
|
161
|
+
const source = 'from' in patch && patch.from != null ? getTopLevelField(patch.from) : null;
|
|
162
|
+
if (targetsSlot && (!allowedPath || ![
|
|
163
|
+
'add',
|
|
164
|
+
'replace',
|
|
165
|
+
'set',
|
|
166
|
+
'remove'
|
|
167
|
+
].includes(patch.op)) || source === '/accessPermissions' || source === '/defaultAccessPermissions') throw new InvalidAccessRulePatchError({
|
|
168
|
+
docID: mutation.sub
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
73
172
|
/**
|
|
74
173
|
* Salvage a candidate `accessPermissions` value per rule, mirroring the engine
|
|
75
174
|
* parser (`parseDocumentAccessPermissions`): a malformed `read` or `write` rule
|
|
@@ -86,7 +185,7 @@ const ACCESS_PERMISSIONS_FIELD = 'accessPermissions';
|
|
|
86
185
|
const perms = value;
|
|
87
186
|
const readValid = perms.read !== undefined && isValidAccessRule(perms.read);
|
|
88
187
|
const writeValid = perms.write !== undefined && isValidAccessRule(perms.write);
|
|
89
|
-
if (!readValid && !writeValid) {
|
|
188
|
+
if (!readValid && !writeValid && Object.keys(perms).length > 0) {
|
|
90
189
|
return null;
|
|
91
190
|
}
|
|
92
191
|
const sanitized = {
|
|
@@ -139,6 +238,19 @@ const ACCESS_PERMISSIONS_FIELD = 'accessPermissions';
|
|
|
139
238
|
droppedReserved
|
|
140
239
|
};
|
|
141
240
|
}
|
|
241
|
+
export function validateSetData(validator, rawData, documentID) {
|
|
242
|
+
const { model, reserved } = splitReservedFields(rawData);
|
|
243
|
+
try {
|
|
244
|
+
return {
|
|
245
|
+
...asType(validator, model),
|
|
246
|
+
...reserved
|
|
247
|
+
};
|
|
248
|
+
} catch (err) {
|
|
249
|
+
throw new MutationSchemaValidationError(documentID, {
|
|
250
|
+
cause: err
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
142
254
|
/**
|
|
143
255
|
* Compute the visible document data from the per-field winning values and the
|
|
144
256
|
* reserved set/delete timestamps. A document is alive when it carries a set
|
|
@@ -153,14 +265,7 @@ const ACCESS_PERMISSIONS_FIELD = 'accessPermissions';
|
|
|
153
265
|
if (!alive) {
|
|
154
266
|
return null;
|
|
155
267
|
}
|
|
156
|
-
|
|
157
|
-
for (const [path, value] of Object.entries(fieldValues)){
|
|
158
|
-
if (path === DELETE_KEY || path === SET_KEY) {
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
data[path.slice(1)] = value;
|
|
162
|
-
}
|
|
163
|
-
return data;
|
|
268
|
+
return assembleDocumentData(fieldValues);
|
|
164
269
|
}
|
|
165
270
|
/**
|
|
166
271
|
* A model's provider merge policy, or `null` for an ordinary model. Read
|
|
@@ -195,14 +300,7 @@ const ACCESS_PERMISSIONS_FIELD = 'accessPermissions';
|
|
|
195
300
|
if (!alive) {
|
|
196
301
|
return null;
|
|
197
302
|
}
|
|
198
|
-
|
|
199
|
-
for (const [path, value] of Object.entries(fieldValues)){
|
|
200
|
-
if (path === DELETE_KEY || path === SET_KEY || path === DELETE_FETCH_KEY || path === SET_FETCH_KEY) {
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
data[path.slice(1)] = value;
|
|
204
|
-
}
|
|
205
|
-
return data;
|
|
303
|
+
return assembleDocumentData(fieldValues);
|
|
206
304
|
}
|
|
207
305
|
/**
|
|
208
306
|
* Pure merge-policy liveness read: given a document's `field_values` cell map
|
|
@@ -222,14 +320,28 @@ const ACCESS_PERMISSIONS_FIELD = 'accessPermissions';
|
|
|
222
320
|
const alive = s != null && (t == null || s > t);
|
|
223
321
|
return alive ? {
|
|
224
322
|
hlc: s,
|
|
225
|
-
digest: setCell
|
|
226
|
-
alive: true
|
|
323
|
+
digest: setCell?.digest,
|
|
324
|
+
alive: true,
|
|
325
|
+
observed: setCell?.observed
|
|
227
326
|
} : {
|
|
228
327
|
hlc: t,
|
|
229
328
|
digest: DELETED_STATE_DIGEST,
|
|
230
|
-
alive: false
|
|
329
|
+
alive: false,
|
|
330
|
+
observed: setCell?.observed
|
|
231
331
|
};
|
|
232
332
|
}
|
|
333
|
+
function omitObjectNulls(value) {
|
|
334
|
+
if (Array.isArray(value)) return value.map(omitObjectNulls);
|
|
335
|
+
if (value === null || typeof value !== 'object') return value;
|
|
336
|
+
const result = {};
|
|
337
|
+
for (const [key, item] of Object.entries(value)){
|
|
338
|
+
if (item !== null && item !== undefined) result[key] = omitObjectNulls(item);
|
|
339
|
+
}
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
export function providerGroupDigest(group) {
|
|
343
|
+
return toB64U(digestJSON(omitObjectNulls(group)));
|
|
344
|
+
}
|
|
233
345
|
/**
|
|
234
346
|
* Pure merge-policy liveness read: the winning side's state digest alone —
|
|
235
347
|
* the live `$setFetch` digest, or `DELETED_STATE_DIGEST` when the delete side
|
|
@@ -330,10 +442,15 @@ export async function applyChangeMutation(ctx, mutation) {
|
|
|
330
442
|
if (!allowed) {
|
|
331
443
|
throw new WriteAccessDeniedError(docID);
|
|
332
444
|
}
|
|
445
|
+
if (changeNeedsShare(mutation.patch)) {
|
|
446
|
+
const shared = ctx.checkShareAccess != null && await ctx.checkShareAccess(doc, mutation);
|
|
447
|
+
if (!shared) throw new WriteAccessDeniedError(docID);
|
|
448
|
+
}
|
|
333
449
|
}
|
|
334
450
|
if (ctx.maxDriftMS != null) {
|
|
335
451
|
validateHLCDrift(mutation.hlc, ctx.maxDriftMS);
|
|
336
452
|
}
|
|
453
|
+
validateAccessRulePatches(mutation);
|
|
337
454
|
const modelID = id.model.toString();
|
|
338
455
|
const [storedHLCs, storedValues, policy] = await Promise.all([
|
|
339
456
|
ctx.store.getFieldHLCs(docID),
|
|
@@ -376,7 +493,10 @@ export async function applyChangeMutation(ctx, mutation) {
|
|
|
376
493
|
...doc,
|
|
377
494
|
data: assembled
|
|
378
495
|
};
|
|
379
|
-
const allowed = await ctx.postStateGate(postState
|
|
496
|
+
const allowed = await ctx.postStateGate(postState, {
|
|
497
|
+
previous: doc,
|
|
498
|
+
mutation
|
|
499
|
+
});
|
|
380
500
|
if (!allowed) {
|
|
381
501
|
return {
|
|
382
502
|
document: null
|
|
@@ -419,27 +539,55 @@ export async function applyChangeMutation(ctx, mutation) {
|
|
|
419
539
|
}
|
|
420
540
|
}
|
|
421
541
|
}
|
|
542
|
+
let permissionsChanged = false;
|
|
543
|
+
const permissionPatches = patchesByField.get('/accessPermissions');
|
|
544
|
+
if (permissionPatches != null) {
|
|
545
|
+
const candidate = {
|
|
546
|
+
accessPermissions: structuredClone(fieldValues['/accessPermissions'] ?? {})
|
|
547
|
+
};
|
|
548
|
+
applyPatches(candidate, permissionPatches, false);
|
|
549
|
+
const touched = new Set();
|
|
550
|
+
for (const patch of permissionPatches){
|
|
551
|
+
if (patch.path === '/accessPermissions') {
|
|
552
|
+
touched.add('read');
|
|
553
|
+
touched.add('write');
|
|
554
|
+
} else touched.add(patch.path.split('/')[2] ?? '');
|
|
555
|
+
}
|
|
556
|
+
permissionsChanged = applyHandPermissions(candidate.accessPermissions, mutation.hlc, fieldValues, fieldHLCs, touched);
|
|
557
|
+
patchesByField.delete('/accessPermissions');
|
|
558
|
+
}
|
|
559
|
+
let defaultChanged = false;
|
|
560
|
+
const defaultPatches = patchesByField.get('/defaultAccessPermissions');
|
|
561
|
+
if (defaultPatches != null) {
|
|
562
|
+
const candidate = {
|
|
563
|
+
defaultAccessPermissions: structuredClone(fieldValues['/defaultAccessPermissions'])
|
|
564
|
+
};
|
|
565
|
+
applyPatches(candidate, defaultPatches, false);
|
|
566
|
+
if (!('defaultAccessPermissions' in candidate)) {
|
|
567
|
+
const path = '/defaultAccessPermissions';
|
|
568
|
+
if (fieldHLCs[path] == null || mutation.hlc >= fieldHLCs[path]) {
|
|
569
|
+
delete fieldValues[path];
|
|
570
|
+
fieldHLCs[path] = mutation.hlc;
|
|
571
|
+
defaultChanged = true;
|
|
572
|
+
}
|
|
573
|
+
} else {
|
|
574
|
+
defaultChanged = applyDefaultSlot(candidate.defaultAccessPermissions, mutation.hlc, docID, fieldValues, fieldHLCs);
|
|
575
|
+
}
|
|
576
|
+
patchesByField.delete('/defaultAccessPermissions');
|
|
577
|
+
}
|
|
422
578
|
// A change field write wins when its HLC beats the field's current HLC and
|
|
423
579
|
// also strictly beats the set timestamp. Losing the set comparison means a
|
|
424
580
|
// newer (or concurrent) set already authored the field set, so a refining
|
|
425
581
|
// change cannot override it.
|
|
426
582
|
//
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
// metadata orthogonal to provider content, on every model) and, on a
|
|
430
|
-
// merge-policy model, any field outside its provider group (a
|
|
431
|
-
// local-annotation field — group fields never reach here, rejected above).
|
|
432
|
-
// Coupling either to the provider group's `$set`/fetch-order frontier would
|
|
433
|
-
// make acceptance depend on that frontier's arrival order relative to the
|
|
434
|
-
// change, which is noncommutative. An ordinary model's non-reserved fields
|
|
435
|
-
// keep the coupled gate unchanged.
|
|
583
|
+
// Access slots were ordered independently above. Provider-local annotations
|
|
584
|
+
// also order independently of the provider group's set frontier.
|
|
436
585
|
const winningFields = [];
|
|
437
586
|
const winningPatches = [];
|
|
438
587
|
for (const [path, patches] of patchesByField){
|
|
439
|
-
const field = path.slice(1);
|
|
440
588
|
const currentHLC = fieldHLCs[path];
|
|
441
589
|
const beatsField = currentHLC == null || mutation.hlc > currentHLC;
|
|
442
|
-
const decoupled =
|
|
590
|
+
const decoupled = groupFieldSet != null;
|
|
443
591
|
const beatsSet = decoupled || s == null || mutation.hlc > s;
|
|
444
592
|
if (beatsField && beatsSet) {
|
|
445
593
|
winningFields.push(path);
|
|
@@ -447,7 +595,7 @@ export async function applyChangeMutation(ctx, mutation) {
|
|
|
447
595
|
}
|
|
448
596
|
}
|
|
449
597
|
// No winning field means no state change; the gate is not invoked.
|
|
450
|
-
if (winningFields.length === 0) {
|
|
598
|
+
if (winningFields.length === 0 && !defaultChanged && !permissionsChanged) {
|
|
451
599
|
return {
|
|
452
600
|
document: doc
|
|
453
601
|
};
|
|
@@ -457,7 +605,7 @@ export async function applyChangeMutation(ctx, mutation) {
|
|
|
457
605
|
// a change only refines fields it already authored.
|
|
458
606
|
const base = {};
|
|
459
607
|
for (const [path, value] of Object.entries(fieldValues)){
|
|
460
|
-
if (path === DELETE_KEY || path === SET_KEY || path === SET_FETCH_KEY || path === DELETE_FETCH_KEY) {
|
|
608
|
+
if (path === DELETE_KEY || path === SET_KEY || path === SET_FETCH_KEY || path === DELETE_FETCH_KEY || path === EFFECTIVE_READ_KEY) {
|
|
461
609
|
continue;
|
|
462
610
|
}
|
|
463
611
|
base[path.slice(1)] = value;
|
|
@@ -491,6 +639,7 @@ export async function applyChangeMutation(ctx, mutation) {
|
|
|
491
639
|
fieldValues[path] = validated[field];
|
|
492
640
|
fieldHLCs[path] = mutation.hlc;
|
|
493
641
|
}
|
|
642
|
+
foldReadRule(fieldValues);
|
|
494
643
|
// A merge-policy document's liveness and visible data follow the
|
|
495
644
|
// fetch-order registers, not `$set`/`/` — mirrors `applySetMutation`'s same
|
|
496
645
|
// choice. The registers themselves are untouched by this change (the group
|
|
@@ -505,7 +654,10 @@ export async function applyChangeMutation(ctx, mutation) {
|
|
|
505
654
|
...doc,
|
|
506
655
|
data: assembled
|
|
507
656
|
};
|
|
508
|
-
const allowed = await ctx.postStateGate(postState
|
|
657
|
+
const allowed = await ctx.postStateGate(postState, {
|
|
658
|
+
previous: doc,
|
|
659
|
+
mutation
|
|
660
|
+
});
|
|
509
661
|
if (!allowed) {
|
|
510
662
|
return {
|
|
511
663
|
document: null
|
|
@@ -570,18 +722,7 @@ export async function applySetMutation(ctx, mutation) {
|
|
|
570
722
|
// Reserved fields are engine metadata the model schema never declares. Split
|
|
571
723
|
// them out before validating the model-declared remainder, then re-attach so
|
|
572
724
|
// they persist unchanged.
|
|
573
|
-
const
|
|
574
|
-
let data;
|
|
575
|
-
try {
|
|
576
|
-
data = {
|
|
577
|
-
...asType(validator, modelData),
|
|
578
|
-
...reserved
|
|
579
|
-
};
|
|
580
|
-
} catch (err) {
|
|
581
|
-
throw new MutationSchemaValidationError(docID, {
|
|
582
|
-
cause: err
|
|
583
|
-
});
|
|
584
|
-
}
|
|
725
|
+
const data = validateSetData(validator, mutation.data, docID);
|
|
585
726
|
const fieldHLCs = {
|
|
586
727
|
...storedHLCs ?? {}
|
|
587
728
|
};
|
|
@@ -610,17 +751,26 @@ export async function applySetMutation(ctx, mutation) {
|
|
|
610
751
|
if (!allowed) {
|
|
611
752
|
throw new WriteAccessDeniedError(docID);
|
|
612
753
|
}
|
|
754
|
+
if (setNeedsShare(mutation.data)) {
|
|
755
|
+
const shared = ctx.checkShareAccess != null && await ctx.checkShareAccess(target, mutation);
|
|
756
|
+
if (!shared) throw new WriteAccessDeniedError(docID);
|
|
757
|
+
}
|
|
613
758
|
}
|
|
614
759
|
if (doc != null && doc.owner !== owner) {
|
|
615
760
|
throw new OwnerChangeError(id.toString(), doc.owner, owner);
|
|
616
761
|
}
|
|
617
|
-
let changed =
|
|
762
|
+
let changed = DEFAULT_ACCESS_PERMISSIONS_FIELD in data && applyDefaultSlot(data.defaultAccessPermissions, mutation.hlc, docID, fieldValues, fieldHLCs);
|
|
763
|
+
if (ACCESS_PERMISSIONS_FIELD in data) {
|
|
764
|
+
const permissions = data.accessPermissions;
|
|
765
|
+
changed = applyHandPermissions(permissions, mutation.hlc, fieldValues, fieldHLCs, new Set(Object.keys(permissions))) || changed;
|
|
766
|
+
}
|
|
618
767
|
let setChanged = false;
|
|
619
768
|
let mergeFields = null;
|
|
620
769
|
if (policy == null) {
|
|
621
770
|
// Per-field LWW: a set field write wins when its HLC beats the field's
|
|
622
771
|
// current HLC. Winning writes update both the retained value and timestamp.
|
|
623
772
|
for (const [field, value] of Object.entries(data)){
|
|
773
|
+
if (field === DEFAULT_ACCESS_PERMISSIONS_FIELD || field === ACCESS_PERMISSIONS_FIELD) continue;
|
|
624
774
|
const path = `/${field}`;
|
|
625
775
|
const currentHLC = fieldHLCs[path];
|
|
626
776
|
if (currentHLC == null || mutation.hlc > currentHLC) {
|
|
@@ -639,7 +789,7 @@ export async function applySetMutation(ctx, mutation) {
|
|
|
639
789
|
// Local-annotation fields (outside the provider group) keep plain
|
|
640
790
|
// per-field HLC LWW, exactly like an ordinary model's fields.
|
|
641
791
|
for (const [field, value] of Object.entries(data)){
|
|
642
|
-
if (groupFields.has(field)) {
|
|
792
|
+
if (groupFields.has(field) || field === DEFAULT_ACCESS_PERMISSIONS_FIELD || field === ACCESS_PERMISSIONS_FIELD) {
|
|
643
793
|
continue;
|
|
644
794
|
}
|
|
645
795
|
const path = `/${field}`;
|
|
@@ -690,7 +840,10 @@ export async function applySetMutation(ctx, mutation) {
|
|
|
690
840
|
}
|
|
691
841
|
fieldValues[SET_FETCH_KEY] = {
|
|
692
842
|
hlc: token,
|
|
693
|
-
digest: candidateDigest
|
|
843
|
+
digest: candidateDigest,
|
|
844
|
+
...frontier?.observed !== undefined && token <= frontier.observed.fetchHLC ? {
|
|
845
|
+
observed: frontier.observed
|
|
846
|
+
} : {}
|
|
694
847
|
};
|
|
695
848
|
fieldHLCs[SET_FETCH_KEY] = token;
|
|
696
849
|
setChanged = true;
|
|
@@ -709,6 +862,7 @@ export async function applySetMutation(ctx, mutation) {
|
|
|
709
862
|
};
|
|
710
863
|
}
|
|
711
864
|
}
|
|
865
|
+
foldReadRule(fieldValues);
|
|
712
866
|
const assembled = policy == null ? assemble(fieldValues, fieldHLCs) : assembleMergePolicy(fieldValues, fieldHLCs);
|
|
713
867
|
const toResult = (document)=>mergeFields == null ? {
|
|
714
868
|
document
|
|
@@ -727,7 +881,10 @@ export async function applySetMutation(ctx, mutation) {
|
|
|
727
881
|
...synthesizedPostState,
|
|
728
882
|
data: assembled
|
|
729
883
|
};
|
|
730
|
-
const allowed = await ctx.postStateGate(postState
|
|
884
|
+
const allowed = await ctx.postStateGate(postState, {
|
|
885
|
+
previous: doc,
|
|
886
|
+
mutation
|
|
887
|
+
});
|
|
731
888
|
if (!allowed) {
|
|
732
889
|
return {
|
|
733
890
|
document: null
|
|
@@ -750,7 +907,10 @@ export async function applySetMutation(ctx, mutation) {
|
|
|
750
907
|
...doc,
|
|
751
908
|
data: assembled
|
|
752
909
|
};
|
|
753
|
-
const allowed = await ctx.postStateGate(postState
|
|
910
|
+
const allowed = await ctx.postStateGate(postState, {
|
|
911
|
+
previous: doc,
|
|
912
|
+
mutation
|
|
913
|
+
});
|
|
754
914
|
if (!allowed) {
|
|
755
915
|
return {
|
|
756
916
|
document: null
|
|
@@ -758,11 +918,12 @@ export async function applySetMutation(ctx, mutation) {
|
|
|
758
918
|
}
|
|
759
919
|
}
|
|
760
920
|
await ctx.store.updateFieldHLCs(id, fieldHLCs, fieldValues);
|
|
761
|
-
|
|
921
|
+
const saved = await ctx.store.saveDocument({
|
|
762
922
|
id,
|
|
763
923
|
existing: doc,
|
|
764
924
|
data: assembled
|
|
765
|
-
})
|
|
925
|
+
});
|
|
926
|
+
return toResult(saved);
|
|
766
927
|
}
|
|
767
928
|
/**
|
|
768
929
|
* A fetch-token-carrying deletion on a merge-policy document's `/fetch`
|
|
@@ -862,6 +1023,16 @@ export async function applySetMutation(ctx, mutation) {
|
|
|
862
1023
|
fieldValues[DELETE_FETCH_KEY] = {
|
|
863
1024
|
hlc: token
|
|
864
1025
|
};
|
|
1026
|
+
if (frontier?.observed !== undefined && token > frontier.observed.fetchHLC) {
|
|
1027
|
+
const setCell = fieldValues[SET_FETCH_KEY];
|
|
1028
|
+
if (setCell !== undefined) {
|
|
1029
|
+
if (setCell.hlc === undefined) delete fieldValues[SET_FETCH_KEY];
|
|
1030
|
+
else fieldValues[SET_FETCH_KEY] = {
|
|
1031
|
+
hlc: setCell.hlc,
|
|
1032
|
+
digest: setCell.digest
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
865
1036
|
fieldHLCs[DELETE_FETCH_KEY] = token;
|
|
866
1037
|
// A winning delete always moves the register to the constant deleted
|
|
867
1038
|
// digest, regardless of what the frontier held before.
|
|
@@ -894,7 +1065,10 @@ export async function applySetMutation(ctx, mutation) {
|
|
|
894
1065
|
...doc,
|
|
895
1066
|
data: assembled
|
|
896
1067
|
};
|
|
897
|
-
const allowed = await ctx.postStateGate(postState
|
|
1068
|
+
const allowed = await ctx.postStateGate(postState, {
|
|
1069
|
+
previous: doc,
|
|
1070
|
+
mutation
|
|
1071
|
+
});
|
|
898
1072
|
if (!allowed) {
|
|
899
1073
|
return {
|
|
900
1074
|
document: null
|
package/lib/errors.d.ts
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
|
+
export type KubunMutationErrorCode = 'KB07' | 'KB09' | 'KB10' | 'KB11' | 'KB12' | 'KB13' | 'KB16' | 'KB17' | 'KB18' | 'KB19' | 'KB20' | 'KB21' | 'KB22';
|
|
2
|
+
export type KubunMutationErrorParams = {
|
|
3
|
+
message: string;
|
|
4
|
+
code: KubunMutationErrorCode;
|
|
5
|
+
docID?: string;
|
|
6
|
+
cause?: unknown;
|
|
7
|
+
};
|
|
8
|
+
export declare class KubunMutationError extends Error {
|
|
9
|
+
#private;
|
|
10
|
+
constructor(params: KubunMutationErrorParams);
|
|
11
|
+
get code(): KubunMutationErrorCode;
|
|
12
|
+
get docID(): string | undefined;
|
|
13
|
+
}
|
|
1
14
|
/**
|
|
2
15
|
* Thrown by the mutation apply path when a non-owner writer's capability
|
|
3
16
|
* check fails. Tagged with the protocol-level error code `KB07` so callers
|
|
4
17
|
* — particularly the RPC handler — can translate the throw into a
|
|
5
18
|
* structured wire response without losing the reason.
|
|
6
19
|
*/
|
|
7
|
-
export declare class WriteAccessDeniedError extends
|
|
8
|
-
#private;
|
|
20
|
+
export declare class WriteAccessDeniedError extends KubunMutationError {
|
|
9
21
|
constructor(docID: string);
|
|
10
22
|
get code(): 'KB07';
|
|
11
23
|
get docID(): string;
|
|
@@ -19,8 +31,7 @@ export declare class WriteAccessDeniedError extends Error {
|
|
|
19
31
|
* error code `KB09` so callers — particularly the RPC handler — can translate
|
|
20
32
|
* the throw into a structured wire response without losing the reason.
|
|
21
33
|
*/
|
|
22
|
-
export declare class DocumentIDMismatchError extends
|
|
23
|
-
#private;
|
|
34
|
+
export declare class DocumentIDMismatchError extends KubunMutationError {
|
|
24
35
|
constructor(docID: string);
|
|
25
36
|
get code(): 'KB09';
|
|
26
37
|
get docID(): string;
|
|
@@ -32,7 +43,7 @@ export declare class DocumentIDMismatchError extends Error {
|
|
|
32
43
|
* the bound — the reject is permanent. Tagged `KB10` so the RPC handler can
|
|
33
44
|
* translate the throw into a structured wire response.
|
|
34
45
|
*/
|
|
35
|
-
export declare class HLCDriftError extends
|
|
46
|
+
export declare class HLCDriftError extends KubunMutationError {
|
|
36
47
|
#private;
|
|
37
48
|
constructor(driftMS: number, maxDriftMS: number);
|
|
38
49
|
get code(): 'KB10';
|
|
@@ -45,7 +56,7 @@ export declare class HLCDriftError extends Error {
|
|
|
45
56
|
* `HLC.tryParse` validation. Tagged `KB16` so the RPC handler can translate the
|
|
46
57
|
* throw into a structured wire response.
|
|
47
58
|
*/
|
|
48
|
-
export declare class InvalidFetchTokenError extends
|
|
59
|
+
export declare class InvalidFetchTokenError extends KubunMutationError {
|
|
49
60
|
#private;
|
|
50
61
|
constructor(token: string);
|
|
51
62
|
get code(): 'KB16';
|
|
@@ -58,7 +69,7 @@ export declare class InvalidFetchTokenError extends Error {
|
|
|
58
69
|
* `KB17` so the RPC handler can translate the throw into a structured wire
|
|
59
70
|
* response.
|
|
60
71
|
*/
|
|
61
|
-
export declare class FetchTokenOrderError extends
|
|
72
|
+
export declare class FetchTokenOrderError extends KubunMutationError {
|
|
62
73
|
constructor();
|
|
63
74
|
get code(): 'KB17';
|
|
64
75
|
}
|
|
@@ -70,7 +81,7 @@ export declare class FetchTokenOrderError extends Error {
|
|
|
70
81
|
* value instead of throwing). Tagged `KB18` so the RPC handler can translate
|
|
71
82
|
* the throw into a structured wire response.
|
|
72
83
|
*/
|
|
73
|
-
export declare class IncompleteProviderGroupError extends
|
|
84
|
+
export declare class IncompleteProviderGroupError extends KubunMutationError {
|
|
74
85
|
#private;
|
|
75
86
|
constructor(docID: string, field: string);
|
|
76
87
|
get code(): 'KB18';
|
|
@@ -86,8 +97,7 @@ export declare class IncompleteProviderGroupError extends Error {
|
|
|
86
97
|
* `KB19` so the RPC handler can translate the throw into a structured wire
|
|
87
98
|
* response.
|
|
88
99
|
*/
|
|
89
|
-
export declare class TombstoneRequiresMergePolicyError extends
|
|
90
|
-
#private;
|
|
100
|
+
export declare class TombstoneRequiresMergePolicyError extends KubunMutationError {
|
|
91
101
|
constructor(docID: string);
|
|
92
102
|
get code(): 'KB19';
|
|
93
103
|
get docID(): string;
|
|
@@ -101,7 +111,7 @@ export declare class TombstoneRequiresMergePolicyError extends Error {
|
|
|
101
111
|
* CHANGE to a field outside the group is unaffected. Tagged `KB20` so the RPC
|
|
102
112
|
* handler can translate the throw into a structured wire response.
|
|
103
113
|
*/
|
|
104
|
-
export declare class ProviderGroupChangeRejectedError extends
|
|
114
|
+
export declare class ProviderGroupChangeRejectedError extends KubunMutationError {
|
|
105
115
|
#private;
|
|
106
116
|
constructor(docID: string, field: string);
|
|
107
117
|
get code(): 'KB20';
|
|
@@ -118,8 +128,7 @@ export declare class ProviderGroupChangeRejectedError extends Error {
|
|
|
118
128
|
* `KB21` so the RPC handler can translate the throw into a structured wire
|
|
119
129
|
* response.
|
|
120
130
|
*/
|
|
121
|
-
export declare class MissingFetchTokenError extends
|
|
122
|
-
#private;
|
|
131
|
+
export declare class MissingFetchTokenError extends KubunMutationError {
|
|
123
132
|
constructor(docID: string);
|
|
124
133
|
get code(): 'KB21';
|
|
125
134
|
get docID(): string;
|
|
@@ -136,8 +145,7 @@ export declare class MissingFetchTokenError extends Error {
|
|
|
136
145
|
* always fails. Tagged `KB22` so the RPC handler can translate the throw into a
|
|
137
146
|
* structured wire response.
|
|
138
147
|
*/
|
|
139
|
-
export declare class MergePolicyRemoveRequiresTombstoneError extends
|
|
140
|
-
#private;
|
|
148
|
+
export declare class MergePolicyRemoveRequiresTombstoneError extends KubunMutationError {
|
|
141
149
|
constructor(docID: string);
|
|
142
150
|
get code(): 'KB22';
|
|
143
151
|
get docID(): string;
|
|
@@ -149,8 +157,7 @@ export declare class MergePolicyRemoveRequiresTombstoneError extends Error {
|
|
|
149
157
|
* Tagged `KB11` so the RPC handler can translate the throw into a structured
|
|
150
158
|
* wire response.
|
|
151
159
|
*/
|
|
152
|
-
export declare class OwnerChangeError extends
|
|
153
|
-
#private;
|
|
160
|
+
export declare class OwnerChangeError extends KubunMutationError {
|
|
154
161
|
constructor(docID: string, fromOwner: string, toOwner: string);
|
|
155
162
|
get code(): 'KB11';
|
|
156
163
|
get docID(): string;
|
|
@@ -161,7 +168,7 @@ export declare class OwnerChangeError extends Error {
|
|
|
161
168
|
* deterministic, permanent reject. Tagged `KB12` so the RPC handler can
|
|
162
169
|
* translate the throw into a structured wire response.
|
|
163
170
|
*/
|
|
164
|
-
export declare class UnsupportedMutationTypeError extends
|
|
171
|
+
export declare class UnsupportedMutationTypeError extends KubunMutationError {
|
|
165
172
|
#private;
|
|
166
173
|
constructor(mutationType: string);
|
|
167
174
|
get code(): 'KB12';
|
|
@@ -174,8 +181,7 @@ export declare class UnsupportedMutationTypeError extends Error {
|
|
|
174
181
|
* preserved as `cause`. Tagged `KB13` so the RPC handler can translate the
|
|
175
182
|
* throw into a structured wire response.
|
|
176
183
|
*/
|
|
177
|
-
export declare class MutationSchemaValidationError extends
|
|
178
|
-
#private;
|
|
184
|
+
export declare class MutationSchemaValidationError extends KubunMutationError {
|
|
179
185
|
constructor(docID: string, options?: {
|
|
180
186
|
cause?: unknown;
|
|
181
187
|
});
|
|
@@ -193,6 +199,15 @@ export declare class MutationSchemaValidationError extends Error {
|
|
|
193
199
|
export declare class InvalidAccessPermissionsError extends Error {
|
|
194
200
|
constructor(detail?: string);
|
|
195
201
|
}
|
|
202
|
+
export declare class InvalidDefaultAccessPermissionsError extends KubunMutationError {
|
|
203
|
+
constructor(docID: string);
|
|
204
|
+
}
|
|
205
|
+
type InvalidAccessRulePatchErrorParams = {
|
|
206
|
+
docID: string;
|
|
207
|
+
};
|
|
208
|
+
export declare class InvalidAccessRulePatchError extends KubunMutationError {
|
|
209
|
+
constructor(params: InvalidAccessRulePatchErrorParams);
|
|
210
|
+
}
|
|
196
211
|
/**
|
|
197
212
|
* True when `err` carries a protocol error code identifying a deterministically
|
|
198
213
|
* permanent mutation reject. A permanent reject is one the sender cannot fix by
|
|
@@ -201,3 +216,4 @@ export declare class InvalidAccessPermissionsError extends Error {
|
|
|
201
216
|
* else — a transient store/db failure — is excluded so it stays retryable.
|
|
202
217
|
*/
|
|
203
218
|
export declare function isPermanentApplyError(err: unknown): boolean;
|
|
219
|
+
export {};
|
package/lib/errors.js
CHANGED
|
@@ -1,20 +1,40 @@
|
|
|
1
|
+
export class KubunMutationError extends Error {
|
|
2
|
+
#code;
|
|
3
|
+
#docID;
|
|
4
|
+
constructor(params){
|
|
5
|
+
super(params.message, Object.hasOwn(params, 'cause') ? {
|
|
6
|
+
cause: params.cause
|
|
7
|
+
} : undefined);
|
|
8
|
+
this.name = 'KubunMutationError';
|
|
9
|
+
this.#code = params.code;
|
|
10
|
+
this.#docID = params.docID;
|
|
11
|
+
}
|
|
12
|
+
get code() {
|
|
13
|
+
return this.#code;
|
|
14
|
+
}
|
|
15
|
+
get docID() {
|
|
16
|
+
return this.#docID;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
1
19
|
/**
|
|
2
20
|
* Thrown by the mutation apply path when a non-owner writer's capability
|
|
3
21
|
* check fails. Tagged with the protocol-level error code `KB07` so callers
|
|
4
22
|
* — particularly the RPC handler — can translate the throw into a
|
|
5
23
|
* structured wire response without losing the reason.
|
|
6
|
-
*/ export class WriteAccessDeniedError extends
|
|
7
|
-
#docID;
|
|
24
|
+
*/ export class WriteAccessDeniedError extends KubunMutationError {
|
|
8
25
|
constructor(docID){
|
|
9
|
-
super(
|
|
26
|
+
super({
|
|
27
|
+
message: `Access denied: cannot write document ${docID}`,
|
|
28
|
+
code: 'KB07',
|
|
29
|
+
docID
|
|
30
|
+
});
|
|
10
31
|
this.name = 'WriteAccessDeniedError';
|
|
11
|
-
this.#docID = docID;
|
|
12
32
|
}
|
|
13
33
|
get code() {
|
|
14
34
|
return 'KB07';
|
|
15
35
|
}
|
|
16
36
|
get docID() {
|
|
17
|
-
return
|
|
37
|
+
return super.docID;
|
|
18
38
|
}
|
|
19
39
|
}
|
|
20
40
|
/**
|
|
@@ -25,18 +45,20 @@
|
|
|
25
45
|
* a victim's deterministic-unique document ID. Tagged with the protocol-level
|
|
26
46
|
* error code `KB09` so callers — particularly the RPC handler — can translate
|
|
27
47
|
* the throw into a structured wire response without losing the reason.
|
|
28
|
-
*/ export class DocumentIDMismatchError extends
|
|
29
|
-
#docID;
|
|
48
|
+
*/ export class DocumentIDMismatchError extends KubunMutationError {
|
|
30
49
|
constructor(docID){
|
|
31
|
-
super(
|
|
50
|
+
super({
|
|
51
|
+
message: `Document ID mismatch: ${docID} is not bound to its owner and unique value`,
|
|
52
|
+
code: 'KB09',
|
|
53
|
+
docID
|
|
54
|
+
});
|
|
32
55
|
this.name = 'DocumentIDMismatchError';
|
|
33
|
-
this.#docID = docID;
|
|
34
56
|
}
|
|
35
57
|
get code() {
|
|
36
58
|
return 'KB09';
|
|
37
59
|
}
|
|
38
60
|
get docID() {
|
|
39
|
-
return
|
|
61
|
+
return super.docID;
|
|
40
62
|
}
|
|
41
63
|
}
|
|
42
64
|
/**
|
|
@@ -45,11 +67,14 @@
|
|
|
45
67
|
* is a fixed property of the mutation, so resending cannot move it back under
|
|
46
68
|
* the bound — the reject is permanent. Tagged `KB10` so the RPC handler can
|
|
47
69
|
* translate the throw into a structured wire response.
|
|
48
|
-
*/ export class HLCDriftError extends
|
|
70
|
+
*/ export class HLCDriftError extends KubunMutationError {
|
|
49
71
|
#driftMS;
|
|
50
72
|
#maxDriftMS;
|
|
51
73
|
constructor(driftMS, maxDriftMS){
|
|
52
|
-
super(
|
|
74
|
+
super({
|
|
75
|
+
message: `HLC timestamp too far in the future: ${driftMS}ms drift exceeds ${maxDriftMS}ms limit`,
|
|
76
|
+
code: 'KB10'
|
|
77
|
+
});
|
|
53
78
|
this.name = 'HLCDriftError';
|
|
54
79
|
this.#driftMS = driftMS;
|
|
55
80
|
this.#maxDriftMS = maxDriftMS;
|
|
@@ -69,10 +94,13 @@
|
|
|
69
94
|
* malformed tokens or NaN values that would slip past `HLC.parse` but fail
|
|
70
95
|
* `HLC.tryParse` validation. Tagged `KB16` so the RPC handler can translate the
|
|
71
96
|
* throw into a structured wire response.
|
|
72
|
-
*/ export class InvalidFetchTokenError extends
|
|
97
|
+
*/ export class InvalidFetchTokenError extends KubunMutationError {
|
|
73
98
|
#token;
|
|
74
99
|
constructor(token){
|
|
75
|
-
super(
|
|
100
|
+
super({
|
|
101
|
+
message: `Invalid fetch token: malformed HLC timestamp '${token}'`,
|
|
102
|
+
code: 'KB16'
|
|
103
|
+
});
|
|
76
104
|
this.name = 'InvalidFetchTokenError';
|
|
77
105
|
this.#token = token;
|
|
78
106
|
}
|
|
@@ -89,9 +117,12 @@
|
|
|
89
117
|
* this reject is permanent — resending cannot move the HLC forward. Tagged
|
|
90
118
|
* `KB17` so the RPC handler can translate the throw into a structured wire
|
|
91
119
|
* response.
|
|
92
|
-
*/ export class FetchTokenOrderError extends
|
|
120
|
+
*/ export class FetchTokenOrderError extends KubunMutationError {
|
|
93
121
|
constructor(){
|
|
94
|
-
super(
|
|
122
|
+
super({
|
|
123
|
+
message: 'Fetch token ordering violated: mutation HLC must be >= sourceFetchHLC',
|
|
124
|
+
code: 'KB17'
|
|
125
|
+
});
|
|
95
126
|
this.name = 'FetchTokenOrderError';
|
|
96
127
|
}
|
|
97
128
|
get code() {
|
|
@@ -105,20 +136,22 @@
|
|
|
105
136
|
* omitted OPTIONAL group field is legitimate — it clears that field's stored
|
|
106
137
|
* value instead of throwing). Tagged `KB18` so the RPC handler can translate
|
|
107
138
|
* the throw into a structured wire response.
|
|
108
|
-
*/ export class IncompleteProviderGroupError extends
|
|
109
|
-
#docID;
|
|
139
|
+
*/ export class IncompleteProviderGroupError extends KubunMutationError {
|
|
110
140
|
#field;
|
|
111
141
|
constructor(docID, field){
|
|
112
|
-
super(
|
|
142
|
+
super({
|
|
143
|
+
message: `Incomplete provider group: document ${docID} SET omits required field '${field}'`,
|
|
144
|
+
code: 'KB18',
|
|
145
|
+
docID
|
|
146
|
+
});
|
|
113
147
|
this.name = 'IncompleteProviderGroupError';
|
|
114
|
-
this.#docID = docID;
|
|
115
148
|
this.#field = field;
|
|
116
149
|
}
|
|
117
150
|
get code() {
|
|
118
151
|
return 'KB18';
|
|
119
152
|
}
|
|
120
153
|
get docID() {
|
|
121
|
-
return
|
|
154
|
+
return super.docID;
|
|
122
155
|
}
|
|
123
156
|
get field() {
|
|
124
157
|
return this.#field;
|
|
@@ -132,18 +165,20 @@
|
|
|
132
165
|
* HLC registers and deletes via the `/`→null CHANGE path instead. Tagged
|
|
133
166
|
* `KB19` so the RPC handler can translate the throw into a structured wire
|
|
134
167
|
* response.
|
|
135
|
-
*/ export class TombstoneRequiresMergePolicyError extends
|
|
136
|
-
#docID;
|
|
168
|
+
*/ export class TombstoneRequiresMergePolicyError extends KubunMutationError {
|
|
137
169
|
constructor(docID){
|
|
138
|
-
super(
|
|
170
|
+
super({
|
|
171
|
+
message: `Tombstone requires a provider-merge-policy model: document ${docID}`,
|
|
172
|
+
code: 'KB19',
|
|
173
|
+
docID
|
|
174
|
+
});
|
|
139
175
|
this.name = 'TombstoneRequiresMergePolicyError';
|
|
140
|
-
this.#docID = docID;
|
|
141
176
|
}
|
|
142
177
|
get code() {
|
|
143
178
|
return 'KB19';
|
|
144
179
|
}
|
|
145
180
|
get docID() {
|
|
146
|
-
return
|
|
181
|
+
return super.docID;
|
|
147
182
|
}
|
|
148
183
|
}
|
|
149
184
|
/**
|
|
@@ -154,20 +189,22 @@
|
|
|
154
189
|
* against it would mix two incompatible orderings on the same fields. A
|
|
155
190
|
* CHANGE to a field outside the group is unaffected. Tagged `KB20` so the RPC
|
|
156
191
|
* handler can translate the throw into a structured wire response.
|
|
157
|
-
*/ export class ProviderGroupChangeRejectedError extends
|
|
158
|
-
#docID;
|
|
192
|
+
*/ export class ProviderGroupChangeRejectedError extends KubunMutationError {
|
|
159
193
|
#field;
|
|
160
194
|
constructor(docID, field){
|
|
161
|
-
super(
|
|
195
|
+
super({
|
|
196
|
+
message: `Provider group CHANGE rejected: document ${docID} CHANGE targets provider group field '${field}'`,
|
|
197
|
+
code: 'KB20',
|
|
198
|
+
docID
|
|
199
|
+
});
|
|
162
200
|
this.name = 'ProviderGroupChangeRejectedError';
|
|
163
|
-
this.#docID = docID;
|
|
164
201
|
this.#field = field;
|
|
165
202
|
}
|
|
166
203
|
get code() {
|
|
167
204
|
return 'KB20';
|
|
168
205
|
}
|
|
169
206
|
get docID() {
|
|
170
|
-
return
|
|
207
|
+
return super.docID;
|
|
171
208
|
}
|
|
172
209
|
get field() {
|
|
173
210
|
return this.#field;
|
|
@@ -182,18 +219,20 @@
|
|
|
182
219
|
* present by the time a merge-policy candidate reaches classification. Tagged
|
|
183
220
|
* `KB21` so the RPC handler can translate the throw into a structured wire
|
|
184
221
|
* response.
|
|
185
|
-
*/ export class MissingFetchTokenError extends
|
|
186
|
-
#docID;
|
|
222
|
+
*/ export class MissingFetchTokenError extends KubunMutationError {
|
|
187
223
|
constructor(docID){
|
|
188
|
-
super(
|
|
224
|
+
super({
|
|
225
|
+
message: `Missing fetch token: document ${docID} merge-policy write carries no sourceFetchHLC`,
|
|
226
|
+
code: 'KB21',
|
|
227
|
+
docID
|
|
228
|
+
});
|
|
189
229
|
this.name = 'MissingFetchTokenError';
|
|
190
|
-
this.#docID = docID;
|
|
191
230
|
}
|
|
192
231
|
get code() {
|
|
193
232
|
return 'KB21';
|
|
194
233
|
}
|
|
195
234
|
get docID() {
|
|
196
|
-
return
|
|
235
|
+
return super.docID;
|
|
197
236
|
}
|
|
198
237
|
}
|
|
199
238
|
/**
|
|
@@ -207,18 +246,20 @@
|
|
|
207
246
|
* `/fetch`. The rejection is deterministic and permanent — the same mutation
|
|
208
247
|
* always fails. Tagged `KB22` so the RPC handler can translate the throw into a
|
|
209
248
|
* structured wire response.
|
|
210
|
-
*/ export class MergePolicyRemoveRequiresTombstoneError extends
|
|
211
|
-
#docID;
|
|
249
|
+
*/ export class MergePolicyRemoveRequiresTombstoneError extends KubunMutationError {
|
|
212
250
|
constructor(docID){
|
|
213
|
-
super(
|
|
251
|
+
super({
|
|
252
|
+
message: `Merge-policy remove requires a tombstone: document ${docID} must be deleted via the fetch-token tombstone path`,
|
|
253
|
+
code: 'KB22',
|
|
254
|
+
docID
|
|
255
|
+
});
|
|
214
256
|
this.name = 'MergePolicyRemoveRequiresTombstoneError';
|
|
215
|
-
this.#docID = docID;
|
|
216
257
|
}
|
|
217
258
|
get code() {
|
|
218
259
|
return 'KB22';
|
|
219
260
|
}
|
|
220
261
|
get docID() {
|
|
221
|
-
return
|
|
262
|
+
return super.docID;
|
|
222
263
|
}
|
|
223
264
|
}
|
|
224
265
|
/**
|
|
@@ -227,18 +268,20 @@
|
|
|
227
268
|
* deterministic, permanent reject — the same mutation will always fail.
|
|
228
269
|
* Tagged `KB11` so the RPC handler can translate the throw into a structured
|
|
229
270
|
* wire response.
|
|
230
|
-
*/ export class OwnerChangeError extends
|
|
231
|
-
#docID;
|
|
271
|
+
*/ export class OwnerChangeError extends KubunMutationError {
|
|
232
272
|
constructor(docID, fromOwner, toOwner){
|
|
233
|
-
super(
|
|
273
|
+
super({
|
|
274
|
+
message: `Cannot change owner from ${fromOwner} to ${toOwner} in document: ${docID}`,
|
|
275
|
+
code: 'KB11',
|
|
276
|
+
docID
|
|
277
|
+
});
|
|
234
278
|
this.name = 'OwnerChangeError';
|
|
235
|
-
this.#docID = docID;
|
|
236
279
|
}
|
|
237
280
|
get code() {
|
|
238
281
|
return 'KB11';
|
|
239
282
|
}
|
|
240
283
|
get docID() {
|
|
241
|
-
return
|
|
284
|
+
return super.docID;
|
|
242
285
|
}
|
|
243
286
|
}
|
|
244
287
|
/**
|
|
@@ -246,10 +289,13 @@
|
|
|
246
289
|
* not support. The type is fixed in the signed mutation, so this is a
|
|
247
290
|
* deterministic, permanent reject. Tagged `KB12` so the RPC handler can
|
|
248
291
|
* translate the throw into a structured wire response.
|
|
249
|
-
*/ export class UnsupportedMutationTypeError extends
|
|
292
|
+
*/ export class UnsupportedMutationTypeError extends KubunMutationError {
|
|
250
293
|
#mutationType;
|
|
251
294
|
constructor(mutationType){
|
|
252
|
-
super(
|
|
295
|
+
super({
|
|
296
|
+
message: `Unsupported mutation type: ${mutationType}`,
|
|
297
|
+
code: 'KB12'
|
|
298
|
+
});
|
|
253
299
|
this.name = 'UnsupportedMutationTypeError';
|
|
254
300
|
this.#mutationType = mutationType;
|
|
255
301
|
}
|
|
@@ -266,18 +312,21 @@
|
|
|
266
312
|
* is a deterministic, permanent reject. The original validation error is
|
|
267
313
|
* preserved as `cause`. Tagged `KB13` so the RPC handler can translate the
|
|
268
314
|
* throw into a structured wire response.
|
|
269
|
-
*/ export class MutationSchemaValidationError extends
|
|
270
|
-
#docID;
|
|
315
|
+
*/ export class MutationSchemaValidationError extends KubunMutationError {
|
|
271
316
|
constructor(docID, options){
|
|
272
|
-
super(
|
|
317
|
+
super({
|
|
318
|
+
message: `Mutation payload failed model schema validation for document ${docID}`,
|
|
319
|
+
code: 'KB13',
|
|
320
|
+
docID,
|
|
321
|
+
...options
|
|
322
|
+
});
|
|
273
323
|
this.name = 'MutationSchemaValidationError';
|
|
274
|
-
this.#docID = docID;
|
|
275
324
|
}
|
|
276
325
|
get code() {
|
|
277
326
|
return 'KB13';
|
|
278
327
|
}
|
|
279
328
|
get docID() {
|
|
280
|
-
return
|
|
329
|
+
return super.docID;
|
|
281
330
|
}
|
|
282
331
|
}
|
|
283
332
|
/**
|
|
@@ -293,6 +342,26 @@
|
|
|
293
342
|
this.name = 'InvalidAccessPermissionsError';
|
|
294
343
|
}
|
|
295
344
|
}
|
|
345
|
+
export class InvalidDefaultAccessPermissionsError extends KubunMutationError {
|
|
346
|
+
constructor(docID){
|
|
347
|
+
super({
|
|
348
|
+
message: 'Invalid default access rule or HLC: expected a rule and HLC no later than the mutation',
|
|
349
|
+
code: 'KB13',
|
|
350
|
+
docID
|
|
351
|
+
});
|
|
352
|
+
this.name = 'InvalidDefaultAccessPermissionsError';
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
export class InvalidAccessRulePatchError extends KubunMutationError {
|
|
356
|
+
constructor(params){
|
|
357
|
+
super({
|
|
358
|
+
message: 'Invalid access rule patch: replace or remove a whole slot or read/write rule',
|
|
359
|
+
code: 'KB13',
|
|
360
|
+
docID: params.docID
|
|
361
|
+
});
|
|
362
|
+
this.name = 'InvalidAccessRulePatchError';
|
|
363
|
+
}
|
|
364
|
+
}
|
|
296
365
|
const PERMANENT_APPLY_ERROR_CODES = new Set([
|
|
297
366
|
'KB07',
|
|
298
367
|
'KB09',
|
package/lib/index.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
export { HLC, type HLCTimestamp } from '@kubun/hlc';
|
|
2
|
-
export
|
|
3
|
-
export {
|
|
2
|
+
export { isAccessSlotOnlyMutation } from './access-slot-only.js';
|
|
3
|
+
export type { DocumentValidator, FetchFrontier, MergeOutcome, MutationApplyResult, MutationContext, MutationDB, PostStateGate, PostStateGateContext, ValidatorsRecord, } from './apply.js';
|
|
4
|
+
export { applyChangeMutation, applyMutation, applySetMutation, applyTombstoneMutation, assembleDocumentData, classifySetOutcome, classifyTombstoneOutcome, DEFAULT_MAX_DRIFT_MS, DELETE_FETCH_KEY, getTopLevelField, providerGroupDigest, resolveModelPolicy, SET_FETCH_KEY, validateFetchTokenDrift, validateSetData, winnerStateDigestOf, } from './apply.js';
|
|
4
5
|
export type { CreateChangeMutationParams, CreateRemoveMutationParams, CreateSetMutationParams, CreateTombstoneMutationParams, } from './create.js';
|
|
5
6
|
export { createChangeMutation, createRemoveMutation, createSetMutation, createTombstoneMutation, } from './create.js';
|
|
6
|
-
export {
|
|
7
|
+
export type { KubunMutationErrorCode, KubunMutationErrorParams } from './errors.js';
|
|
8
|
+
export { DocumentIDMismatchError, FetchTokenOrderError, HLCDriftError, IncompleteProviderGroupError, InvalidAccessPermissionsError, InvalidAccessRulePatchError, InvalidDefaultAccessPermissionsError, InvalidFetchTokenError, isPermanentApplyError, KubunMutationError, MergePolicyRemoveRequiresTombstoneError, MissingFetchTokenError, MutationSchemaValidationError, OwnerChangeError, ProviderGroupChangeRejectedError, TombstoneRequiresMergePolicyError, UnsupportedMutationTypeError, WriteAccessDeniedError, } from './errors.js';
|
|
9
|
+
export { type HeldProviderMutation, type HeldProviderWinner, type SelectHeldProviderWinnerParams, selectHeldProviderWinner, } from './observation-winner.js';
|
|
7
10
|
export type { CreateDocumentParams, CreateMutationOperationsParams, MutationOperations, RemoveDocumentParams, SetDocumentParams, UpdateDocumentParams, } from './operations.js';
|
|
8
11
|
export { convertPatchInput, createMutationOperations } from './operations.js';
|
|
9
12
|
export { DELETED_STATE_DIGEST, type StateDigestInput, stateDigest } from './state-digest.js';
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { HLC } from '@kubun/hlc';
|
|
2
|
-
export {
|
|
2
|
+
export { isAccessSlotOnlyMutation } from './access-slot-only.js';
|
|
3
|
+
export { applyChangeMutation, applyMutation, applySetMutation, applyTombstoneMutation, assembleDocumentData, classifySetOutcome, classifyTombstoneOutcome, DEFAULT_MAX_DRIFT_MS, DELETE_FETCH_KEY, getTopLevelField, providerGroupDigest, resolveModelPolicy, SET_FETCH_KEY, validateFetchTokenDrift, validateSetData, winnerStateDigestOf } from './apply.js';
|
|
3
4
|
export { createChangeMutation, createRemoveMutation, createSetMutation, createTombstoneMutation } from './create.js';
|
|
4
|
-
export { DocumentIDMismatchError, FetchTokenOrderError, HLCDriftError, IncompleteProviderGroupError, InvalidAccessPermissionsError, InvalidFetchTokenError, isPermanentApplyError, MergePolicyRemoveRequiresTombstoneError, MissingFetchTokenError, MutationSchemaValidationError, OwnerChangeError, ProviderGroupChangeRejectedError, TombstoneRequiresMergePolicyError, UnsupportedMutationTypeError, WriteAccessDeniedError } from './errors.js';
|
|
5
|
+
export { DocumentIDMismatchError, FetchTokenOrderError, HLCDriftError, IncompleteProviderGroupError, InvalidAccessPermissionsError, InvalidAccessRulePatchError, InvalidDefaultAccessPermissionsError, InvalidFetchTokenError, isPermanentApplyError, KubunMutationError, MergePolicyRemoveRequiresTombstoneError, MissingFetchTokenError, MutationSchemaValidationError, OwnerChangeError, ProviderGroupChangeRejectedError, TombstoneRequiresMergePolicyError, UnsupportedMutationTypeError, WriteAccessDeniedError } from './errors.js';
|
|
6
|
+
export { selectHeldProviderWinner } from './observation-winner.js';
|
|
5
7
|
export { convertPatchInput, createMutationOperations } from './operations.js';
|
|
6
8
|
export { DELETED_STATE_DIGEST, stateDigest } from './state-digest.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SetDocumentMutation, TombstoneDocumentMutation } from '@kubun/protocol';
|
|
2
|
+
import { type DocumentValidator, type ObservedProviderValue } from './apply.js';
|
|
3
|
+
export type HeldProviderMutation = (SetDocumentMutation | TombstoneDocumentMutation) & {
|
|
4
|
+
sourceFetchHLC: string;
|
|
5
|
+
};
|
|
6
|
+
export type HeldProviderWinner = {
|
|
7
|
+
mutation: HeldProviderMutation;
|
|
8
|
+
token: string;
|
|
9
|
+
digest: string;
|
|
10
|
+
present: boolean;
|
|
11
|
+
group: Record<string, unknown>;
|
|
12
|
+
};
|
|
13
|
+
export type SelectHeldProviderWinnerParams = {
|
|
14
|
+
mutations: ReadonlyArray<HeldProviderMutation>;
|
|
15
|
+
observed: ObservedProviderValue;
|
|
16
|
+
fields: ReadonlyArray<string>;
|
|
17
|
+
validator: DocumentValidator;
|
|
18
|
+
};
|
|
19
|
+
export declare function selectHeldProviderWinner(params: SelectHeldProviderWinnerParams): HeldProviderWinner | null;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { providerGroupDigest, validateSetData } from './apply.js';
|
|
2
|
+
import { DELETED_STATE_DIGEST, stateDigest } from './state-digest.js';
|
|
3
|
+
function candidate(mutation, fields, validator) {
|
|
4
|
+
const token = mutation.sourceFetchHLC;
|
|
5
|
+
if (mutation.typ === 'tombstone') {
|
|
6
|
+
return {
|
|
7
|
+
mutation,
|
|
8
|
+
token,
|
|
9
|
+
digest: DELETED_STATE_DIGEST,
|
|
10
|
+
present: false,
|
|
11
|
+
group: {}
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const group = {};
|
|
15
|
+
const data = validateSetData(validator, mutation.data, mutation.sub);
|
|
16
|
+
for (const field of fields){
|
|
17
|
+
if (field in data) group[field] = data[field];
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
mutation,
|
|
21
|
+
token,
|
|
22
|
+
digest: stateDigest({
|
|
23
|
+
state: 'live',
|
|
24
|
+
group
|
|
25
|
+
}),
|
|
26
|
+
present: true,
|
|
27
|
+
group
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function comparesAfter(left, right) {
|
|
31
|
+
if (left.token !== right.token) return left.token > right.token;
|
|
32
|
+
if (left.present !== right.present) return !left.present;
|
|
33
|
+
return left.digest > right.digest;
|
|
34
|
+
}
|
|
35
|
+
export function selectHeldProviderWinner(params) {
|
|
36
|
+
const { mutations, observed, fields, validator } = params;
|
|
37
|
+
let base = null;
|
|
38
|
+
let qualifying = null;
|
|
39
|
+
for (const mutation of mutations){
|
|
40
|
+
const current = candidate(mutation, fields, validator);
|
|
41
|
+
if (base === null || comparesAfter(current, base)) base = current;
|
|
42
|
+
const matches = current.present === observed.present && (current.present ? providerGroupDigest(current.group) === observed.valueDigest : observed.valueDigest === null);
|
|
43
|
+
if (current.token >= observed.fetchHLC || matches) {
|
|
44
|
+
if (qualifying === null || comparesAfter(current, qualifying)) qualifying = current;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return qualifying ?? base;
|
|
48
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/mutation",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"keywords": [],
|
|
5
5
|
"license": "see LICENSE.md",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -15,15 +15,15 @@
|
|
|
15
15
|
"LICENSE.md"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@kubun/hlc": "^0.
|
|
19
|
-
"@kubun/id": "^0.
|
|
20
|
-
"@sozai/codec": "^0.4.
|
|
18
|
+
"@kubun/hlc": "^0.18.0",
|
|
19
|
+
"@kubun/id": "^0.18.0",
|
|
20
|
+
"@sozai/codec": "^0.4.3",
|
|
21
21
|
"@sozai/patch": "^0.1.3",
|
|
22
22
|
"@sozai/runtime": "^0.1.0",
|
|
23
|
-
"@sozai/schema": "^0.1.
|
|
23
|
+
"@sozai/schema": "^0.1.4"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@kubun/protocol": "^0.
|
|
26
|
+
"@kubun/protocol": "^0.18.0"
|
|
27
27
|
},
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public"
|