@forumone/throughline-core 0.2.1 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,78 @@
1
1
  # @forumone/throughline-core
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d20f909: Bind an approval to the document's content rather than to its `updatedAt`.
8
+
9
+ `request_approval` stored `String(document['updatedAt'] ?? …)` as `targetVersion`,
10
+ and publishing's approval step recomputed the same expression at publish time. So
11
+ an approval was tied to a timestamp that moves on **every** save. An editor fixing
12
+ a typo between an approver opening the request and clicking approve invalidated the
13
+ approval — and the approver spent that time reading a version that no longer
14
+ existed.
15
+
16
+ Requiring re-approval after an edit is a defensible rule. Inheriting it from
17
+ whichever timestamp field happened to be nearby is not, and it is why **autosave
18
+ could not be turned on** anywhere the approvals plugin is installed: autosave moves
19
+ `updatedAt` every couple of seconds of typing, so a pending approval would be
20
+ invalidated continuously.
21
+
22
+ Both sides now call `documentContentHash(document)`, new in
23
+ `@forumone/throughline-core`. It hashes the document with the metadata that moves
24
+ without the content moving stripped at every level — `id`, `createdAt`,
25
+ `updatedAt`, `_status`, `__v`, `_id`, `globalType` — over keys in sorted order,
26
+ since blocks come back out of JSONB in no promised order. Array order is preserved,
27
+ because that is the order of the blocks on the page. `{ exclude }` adds
28
+ app-specific bookkeeping fields to the strip list.
29
+
30
+ The rule is now the one that was wanted all along: a save that changed nothing
31
+ keeps a granted approval, a save that changed something invalidates it, and an edit
32
+ that is reverted brings the approval back. That last one is why this is a content
33
+ hash rather than a version id — a version id moves whether or not the content did.
34
+
35
+ The two sides only agree because they load the document identically, with
36
+ `payload.findByID({ collection, id, draft: true })` at the config's default depth.
37
+ A populated relationship and a bare relationship id are different values and no
38
+ normalising makes them one, so a caller hashing a document fetched at some other
39
+ depth gets a hash that matches nothing. That is stated on the function.
40
+
41
+ **Approvals pending at upgrade must be re-requested.** Their `targetVersion` holds
42
+ an ISO timestamp; the publish step now computes a hash, so nothing matches and
43
+ those documents report `approval-required` until a fresh request is granted. No
44
+ migration is offered, because the old value cannot be converted — the content it
45
+ was granted against is not recoverable from a timestamp. Grant a moment for
46
+ in-flight requests to clear before upgrading, or expect approvers to be asked once
47
+ more.
48
+
49
+ Also exports `isDraftWrite` from `@forumone/throughline-publishing`. It is the
50
+ predicate the plugin's own trust boundary uses to tell a "Save draft" apart from
51
+ an unpublish, and it is unavailable to host code that needs the same answer: an
52
+ `afterChange` hook cannot work it out, because Payload sets `data._status =
53
+ 'draft'` on any `draft: true` update before the hooks run and `previousDoc` is the
54
+ latest _version_ rather than the live document. With autosave on, a host hook that
55
+ drops a cache or sends a notification fires every few seconds of typing unless it
56
+ asks this first.
57
+
58
+ Minor rather than patch on all three: `documentContentHash` and `isDraftWrite` are
59
+ new public API, and the stored meaning of `targetVersion` changes.
60
+
61
+ ## 0.2.2
62
+
63
+ ### Patch Changes
64
+
65
+ - 7ee992d: Fix broken external installs of the core plugins.
66
+
67
+ Every core plugin emits a runtime `import { getPluginRegistry } from '@forumone/throughline-plugin-contract'`, but `plugin-contract` was marked `private` and never published — so the published plugins pinned `@forumone/throughline-plugin-contract: 0.0.0`, a version that does not exist on npm, and any external `pnpm install` failed with a 404.
68
+
69
+ `plugin-contract` is now published, so the dependent plugins re-pin a real version. The cross-plugin registry is keyed on a global `Symbol.for(...)` and stored on the Payload instance, so behavior is unchanged.
70
+
71
+ Also fixes the scaffolder, which pinned `@forumone/throughline-reference-ds@^0.1.0` (latest is `0.2.0`) in the generated `apps/web` and `design-system` packages.
72
+
73
+ - Updated dependencies [7ee992d]
74
+ - @forumone/throughline-plugin-contract@0.2.1
75
+
3
76
  ## 0.2.1
4
77
 
5
78
  ### Patch Changes
package/README.md CHANGED
@@ -12,7 +12,7 @@ The shared plumbing every Throughline server package depends on. Drop it into a
12
12
  | MCP | `./mcp` | `createMcpHandler`, `McpMetaSchema`, `withMeta` |
13
13
  | Env | `./env` | `ENV_VARS`, `validateBaseEnv`, `requireEnv`, `optionalEnv` |
14
14
  | Logger | (main) | `defaultLogger`, `createNamedLogger` |
15
- | Utils | (main) | `shallowDiff`, `generateId` |
15
+ | Utils | (main) | `shallowDiff`, `generateId`, `documentContentHash` |
16
16
 
17
17
  The main entry re-exports everything; the subpath exports keep bundles smaller for consumers who only need one slice.
18
18
 
@@ -116,6 +116,21 @@ validateBaseEnv() // throws on missing PAYLOAD_SECRET / DATABASE_URI / NEXT_PUBL
116
116
  const apiKey = requireEnv(ENV_VARS.PUBLISHING_SERVER_API_KEY)
117
117
  ```
118
118
 
119
+ ## Document content hashing
120
+
121
+ `documentContentHash(document)` reduces a Payload document to a hash of the part an editor authored, ignoring the metadata that moves without the content moving — `id`, `createdAt`, `updatedAt`, `_status`, `__v`, `_id`, `globalType`, stripped at every level of the document. Object key order does not affect the result, because blocks come back out of JSONB in no promised order; array order does, because that is the order of the blocks on the page.
122
+
123
+ ```ts
124
+ import { documentContentHash } from '@forumone/throughline-core'
125
+
126
+ const version = await documentContentHash(page)
127
+ const withExtras = await documentContentHash(page, { exclude: ['syncedAt'] })
128
+ ```
129
+
130
+ It exists so that approvals can bind to *what an approver read* rather than to when it was last saved. Approvals writes it as `targetVersion`; publishing recomputes it at publish time. Two consequences worth stating: a save that changed nothing keeps a granted approval, and an edit that is reverted brings one back.
131
+
132
+ **Two callers only agree if they hash a document loaded the same way.** Both of the above use `payload.findByID({ collection, id, draft: true })` at the config's default depth. A populated relationship and a bare relationship id are different values, and normalising cannot turn one into the other — so a third caller fetching at a different depth would produce a hash that matches nothing.
133
+
119
134
  ## Why all of this lives in one package
120
135
 
121
136
  Server packages (Component, Publishing, Approvals, Audit Query, Forms, Integrations) all depend on the same audit log, the same authentication pattern, and the same event taxonomy. Splitting these across packages would create circular dependencies — every server package would need the audit writer, and an audit-only package would need to know about every server. Consolidating the plumbing here keeps the dependency graph one-way: core → server packages → client app.
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export type { CoreEvents, FrameworkEvents, InngestClientOptions } from './events
9
9
  export { McpMetaSchema, createMcpHandler, withMeta } from './mcp/index.js';
10
10
  export type { McpHandlerOptions, McpMeta } from './mcp/index.js';
11
11
  export { defaultLogger, createNamedLogger } from './logger/index.js';
12
- export { shallowDiff, generateId } from './utils/index.js';
12
+ export { shallowDiff, generateId, documentContentHash } from './utils/index.js';
13
+ export type { DocumentContentHashOptions } from './utils/index.js';
13
14
  export type { AuthenticatedUser, BaseCorePluginOptions, CorePlugin, Logger, McpAuthResult, McpAuthenticator, McpToolContext, McpToolDefinition, PluginRegistry, PluginRegistryEntry, } from '@forumone/throughline-plugin-contract';
14
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACX,qBAAqB,EACrB,iBAAiB,EACjB,cAAc,GACf,MAAM,kBAAkB,CAAA;AACzB,YAAY,EACV,WAAW,EACX,UAAU,EACV,sBAAsB,EACtB,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,WAAW,EACX,kBAAkB,GACnB,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,8BAA8B,EAC9B,cAAc,EACd,SAAS,GACV,MAAM,iBAAiB,CAAA;AACxB,YAAY,EAAE,wBAAwB,EAAE,+BAA+B,EAAE,MAAM,iBAAiB,CAAA;AAEhG,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AACnF,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAEzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AACvD,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAE1F,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAC1E,YAAY,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAEhE,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAEpE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAI1D,YAAY,EACV,iBAAiB,EACjB,qBAAqB,EACrB,UAAU,EACV,MAAM,EACN,aAAa,EACb,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,mBAAmB,GACpB,MAAM,uCAAuC,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACX,qBAAqB,EACrB,iBAAiB,EACjB,cAAc,GACf,MAAM,kBAAkB,CAAA;AACzB,YAAY,EACV,WAAW,EACX,UAAU,EACV,sBAAsB,EACtB,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,WAAW,EACX,kBAAkB,GACnB,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,8BAA8B,EAC9B,cAAc,EACd,SAAS,GACV,MAAM,iBAAiB,CAAA;AACxB,YAAY,EAAE,wBAAwB,EAAE,+BAA+B,EAAE,MAAM,iBAAiB,CAAA;AAEhG,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AACnF,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAEzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AACvD,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAE1F,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAC1E,YAAY,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAEhE,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAEpE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAC/E,YAAY,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAA;AAIlE,YAAY,EACV,iBAAiB,EACjB,qBAAqB,EACrB,UAAU,EACV,MAAM,EACN,aAAa,EACb,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,mBAAmB,GACpB,MAAM,uCAAuC,CAAA"}
package/dist/index.js CHANGED
@@ -6,5 +6,5 @@ export { ENV_VARS, validateBaseEnv, requireEnv, optionalEnv } from './env/index.
6
6
  export { createInngestClient } from './events/index.js';
7
7
  export { McpMetaSchema, createMcpHandler, withMeta } from './mcp/index.js';
8
8
  export { defaultLogger, createNamedLogger } from './logger/index.js';
9
- export { shallowDiff, generateId } from './utils/index.js';
9
+ export { shallowDiff, generateId, documentContentHash } from './utils/index.js';
10
10
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,4DAA4D;AAE5D,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACX,qBAAqB,EACrB,iBAAiB,EACjB,cAAc,GACf,MAAM,kBAAkB,CAAA;AAYzB,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,8BAA8B,EAC9B,cAAc,EACd,SAAS,GACV,MAAM,iBAAiB,CAAA;AAGxB,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAGnF,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AAGvD,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAG1E,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAEpE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,4DAA4D;AAE5D,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACX,qBAAqB,EACrB,iBAAiB,EACjB,cAAc,GACf,MAAM,kBAAkB,CAAA;AAYzB,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,8BAA8B,EAC9B,cAAc,EACd,SAAS,GACV,MAAM,iBAAiB,CAAA;AAGxB,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAGnF,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AAGvD,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAG1E,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAEpE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA"}
@@ -0,0 +1,31 @@
1
+ export interface DocumentContentHashOptions {
2
+ /**
3
+ * Extra field names to treat as volatile, stripped at every level.
4
+ * For app-specific bookkeeping written on save — a sync timestamp, a
5
+ * cached count — that no approver is looking at.
6
+ */
7
+ exclude?: string[];
8
+ }
9
+ /**
10
+ * Hashes the *content* of a document, ignoring the metadata that moves on
11
+ * every save.
12
+ *
13
+ * This is the binding an approval is tied to. Two documents hash the same
14
+ * when an approver would read the same thing, which means:
15
+ *
16
+ * - a save that changed nothing leaves a granted approval standing, and
17
+ * - a save that changed something invalidates it.
18
+ *
19
+ * That is the rule stated deliberately, rather than inherited from
20
+ * whichever timestamp field happened to be nearby.
21
+ *
22
+ * **Both sides must hash a document fetched the same way.** The approvals
23
+ * request tool and publishing's approval step both use
24
+ * `payload.findByID({ collection, id, draft: true })` at the config's
25
+ * default depth, so they see the same shape. A caller that hashes a
26
+ * document fetched at some other depth gets a hash that matches nothing:
27
+ * a populated relationship and a bare relationship id are not the same
28
+ * value, and no amount of normalising makes them one.
29
+ */
30
+ export declare function documentContentHash(document: Record<string, unknown>, options?: DocumentContentHashOptions): Promise<string>;
31
+ //# sourceMappingURL=content-hash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-hash.d.ts","sourceRoot":"","sources":["../../src/utils/content-hash.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,0BAA0B;IACzC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CACnB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,MAAM,CAAC,CAIjB"}
@@ -0,0 +1,94 @@
1
+ import { sha256Hex } from '../auth/api-keys.js';
2
+ /**
3
+ * Fields that move without the content moving. Stripped at every level of
4
+ * the document before hashing.
5
+ *
6
+ * `updatedAt` is the reason this module exists: an approval bound to it is
7
+ * invalidated by any save at all, including one that changed nothing, and
8
+ * including every tick of autosave. `createdAt`, `_status`, `__v`,
9
+ * `globalType` and `_id` are storage and publishing machinery rather than
10
+ * anything an approver read.
11
+ *
12
+ * The document's own `id` is stripped separately, at the top level only —
13
+ * see `normalize`.
14
+ */
15
+ const VOLATILE_FIELDS = new Set(['createdAt', 'updatedAt', '_status', '__v', '_id', 'globalType']);
16
+ /**
17
+ * Hashes the *content* of a document, ignoring the metadata that moves on
18
+ * every save.
19
+ *
20
+ * This is the binding an approval is tied to. Two documents hash the same
21
+ * when an approver would read the same thing, which means:
22
+ *
23
+ * - a save that changed nothing leaves a granted approval standing, and
24
+ * - a save that changed something invalidates it.
25
+ *
26
+ * That is the rule stated deliberately, rather than inherited from
27
+ * whichever timestamp field happened to be nearby.
28
+ *
29
+ * **Both sides must hash a document fetched the same way.** The approvals
30
+ * request tool and publishing's approval step both use
31
+ * `payload.findByID({ collection, id, draft: true })` at the config's
32
+ * default depth, so they see the same shape. A caller that hashes a
33
+ * document fetched at some other depth gets a hash that matches nothing:
34
+ * a populated relationship and a bare relationship id are not the same
35
+ * value, and no amount of normalising makes them one.
36
+ */
37
+ export async function documentContentHash(document, options = {}) {
38
+ const volatile = new Set(VOLATILE_FIELDS);
39
+ for (const field of options.exclude ?? [])
40
+ volatile.add(field);
41
+ return sha256Hex(stableStringify(normalize(document, volatile, true)));
42
+ }
43
+ /**
44
+ * Strips volatile fields and normalises values whose representation can
45
+ * vary without their meaning varying.
46
+ *
47
+ * `id` is dropped only at the top level, where it is the document's own id
48
+ * and constant for the life of the document. Nested `id`s are kept, because
49
+ * that is where a populated relationship carries which document it points
50
+ * at — dropping those would make a page that swapped one image for another
51
+ * with the same alt text hash identically to the one an approver read.
52
+ */
53
+ function normalize(value, volatile, topLevel = false) {
54
+ if (value instanceof Date)
55
+ return value.toISOString();
56
+ if (Array.isArray(value))
57
+ return value.map((entry) => normalize(entry, volatile));
58
+ if (value === null || typeof value !== 'object')
59
+ return value;
60
+ const result = {};
61
+ for (const [key, entry] of Object.entries(value)) {
62
+ if (volatile.has(key))
63
+ continue;
64
+ if (topLevel && key === 'id')
65
+ continue;
66
+ // A field the caller never set and one set to `undefined` are the same
67
+ // document. JSON.stringify already drops these; doing it here keeps the
68
+ // sorted key list honest.
69
+ if (entry === undefined)
70
+ continue;
71
+ result[key] = normalize(entry, volatile);
72
+ }
73
+ return result;
74
+ }
75
+ /**
76
+ * `JSON.stringify` with object keys in sorted order, so that two documents
77
+ * differing only in key order hash the same. Payload reads blocks back out
78
+ * of JSONB, and neither Postgres nor the JSON parser promises to hand them
79
+ * back in the order they went in.
80
+ *
81
+ * Array order is meaningful — it is the order of the blocks on the page —
82
+ * and is preserved.
83
+ */
84
+ function stableStringify(value) {
85
+ if (Array.isArray(value)) {
86
+ return `[${value.map(stableStringify).join(',')}]`;
87
+ }
88
+ if (value !== null && typeof value === 'object') {
89
+ const entries = Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
90
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(',')}}`;
91
+ }
92
+ return JSON.stringify(value) ?? 'null';
93
+ }
94
+ //# sourceMappingURL=content-hash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-hash.js","sourceRoot":"","sources":["../../src/utils/content-hash.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAE/C;;;;;;;;;;;;GAYG;AACH,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC,CAAA;AAWlG;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,QAAiC,EACjC,UAAsC,EAAE;IAExC,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,eAAe,CAAC,CAAA;IACzC,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,OAAO,IAAI,EAAE;QAAE,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;IAC9D,OAAO,SAAS,CAAC,eAAe,CAAC,SAAS,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;AACxE,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,SAAS,CAAC,KAAc,EAAE,QAAqB,EAAE,QAAQ,GAAG,KAAK;IACxE,IAAI,KAAK,YAAY,IAAI;QAAE,OAAO,KAAK,CAAC,WAAW,EAAE,CAAA;IACrD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,SAAS,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAA;IACjF,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAE7D,MAAM,MAAM,GAA4B,EAAE,CAAA;IAC1C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC,EAAE,CAAC;QAC5E,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,SAAQ;QAC/B,IAAI,QAAQ,IAAI,GAAG,KAAK,IAAI;YAAE,SAAQ;QACtC,uEAAuE;QACvE,wEAAwE;QACxE,0BAA0B;QAC1B,IAAI,KAAK,KAAK,SAAS;YAAE,SAAQ;QACjC,MAAM,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAA;IAC1C,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,eAAe,CAAC,KAAc;IACrC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAA;IACpD,CAAC;IACD,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CACjF,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAC3B,CAAA;QACD,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAA;IAC/F,CAAC;IACD,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,MAAM,CAAA;AACxC,CAAC"}
@@ -1,3 +1,5 @@
1
1
  export { shallowDiff } from './diff.js';
2
+ export { documentContentHash } from './content-hash.js';
3
+ export type { DocumentContentHashOptions } from './content-hash.js';
2
4
  export { generateId } from './id.js';
3
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AACvC,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AACvD,YAAY,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAA;AACnE,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA"}
@@ -1,3 +1,4 @@
1
1
  export { shallowDiff } from './diff.js';
2
+ export { documentContentHash } from './content-hash.js';
2
3
  export { generateId } from './id.js';
3
4
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AACvC,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AAEvD,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forumone/throughline-core",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Core plumbing for Throughline: audit log, MCP authentication and handler, event taxonomy + Inngest client factory, env handling, logger, shared utilities.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -62,7 +62,7 @@
62
62
  "dependencies": {
63
63
  "zod": "^3.23.0",
64
64
  "zod-to-json-schema": "^3.25.2",
65
- "@forumone/throughline-plugin-contract": "0.0.0"
65
+ "@forumone/throughline-plugin-contract": "0.2.1"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@types/node": "^20.17.0",