@contentrain/mcp 3.3.3 → 3.4.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/README.md +55 -0
- package/dist/{content-manager-9yUO24W2.d.mts → content-manager-BqaMkMVk.d.mts} +2 -2
- package/dist/{content-manager-9yUO24W2.d.mts.map → content-manager-BqaMkMVk.d.mts.map} +1 -1
- package/dist/core/config.d.mts +1 -1
- package/dist/core/content-manager.d.mts +1 -1
- package/dist/core/context.d.mts +1 -1
- package/dist/core/contracts/index.d.mts +1 -1
- package/dist/core/migration/index.d.mts +122 -0
- package/dist/core/migration/index.d.mts.map +1 -0
- package/dist/core/migration/index.mjs +150 -0
- package/dist/core/migration/index.mjs.map +1 -0
- package/dist/core/model-manager.d.mts +1 -1
- package/dist/core/ops/index.d.mts +3 -3
- package/dist/core/overlay-reader.d.mts +1 -1
- package/dist/core/validator/index.d.mts +1 -1
- package/dist/git/reconcile.d.mts +1 -1
- package/dist/index-A5iw3yDk.d.mts +2 -0
- package/dist/{index-_HJVkDJa.d.mts → index-DKZNUqpP.d.mts} +2 -2
- package/dist/{index-_HJVkDJa.d.mts.map → index-DKZNUqpP.d.mts.map} +1 -1
- package/dist/index.mjs +1 -1
- package/dist/providers/github/index.d.mts +1 -1
- package/dist/providers/gitlab/index.d.mts +1 -1
- package/dist/providers/local/index.d.mts +4 -4
- package/dist/providers/local/index.d.mts.map +1 -1
- package/dist/server/http/index.d.mts +1 -1
- package/dist/server/http/index.mjs +1 -1
- package/dist/{server-SDsD0XGM.d.mts → server-CUtmhlno.d.mts} +2 -2
- package/dist/{server-SDsD0XGM.d.mts.map → server-CUtmhlno.d.mts.map} +1 -1
- package/dist/{server-BgyNDHvp.mjs → server-Do2BcSBg.mjs} +2 -2
- package/dist/{server-BgyNDHvp.mjs.map → server-Do2BcSBg.mjs.map} +1 -1
- package/dist/server.d.mts +1 -1
- package/dist/server.mjs +1 -1
- package/dist/testing/memory-provider.d.mts +1 -1
- package/package.json +8 -4
- package/dist/index-ByrVsRJE.d.mts +0 -2
package/README.md
CHANGED
|
@@ -248,6 +248,7 @@ The package also exposes low-level modules for embedding and advanced use:
|
|
|
248
248
|
- `@contentrain/mcp/core/contracts`
|
|
249
249
|
- `@contentrain/mcp/core/ops` — plan APIs (including `planReconcile` and its `bindRef` reader adapter) plus content-root-relative path helpers: `contentDirPath`, `contentFilePath`, `documentFilePath`, `metaFilePath`
|
|
250
250
|
- `@contentrain/mcp/core/overlay-reader`
|
|
251
|
+
- `@contentrain/mcp/core/migration` — the allowlisted migration write path: `createMigrationWriter`, `scopeHash`, `decide`. See **Migration writes** below
|
|
251
252
|
- `@contentrain/mcp/util/detect`
|
|
252
253
|
- `@contentrain/mcp/util/fs`
|
|
253
254
|
- `@contentrain/mcp/git/transaction`
|
|
@@ -262,6 +263,60 @@ The package also exposes low-level modules for embedding and advanced use:
|
|
|
262
263
|
|
|
263
264
|
These are intended for Contentrain tooling and advanced integrations, not for direct manual editing of `.contentrain/` files.
|
|
264
265
|
|
|
266
|
+
## Migration Writes
|
|
267
|
+
|
|
268
|
+
A migration does not deliver content, it delivers a codebase: `src/`, `public/`,
|
|
269
|
+
`package.json`, a lockfile, deploy configuration. The content write path was
|
|
270
|
+
never built for any of that, and widening it would quietly move a security
|
|
271
|
+
boundary the whole product rests on — "Contentrain only writes `.contentrain/`"
|
|
272
|
+
is a promise, not an implementation detail.
|
|
273
|
+
|
|
274
|
+
`@contentrain/mcp/core/migration` is the gate instead. It wraps a provider whose
|
|
275
|
+
`applyPlan` can already write any path, and puts consent in front of it.
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
import { createMigrationWriter, scopeHash } from '@contentrain/mcp/core/migration'
|
|
279
|
+
|
|
280
|
+
const scope = {
|
|
281
|
+
allow: ['src/**', 'public/**', 'package.json', 'astro.config.mjs'],
|
|
282
|
+
branch: 'migration/acme',
|
|
283
|
+
base: 'main',
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Show the user `scope`, not the hash. Then bind their consent to it:
|
|
287
|
+
const approval = {
|
|
288
|
+
scope_hash: await scopeHash(scope),
|
|
289
|
+
approver: { kind: 'human', id: userId, role: 'owner' },
|
|
290
|
+
approved_at: new Date().toISOString(),
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const writer = createMigrationWriter(provider, scope, approval)
|
|
294
|
+
await writer.applyPlan({ ...plan, step: 'emit', actor })
|
|
295
|
+
writer.audit // which file, in which step, by which actor, in which commit
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
What it guarantees:
|
|
299
|
+
|
|
300
|
+
| | |
|
|
301
|
+
|---|---|
|
|
302
|
+
| **Scope** | Only paths an allowlist pattern covers. `.contentrain/**` is not implicit — a migration that wants the store asks for it, and the person approving sees that it does |
|
|
303
|
+
| **Consent** | One approval, bound to the exact scope by `scope_hash`. An approval of `src/**` cannot be replayed against a scope that also has `.github/workflows/**` |
|
|
304
|
+
| **Branch** | A `migration/*` ref. The `contentrain` branch is never a target and never a base — including through `ApplyPlanInput`'s default, which a migration must not inherit |
|
|
305
|
+
| **Trail** | One `MigrationAuditEntry` per file: step, path, action, the pattern that allowed it, actor, timestamp, commit |
|
|
306
|
+
| **Undo** | One branch, delivered as one PR, revertable with one `git revert` |
|
|
307
|
+
|
|
308
|
+
Every refusal happens before the provider is called, and a plan is refused
|
|
309
|
+
whole: a plan with one path outside the scope writes nothing at all, not even
|
|
310
|
+
its allowed part. A half-applied migration is harder to recover from than one
|
|
311
|
+
that never started.
|
|
312
|
+
|
|
313
|
+
The path matcher is a security control and is written to fail closed. Traversal,
|
|
314
|
+
absolute paths, backslash separators, percent-encoding, empty segments and
|
|
315
|
+
control characters are refused before any pattern is consulted; patterns are
|
|
316
|
+
anchored at both ends, so `src/*` does not cover `src/a/b` and nothing matches
|
|
317
|
+
by being a prefix; and regex metacharacters in a pattern are literal, so an
|
|
318
|
+
allowlist entry cannot silently cover more than it reads.
|
|
319
|
+
|
|
265
320
|
## Design Constraints
|
|
266
321
|
|
|
267
322
|
Key design decisions in this package:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as RepoReader } from "./index-
|
|
1
|
+
import { _ as RepoReader } from "./index-A5iw3yDk.mjs";
|
|
2
2
|
import { ContentrainConfig, LocaleStrategy, ModelDefinition, Vocabulary, parseMarkdownFrontmatter as parseFrontmatter, serializeMarkdownFrontmatter as serializeFrontmatter, validateEntryId as validateEntryId$1, validateLocale as validateLocale$1, validateSlug as validateSlug$1 } from "@contentrain/types";
|
|
3
3
|
|
|
4
4
|
//#region src/core/content-manager.d.ts
|
|
@@ -73,4 +73,4 @@ declare function readContent(projectRoot: string, model: ModelDefinition, opts:
|
|
|
73
73
|
}): Promise<unknown>;
|
|
74
74
|
//#endregion
|
|
75
75
|
export { validateEntryId$1 as _, ListOpts as a, writeContent as b, deleteContent as c, readContent as d, resolveContentDir as f, serializeFrontmatter as g, resolveMdFilePath as h, DeleteOpts as i, listContent as l, resolveLocaleStrategy as m, ContentFileModel as n, LocaleStrategyModel as o, resolveJsonFilePath as p, ContentPathModel as r, WriteResult as s, ContentEntry as t, parseFrontmatter as u, validateLocale$1 as v, validateSlug$1 as y };
|
|
76
|
-
//# sourceMappingURL=content-manager-
|
|
76
|
+
//# sourceMappingURL=content-manager-BqaMkMVk.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content-manager-
|
|
1
|
+
{"version":3,"file":"content-manager-BqaMkMVk.d.mts","names":[],"sources":["../src/core/content-manager.ts"],"mappings":";;;;KAyBY,gBAAA,GAAmB,IAAA,CAAK,eAAA;AAAA,KACxB,mBAAA,GAAsB,IAAA,CAAK,eAAA;AAAA,KAC3B,gBAAA,GAAmB,IAAA,CAAK,eAAA;AAAA,iBAEpB,iBAAA,CAAkB,WAAA,UAAqB,KAAA,EAAO,gBAAA;AAAA,iBAO9C,qBAAA,CAAsB,KAAA,EAAO,mBAAA,GAAsB,cAAA;;iBAKnD,mBAAA,CAAoB,GAAA,UAAa,KAAA,EAAO,gBAAA,EAAkB,MAAA;;iBAc1D,iBAAA,CAAkB,GAAA,UAAa,KAAA,EAAO,IAAA,CAAK,eAAA,+BAA8C,MAAA,UAAgB,IAAA;AAAA,UAexG,YAAA;EACf,EAAA;EACA,IAAA;EACA,MAAA;EACA,IAAA,EAAM,MAAA;EA7CQ;;;;;EAmDd,UAAA;EAnDqD;EAqDrD,SAAA;AAAA;AAAA,UAGe,WAAA;EACf,MAAA;EACA,EAAA;EACA,IAAA;EACA,MAAA;EACA,UAAA;AAAA;AAAA,UAGe,UAAA;EACf,EAAA;EACA,IAAA;EACA,MAAA;EACA,IAAA;EAxDsE;EA0DtE,aAAA;AAAA;AAAA,UAGe,QAAA;EACf,MAAA;EACA,MAAA,GAAS,MAAA;EACT,OAAA;EACA,KAAA;EACA,MAAA;AAAA;AAAA,iBAKoB,YAAA,CACpB,WAAA,UACA,KAAA,EAAO,eAAA,EACP,OAAA,EAAS,YAAA,IACT,MAAA,EAAQ,iBAAA,EACR,UAAA,GAAa,UAAA,UACZ,OAAA,CAAQ,WAAA;AAAA,iBAuJW,aAAA,CACpB,WAAA,UACA,KAAA,EAAO,eAAA,EACP,IAAA,EAAM,UAAA,GACL,OAAA;;;;;;;;AA3MH;;;;;;;;iBAwVgB,WAAA,CACd,WAAA,UACA,KAAA,EAAO,eAAA,EACP,IAAA,EAAM,QAAA,EACN,MAAA,EAAQ,iBAAA,GACP,OAAA;AAAA,iBACa,WAAA,CACd,MAAA,EAAQ,UAAA,EACR,KAAA,EAAO,eAAA,EACP,IAAA,EAAM,QAAA,EACN,MAAA,EAAQ,iBAAA,GACP,OAAA;AAAA,iBA0QmB,WAAA,CACpB,WAAA,UACA,KAAA,EAAO,eAAA,EACP,IAAA;EAAQ,MAAA;EAAgB,OAAA;EAAkB,IAAA;AAAA,IACzC,OAAA"}
|
package/dist/core/config.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as validateEntryId, a as ListOpts, b as writeContent, c as deleteContent, d as readContent, f as resolveContentDir, g as serializeFrontmatter, h as resolveMdFilePath, i as DeleteOpts, l as listContent, m as resolveLocaleStrategy, n as ContentFileModel, o as LocaleStrategyModel, p as resolveJsonFilePath, r as ContentPathModel, s as WriteResult, t as ContentEntry, u as parseFrontmatter, v as validateLocale, y as validateSlug } from "../content-manager-
|
|
1
|
+
import { _ as validateEntryId, a as ListOpts, b as writeContent, c as deleteContent, d as readContent, f as resolveContentDir, g as serializeFrontmatter, h as resolveMdFilePath, i as DeleteOpts, l as listContent, m as resolveLocaleStrategy, n as ContentFileModel, o as LocaleStrategyModel, p as resolveJsonFilePath, r as ContentPathModel, s as WriteResult, t as ContentEntry, u as parseFrontmatter, v as validateLocale, y as validateSlug } from "../content-manager-BqaMkMVk.mjs";
|
|
2
2
|
export { ContentEntry, ContentFileModel, ContentPathModel, DeleteOpts, ListOpts, LocaleStrategyModel, WriteResult, deleteContent, listContent, parseFrontmatter, readContent, resolveContentDir, resolveJsonFilePath, resolveLocaleStrategy, resolveMdFilePath, serializeFrontmatter, validateEntryId, validateLocale, validateSlug, writeContent };
|
package/dist/core/context.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as RepoReader, a as FileChange, c as MediaAsset, d as MediaListResult, f as MediaProvider, g as RepoProvider, h as ProviderCapabilities, i as CommitAuthor, l as MediaIngestInput, m as MergeResult, n as Branch, o as FileDiff, p as MediaUpdateInput, r as Commit, s as LOCAL_CAPABILITIES, t as ApplyPlanInput, u as MediaListOptions, v as RepoWriter, y as WriteReadiness } from "../../index-
|
|
1
|
+
import { _ as RepoReader, a as FileChange, c as MediaAsset, d as MediaListResult, f as MediaProvider, g as RepoProvider, h as ProviderCapabilities, i as CommitAuthor, l as MediaIngestInput, m as MergeResult, n as Branch, o as FileDiff, p as MediaUpdateInput, r as Commit, s as LOCAL_CAPABILITIES, t as ApplyPlanInput, u as MediaListOptions, v as RepoWriter, y as WriteReadiness } from "../../index-A5iw3yDk.mjs";
|
|
2
2
|
export { ApplyPlanInput, Branch, Commit, CommitAuthor, FileChange, FileDiff, LOCAL_CAPABILITIES, MediaAsset, MediaIngestInput, MediaListOptions, MediaListResult, MediaProvider, MediaUpdateInput, MergeResult, ProviderCapabilities, RepoProvider, RepoReader, RepoWriter, WriteReadiness };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { ActorRef, ApplyPlanInput, Commit, RepoProvider, RepoWriter } from "@contentrain/types";
|
|
2
|
+
|
|
3
|
+
//#region src/core/migration/allowlist.d.ts
|
|
4
|
+
/** A path that is safe to compare against patterns, or undefined. */
|
|
5
|
+
declare function normalizePath(path: string): string | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* Match one normalised path against one pattern.
|
|
8
|
+
*
|
|
9
|
+
* `*` matches within a segment, `**` spans segments. A pattern is anchored at
|
|
10
|
+
* both ends — `src/*` does not cover `src/a/b`, and nothing covers a path by
|
|
11
|
+
* being a prefix of it. That strictness is the point: an allowlist is read by a
|
|
12
|
+
* person deciding whether to approve it, so a pattern has to mean what it looks
|
|
13
|
+
* like.
|
|
14
|
+
*/
|
|
15
|
+
declare function matchesPattern(path: string, pattern: string): boolean;
|
|
16
|
+
interface AllowDecision {
|
|
17
|
+
allowed: boolean;
|
|
18
|
+
/** The pattern that permitted the path, when one did. */
|
|
19
|
+
pattern?: string;
|
|
20
|
+
/** Why the path was refused. */
|
|
21
|
+
reason?: 'malformed' | 'not-allowlisted';
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Whether a write to `path` is permitted by `patterns`.
|
|
25
|
+
*
|
|
26
|
+
* `.contentrain/**` is NOT implicit. The content engine's own writes go through
|
|
27
|
+
* the content path with its own invariants; a migration that also wants to
|
|
28
|
+
* write the store asks for it explicitly, and the person approving sees that it
|
|
29
|
+
* does.
|
|
30
|
+
*/
|
|
31
|
+
declare function decide(path: string, patterns: readonly string[]): AllowDecision;
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/core/migration/writer.d.ts
|
|
34
|
+
/** Branch names a migration may write to. */
|
|
35
|
+
declare const MIGRATION_BRANCH_PREFIX = "migration/";
|
|
36
|
+
interface MigrationScope {
|
|
37
|
+
/**
|
|
38
|
+
* Path patterns this migration may write. `*` matches within a segment,
|
|
39
|
+
* `**` spans segments. Nothing is implicit — not even `.contentrain/**`.
|
|
40
|
+
*/
|
|
41
|
+
allow: string[];
|
|
42
|
+
/** The branch every write lands on. Must start with `migration/`. */
|
|
43
|
+
branch: string;
|
|
44
|
+
/** Optional base to fork from. Must not be the content branch. */
|
|
45
|
+
base?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A user's consent, bound to the exact scope they were shown.
|
|
49
|
+
*
|
|
50
|
+
* `scope_hash` is what makes the consent meaningful: an approval of "may write
|
|
51
|
+
* `src/**` on `migration/acme`" cannot be replayed against a scope that also
|
|
52
|
+
* includes `.github/workflows/**`. Compute it with {@link scopeHash} and show
|
|
53
|
+
* the user the scope, not the hash.
|
|
54
|
+
*/
|
|
55
|
+
interface MigrationApproval {
|
|
56
|
+
scope_hash: string;
|
|
57
|
+
approver: ActorRef;
|
|
58
|
+
/** ISO 8601 UTC. */
|
|
59
|
+
approved_at: string;
|
|
60
|
+
/** ISO 8601 UTC; consent past this instant is not consent. */
|
|
61
|
+
expires_at?: string;
|
|
62
|
+
}
|
|
63
|
+
/** One write, as it happened. The audit trail is a list of these. */
|
|
64
|
+
interface MigrationAuditEntry {
|
|
65
|
+
/** The step of the migration that produced the write. */
|
|
66
|
+
step: string;
|
|
67
|
+
path: string;
|
|
68
|
+
action: 'write' | 'delete';
|
|
69
|
+
/** The allowlist pattern that permitted it. */
|
|
70
|
+
pattern: string;
|
|
71
|
+
actor: ActorRef;
|
|
72
|
+
/** ISO 8601 UTC. */
|
|
73
|
+
at: string;
|
|
74
|
+
/** The commit the write landed in. */
|
|
75
|
+
commit: string;
|
|
76
|
+
branch: string;
|
|
77
|
+
}
|
|
78
|
+
interface MigrationWriteInput extends ApplyPlanInput {
|
|
79
|
+
/** Names the migration step, for the audit trail. */
|
|
80
|
+
step: string;
|
|
81
|
+
/** Who is writing. Recorded on every entry. */
|
|
82
|
+
actor: ActorRef;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The wrapped writer. `applyPlan` asks for `step` and `actor` because an audit
|
|
86
|
+
* trail without them answers "what changed" but not "who did this, and when" —
|
|
87
|
+
* which is the question a trail exists for. It still satisfies `RepoWriter`, so
|
|
88
|
+
* anything that takes a provider's writer takes this.
|
|
89
|
+
*/
|
|
90
|
+
interface MigrationWriter extends RepoWriter {
|
|
91
|
+
applyPlan: (input: MigrationWriteInput) => Promise<Commit>;
|
|
92
|
+
readonly audit: readonly MigrationAuditEntry[];
|
|
93
|
+
}
|
|
94
|
+
declare class MigrationWriteError extends Error {
|
|
95
|
+
readonly code: 'scope_not_approved' | 'approval_expired' | 'branch_not_migration' | 'content_branch_target' | 'path_not_allowed';
|
|
96
|
+
/** The paths that caused a `path_not_allowed` refusal. */
|
|
97
|
+
readonly paths?: string[] | undefined;
|
|
98
|
+
constructor(message: string, code: 'scope_not_approved' | 'approval_expired' | 'branch_not_migration' | 'content_branch_target' | 'path_not_allowed', /** The paths that caused a `path_not_allowed` refusal. */
|
|
99
|
+
|
|
100
|
+
paths?: string[] | undefined);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The identity of a scope: the sorted allowlist, the branch and the base.
|
|
104
|
+
*
|
|
105
|
+
* Sorted so that two callers listing the same patterns in a different order get
|
|
106
|
+
* one hash — the user approved a set of permissions, not an ordering. SHA-256
|
|
107
|
+
* over canonical JSON, matching `computePlanHash` in `@contentrain/types`, so a
|
|
108
|
+
* consumer can recompute it from the published contract.
|
|
109
|
+
*/
|
|
110
|
+
declare function scopeHash(scope: MigrationScope): Promise<string>;
|
|
111
|
+
/**
|
|
112
|
+
* Wrap a provider so it will only write a migration's approved scope.
|
|
113
|
+
*
|
|
114
|
+
* The returned writer refuses — before touching the provider — a branch that is
|
|
115
|
+
* not the approved one, any target or base that is the content branch, an
|
|
116
|
+
* approval that does not match the scope or has expired, and any change whose
|
|
117
|
+
* path no pattern covers.
|
|
118
|
+
*/
|
|
119
|
+
declare function createMigrationWriter(provider: RepoProvider, scope: MigrationScope, approval: MigrationApproval, audit?: MigrationAuditEntry[]): MigrationWriter;
|
|
120
|
+
//#endregion
|
|
121
|
+
export { type AllowDecision, MIGRATION_BRANCH_PREFIX, type MigrationApproval, type MigrationAuditEntry, type MigrationScope, MigrationWriteError, type MigrationWriteInput, type MigrationWriter, createMigrationWriter, decide, matchesPattern, normalizePath, scopeHash };
|
|
122
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/core/migration/allowlist.ts","../../../src/core/migration/writer.ts"],"mappings":";;;;iBA4BgB,aAAA,CAAc,IAAA;;AAA9B;;;;;AAyBA;;;iBAAgB,cAAA,CAAe,IAAA,UAAc,OAAA;AAAA,UAiB5B,aAAA;EACf,OAAA;EAD4B;EAG5B,OAAA;EAH4B;EAK5B,MAAA;AAAA;;;;AAWF;;;;;iBAAgB,MAAA,CAAO,IAAA,UAAc,QAAA,sBAA8B,aAAA;;;;cCrDtD,uBAAA;AAAA,UAEI,cAAA;EDPY;;;;ECY3B,KAAA;EDa4B;ECX5B,MAAA;EDW6B;ECT7B,IAAA;AAAA;;;;;;;;;UAWe,iBAAA;EACf,UAAA;EACA,QAAA,EAAU,QAAA;ED6BoE;EC3B9E,WAAA;ED2BmC;ECzBnC,UAAA;AAAA;;UAIe,mBAAA;;EAEf,IAAA;EACA,IAAA;EACA,MAAA;;EAEA,OAAA;EACA,KAAA,EAAO,QAAA;EArCQ;EAuCf,EAAA;;EAEA,MAAA;EACA,MAAA;AAAA;AAAA,UAGe,mBAAA,SAA4B,cAAA;EApCvC;EAsCJ,IAAA;EA3Be;EA6Bf,KAAA,EAAO,QAAA;AAAA;;;;;;;UASQ,eAAA,SAAwB,UAAA;EACvC,SAAA,GAAY,KAAA,EAAO,mBAAA,KAAwB,OAAA,CAAQ,MAAA;EAAA,SAC1C,KAAA,WAAgB,mBAAA;AAAA;AAAA,cAGd,mBAAA,SAA4B,KAAA;EAAA,SAG5B,IAAA;EAlCX;EAAA,SAyCW,KAAA;cART,OAAA,UACS,IAAA,qHA9BX;;EAqCW,KAAA;AAAA;;;;;;AA5Bb;;;iBA2CsB,SAAA,CAAU,KAAA,EAAO,cAAA,GAAiB,OAAA;;;;;;;AA9BxD;;iBAoDgB,qBAAA,CACd,QAAA,EAAU,YAAA,EACV,KAAA,EAAO,cAAA,EACP,QAAA,EAAU,iBAAA,EACV,KAAA,GAAO,mBAAA,KACN,eAAA"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { CONTENTRAIN_BRANCH, canonicalStringify } from "@contentrain/types";
|
|
2
|
+
//#region src/core/migration/allowlist.ts
|
|
3
|
+
/** Placeholders used while compiling a glob; neither can occur in a path. */
|
|
4
|
+
const GLOBSTAR_SLASH = "\0GS\0";
|
|
5
|
+
const GLOBSTAR = "\0G\0";
|
|
6
|
+
const CONTROL_CHAR_MAX = 31;
|
|
7
|
+
function hasControlCharacter(value) {
|
|
8
|
+
for (let i = 0; i < value.length; i += 1) if (value.charCodeAt(i) <= CONTROL_CHAR_MAX) return true;
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
/** A path that is safe to compare against patterns, or undefined. */
|
|
12
|
+
function normalizePath(path) {
|
|
13
|
+
if (path === "" || path.length > 4096) return void 0;
|
|
14
|
+
if (hasControlCharacter(path)) return void 0;
|
|
15
|
+
if (path.includes("\\")) return void 0;
|
|
16
|
+
if (path.includes("%")) return void 0;
|
|
17
|
+
if (path.startsWith("/")) return void 0;
|
|
18
|
+
const segments = path.split("/");
|
|
19
|
+
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) return void 0;
|
|
20
|
+
return segments.join("/");
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Match one normalised path against one pattern.
|
|
24
|
+
*
|
|
25
|
+
* `*` matches within a segment, `**` spans segments. A pattern is anchored at
|
|
26
|
+
* both ends — `src/*` does not cover `src/a/b`, and nothing covers a path by
|
|
27
|
+
* being a prefix of it. That strictness is the point: an allowlist is read by a
|
|
28
|
+
* person deciding whether to approve it, so a pattern has to mean what it looks
|
|
29
|
+
* like.
|
|
30
|
+
*/
|
|
31
|
+
function matchesPattern(path, pattern) {
|
|
32
|
+
if (normalizePath(pattern.replaceAll("*", "x")) === void 0) return false;
|
|
33
|
+
const escaped = pattern.replace(/[.+^${}()|[\]]/g, "\\$&").replaceAll("**/", GLOBSTAR_SLASH).replaceAll("**", GLOBSTAR).replaceAll("*", "[^/]*").replaceAll(GLOBSTAR_SLASH, "(?:[^/]+/)*").replaceAll(GLOBSTAR, ".*");
|
|
34
|
+
return new RegExp(`^${escaped}$`).test(path);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Whether a write to `path` is permitted by `patterns`.
|
|
38
|
+
*
|
|
39
|
+
* `.contentrain/**` is NOT implicit. The content engine's own writes go through
|
|
40
|
+
* the content path with its own invariants; a migration that also wants to
|
|
41
|
+
* write the store asks for it explicitly, and the person approving sees that it
|
|
42
|
+
* does.
|
|
43
|
+
*/
|
|
44
|
+
function decide(path, patterns) {
|
|
45
|
+
const normalized = normalizePath(path);
|
|
46
|
+
if (normalized === void 0) return {
|
|
47
|
+
allowed: false,
|
|
48
|
+
reason: "malformed"
|
|
49
|
+
};
|
|
50
|
+
for (const pattern of patterns) if (matchesPattern(normalized, pattern)) return {
|
|
51
|
+
allowed: true,
|
|
52
|
+
pattern
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
allowed: false,
|
|
56
|
+
reason: "not-allowlisted"
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/core/migration/writer.ts
|
|
61
|
+
/** Branch names a migration may write to. */
|
|
62
|
+
const MIGRATION_BRANCH_PREFIX = "migration/";
|
|
63
|
+
var MigrationWriteError = class extends Error {
|
|
64
|
+
constructor(message, code, paths) {
|
|
65
|
+
super(message);
|
|
66
|
+
this.code = code;
|
|
67
|
+
this.paths = paths;
|
|
68
|
+
this.name = "MigrationWriteError";
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* The identity of a scope: the sorted allowlist, the branch and the base.
|
|
73
|
+
*
|
|
74
|
+
* Sorted so that two callers listing the same patterns in a different order get
|
|
75
|
+
* one hash — the user approved a set of permissions, not an ordering. SHA-256
|
|
76
|
+
* over canonical JSON, matching `computePlanHash` in `@contentrain/types`, so a
|
|
77
|
+
* consumer can recompute it from the published contract.
|
|
78
|
+
*/
|
|
79
|
+
async function scopeHash(scope) {
|
|
80
|
+
const payload = canonicalStringify({
|
|
81
|
+
allow: [...new Set(scope.allow)].toSorted(),
|
|
82
|
+
branch: scope.branch,
|
|
83
|
+
base: scope.base
|
|
84
|
+
});
|
|
85
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(payload));
|
|
86
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
87
|
+
}
|
|
88
|
+
function isContentBranch(name) {
|
|
89
|
+
return name === CONTENTRAIN_BRANCH;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Wrap a provider so it will only write a migration's approved scope.
|
|
93
|
+
*
|
|
94
|
+
* The returned writer refuses — before touching the provider — a branch that is
|
|
95
|
+
* not the approved one, any target or base that is the content branch, an
|
|
96
|
+
* approval that does not match the scope or has expired, and any change whose
|
|
97
|
+
* path no pattern covers.
|
|
98
|
+
*/
|
|
99
|
+
function createMigrationWriter(provider, scope, approval, audit = []) {
|
|
100
|
+
let verified;
|
|
101
|
+
const assertScope = async (now) => {
|
|
102
|
+
verified ??= await scopeHash(scope);
|
|
103
|
+
if (approval.scope_hash !== verified) throw new MigrationWriteError("The approval was given for a different scope. Show the user the current allowlist and branch, and collect consent again.", "scope_not_approved");
|
|
104
|
+
if (approval.expires_at && approval.expires_at <= now) throw new MigrationWriteError(`The approval expired at ${approval.expires_at}.`, "approval_expired");
|
|
105
|
+
if (!scope.branch.startsWith("migration/")) throw new MigrationWriteError(`A migration writes to "${MIGRATION_BRANCH_PREFIX}*"; "${scope.branch}" is not one.`, "branch_not_migration");
|
|
106
|
+
if (isContentBranch(scope.branch) || isContentBranch(scope.base)) throw new MigrationWriteError(`A migration never targets or forks the "${CONTENTRAIN_BRANCH}" branch — that ref is the content state's single source of truth.`, "content_branch_target");
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
get audit() {
|
|
110
|
+
return audit;
|
|
111
|
+
},
|
|
112
|
+
async applyPlan(input) {
|
|
113
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
114
|
+
await assertScope(now);
|
|
115
|
+
if (input.branch !== scope.branch) throw new MigrationWriteError(`The approval covers "${scope.branch}"; this write targets "${input.branch}".`, "branch_not_migration");
|
|
116
|
+
if (isContentBranch(input.branch) || isContentBranch(input.base)) throw new MigrationWriteError(`A migration never targets or forks the "${CONTENTRAIN_BRANCH}" branch.`, "content_branch_target");
|
|
117
|
+
const decisions = input.changes.map((change) => ({
|
|
118
|
+
change,
|
|
119
|
+
decision: decide(change.path, scope.allow)
|
|
120
|
+
}));
|
|
121
|
+
const refused = decisions.filter((entry) => !entry.decision.allowed);
|
|
122
|
+
if (refused.length) {
|
|
123
|
+
const paths = refused.map((entry) => entry.change.path);
|
|
124
|
+
throw new MigrationWriteError(`${paths.length} path${paths.length === 1 ? "" : "s"} outside the approved scope: ${paths.slice(0, 5).join(", ")}${paths.length > 5 ? ", …" : ""}`, "path_not_allowed", paths);
|
|
125
|
+
}
|
|
126
|
+
const commit = await provider.applyPlan({
|
|
127
|
+
...input,
|
|
128
|
+
base: input.base ?? scope.base
|
|
129
|
+
});
|
|
130
|
+
for (const { change, decision } of decisions) audit.push({
|
|
131
|
+
step: input.step ?? "unnamed",
|
|
132
|
+
path: change.path,
|
|
133
|
+
action: change.content === null ? "delete" : "write",
|
|
134
|
+
pattern: decision.pattern,
|
|
135
|
+
actor: input.actor ?? {
|
|
136
|
+
kind: "system",
|
|
137
|
+
id: "migration"
|
|
138
|
+
},
|
|
139
|
+
at: now,
|
|
140
|
+
commit: commit.sha,
|
|
141
|
+
branch: input.branch
|
|
142
|
+
});
|
|
143
|
+
return commit;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
export { MIGRATION_BRANCH_PREFIX, MigrationWriteError, createMigrationWriter, decide, matchesPattern, normalizePath, scopeHash };
|
|
149
|
+
|
|
150
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/core/migration/allowlist.ts","../../../src/core/migration/writer.ts"],"sourcesContent":["// ─── Path allowlisting ───\n//\n// The security boundary for migration writes. A migration writes `src/`,\n// `public/`, `package.json`, a lockfile and deploy configuration — everything\n// the content engine deliberately never touches — so the guarantee cannot come\n// from \"only `.contentrain/`\" any more. It comes from a list the user saw and\n// approved.\n//\n// Which makes this matcher a security control, not a convenience. It is written\n// to fail closed: anything it cannot confidently normalise is denied, and the\n// tricks that defeat naive matchers — traversal, absolute paths, backslashes,\n// percent-encoded separators, a leading `./` — are rejected before a pattern is\n// even consulted.\n\n/** Placeholders used while compiling a glob; neither can occur in a path. */\nconst GLOBSTAR_SLASH = '\u0000GS\u0000'\nconst GLOBSTAR = '\u0000G\u0000'\n\nconst CONTROL_CHAR_MAX = 0x1f\n\nfunction hasControlCharacter(value: string): boolean {\n for (let i = 0; i < value.length; i += 1) {\n if (value.charCodeAt(i) <= CONTROL_CHAR_MAX) return true\n }\n return false\n}\n\n/** A path that is safe to compare against patterns, or undefined. */\nexport function normalizePath(path: string): string | undefined {\n if (path === '' || path.length > 4096) return undefined\n // Control characters, including the NUL that truncates a C-side path.\n if (hasControlCharacter(path)) return undefined\n // A backslash is a separator on Windows and a literal elsewhere; treating it\n // as either makes the two platforms disagree about what a pattern covers.\n if (path.includes('\\\\')) return undefined\n // `%2e%2e` and friends: this layer compares raw repository paths, so a path\n // arriving percent-encoded is either a bug or an attempt.\n if (path.includes('%')) return undefined\n if (path.startsWith('/')) return undefined\n const segments = path.split('/')\n if (segments.some(segment => segment === '' || segment === '.' || segment === '..')) return undefined\n return segments.join('/')\n}\n\n/**\n * Match one normalised path against one pattern.\n *\n * `*` matches within a segment, `**` spans segments. A pattern is anchored at\n * both ends — `src/*` does not cover `src/a/b`, and nothing covers a path by\n * being a prefix of it. That strictness is the point: an allowlist is read by a\n * person deciding whether to approve it, so a pattern has to mean what it looks\n * like.\n */\nexport function matchesPattern(path: string, pattern: string): boolean {\n // A pattern is held to the same shape as a path, with `*` standing in for a\n // name. A pattern that could not be a path cannot describe one.\n if (normalizePath(pattern.replaceAll('*', 'x')) === undefined) return false\n\n const escaped = pattern\n .replace(/[.+^${}()|[\\]]/g, '\\\\$&')\n // `**/` may also match nothing, so `src/**/x.ts` covers `src/x.ts`.\n .replaceAll('**/', GLOBSTAR_SLASH)\n .replaceAll('**', GLOBSTAR)\n .replaceAll('*', '[^/]*')\n .replaceAll(GLOBSTAR_SLASH, '(?:[^/]+/)*')\n .replaceAll(GLOBSTAR, '.*')\n\n return new RegExp(`^${escaped}$`).test(path)\n}\n\nexport interface AllowDecision {\n allowed: boolean\n /** The pattern that permitted the path, when one did. */\n pattern?: string\n /** Why the path was refused. */\n reason?: 'malformed' | 'not-allowlisted'\n}\n\n/**\n * Whether a write to `path` is permitted by `patterns`.\n *\n * `.contentrain/**` is NOT implicit. The content engine's own writes go through\n * the content path with its own invariants; a migration that also wants to\n * write the store asks for it explicitly, and the person approving sees that it\n * does.\n */\nexport function decide(path: string, patterns: readonly string[]): AllowDecision {\n const normalized = normalizePath(path)\n if (normalized === undefined) return { allowed: false, reason: 'malformed' }\n for (const pattern of patterns) {\n if (matchesPattern(normalized, pattern)) return { allowed: true, pattern }\n }\n return { allowed: false, reason: 'not-allowlisted' }\n}\n","// ─── The allowlisted migration write path ───\n//\n// A migration does not deliver content, it delivers a codebase: `src/`,\n// `public/`, `package.json`, a lockfile, deploy configuration. None of that is\n// what the content engine's write path was built for, and widening that path\n// would quietly move a security boundary the whole product rests on.\n//\n// So this is a wrapper, not a provider. `GitHubProvider.applyPlan` can already\n// write any path; what was missing was a gate in front of it. The gate carries\n// every requirement the repository-boundary contract asks for:\n//\n// scope a file allowlist; nothing outside it is written\n// consent one explicit approval, bound to the exact scope it was shown\n// branch a `migration/*` ref; the content branch is never a target or base\n// trail which file, in which step, by which actor, in which commit\n// undo one branch, delivered as one PR, revertable in one `git revert`\n//\n// Every refusal happens before the provider is called. A rejected plan writes\n// nothing at all — not the allowed part of it — because a half-applied\n// migration is harder to reason about than one that did not start.\n\nimport type {\n ActorRef,\n ApplyPlanInput,\n Commit,\n FileChange,\n RepoProvider,\n RepoWriter,\n} from '@contentrain/types'\nimport { CONTENTRAIN_BRANCH, canonicalStringify } from '@contentrain/types'\nimport { decide } from './allowlist.js'\n\n/** Branch names a migration may write to. */\nexport const MIGRATION_BRANCH_PREFIX = 'migration/'\n\nexport interface MigrationScope {\n /**\n * Path patterns this migration may write. `*` matches within a segment,\n * `**` spans segments. Nothing is implicit — not even `.contentrain/**`.\n */\n allow: string[]\n /** The branch every write lands on. Must start with `migration/`. */\n branch: string\n /** Optional base to fork from. Must not be the content branch. */\n base?: string\n}\n\n/**\n * A user's consent, bound to the exact scope they were shown.\n *\n * `scope_hash` is what makes the consent meaningful: an approval of \"may write\n * `src/**` on `migration/acme`\" cannot be replayed against a scope that also\n * includes `.github/workflows/**`. Compute it with {@link scopeHash} and show\n * the user the scope, not the hash.\n */\nexport interface MigrationApproval {\n scope_hash: string\n approver: ActorRef\n /** ISO 8601 UTC. */\n approved_at: string\n /** ISO 8601 UTC; consent past this instant is not consent. */\n expires_at?: string\n}\n\n/** One write, as it happened. The audit trail is a list of these. */\nexport interface MigrationAuditEntry {\n /** The step of the migration that produced the write. */\n step: string\n path: string\n action: 'write' | 'delete'\n /** The allowlist pattern that permitted it. */\n pattern: string\n actor: ActorRef\n /** ISO 8601 UTC. */\n at: string\n /** The commit the write landed in. */\n commit: string\n branch: string\n}\n\nexport interface MigrationWriteInput extends ApplyPlanInput {\n /** Names the migration step, for the audit trail. */\n step: string\n /** Who is writing. Recorded on every entry. */\n actor: ActorRef\n}\n\n/**\n * The wrapped writer. `applyPlan` asks for `step` and `actor` because an audit\n * trail without them answers \"what changed\" but not \"who did this, and when\" —\n * which is the question a trail exists for. It still satisfies `RepoWriter`, so\n * anything that takes a provider's writer takes this.\n */\nexport interface MigrationWriter extends RepoWriter {\n applyPlan: (input: MigrationWriteInput) => Promise<Commit>\n readonly audit: readonly MigrationAuditEntry[]\n}\n\nexport class MigrationWriteError extends Error {\n constructor(\n message: string,\n readonly code:\n | 'scope_not_approved'\n | 'approval_expired'\n | 'branch_not_migration'\n | 'content_branch_target'\n | 'path_not_allowed',\n /** The paths that caused a `path_not_allowed` refusal. */\n readonly paths?: string[],\n ) {\n super(message)\n this.name = 'MigrationWriteError'\n }\n}\n\n/**\n * The identity of a scope: the sorted allowlist, the branch and the base.\n *\n * Sorted so that two callers listing the same patterns in a different order get\n * one hash — the user approved a set of permissions, not an ordering. SHA-256\n * over canonical JSON, matching `computePlanHash` in `@contentrain/types`, so a\n * consumer can recompute it from the published contract.\n */\nexport async function scopeHash(scope: MigrationScope): Promise<string> {\n const payload = canonicalStringify({\n allow: [...new Set(scope.allow)].toSorted(),\n branch: scope.branch,\n base: scope.base,\n })\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(payload))\n return [...new Uint8Array(digest)].map(byte => byte.toString(16).padStart(2, '0')).join('')\n}\n\nfunction isContentBranch(name: string | undefined): boolean {\n return name === CONTENTRAIN_BRANCH\n}\n\n/**\n * Wrap a provider so it will only write a migration's approved scope.\n *\n * The returned writer refuses — before touching the provider — a branch that is\n * not the approved one, any target or base that is the content branch, an\n * approval that does not match the scope or has expired, and any change whose\n * path no pattern covers.\n */\nexport function createMigrationWriter(\n provider: RepoProvider,\n scope: MigrationScope,\n approval: MigrationApproval,\n audit: MigrationAuditEntry[] = [],\n): MigrationWriter {\n let verified: string | undefined\n\n const assertScope = async (now: string): Promise<void> => {\n verified ??= await scopeHash(scope)\n if (approval.scope_hash !== verified) {\n throw new MigrationWriteError(\n 'The approval was given for a different scope. Show the user the current allowlist and branch, and collect consent again.',\n 'scope_not_approved',\n )\n }\n if (approval.expires_at && approval.expires_at <= now) {\n throw new MigrationWriteError(`The approval expired at ${approval.expires_at}.`, 'approval_expired')\n }\n if (!scope.branch.startsWith(MIGRATION_BRANCH_PREFIX)) {\n throw new MigrationWriteError(\n `A migration writes to \"${MIGRATION_BRANCH_PREFIX}*\"; \"${scope.branch}\" is not one.`,\n 'branch_not_migration',\n )\n }\n if (isContentBranch(scope.branch) || isContentBranch(scope.base)) {\n throw new MigrationWriteError(\n `A migration never targets or forks the \"${CONTENTRAIN_BRANCH}\" branch — that ref is the content state's single source of truth.`,\n 'content_branch_target',\n )\n }\n }\n\n return {\n get audit() {\n return audit\n },\n\n async applyPlan(input: MigrationWriteInput): Promise<Commit> {\n const now = new Date().toISOString()\n await assertScope(now)\n\n if (input.branch !== scope.branch) {\n throw new MigrationWriteError(\n `The approval covers \"${scope.branch}\"; this write targets \"${input.branch}\".`,\n 'branch_not_migration',\n )\n }\n if (isContentBranch(input.branch) || isContentBranch(input.base)) {\n throw new MigrationWriteError(\n `A migration never targets or forks the \"${CONTENTRAIN_BRANCH}\" branch.`,\n 'content_branch_target',\n )\n }\n\n // Decide every path before writing any of them. A plan that is partly\n // outside the scope is refused whole: a migration that wrote half its\n // files is worse to recover from than one that wrote none.\n const decisions = input.changes.map((change: FileChange) => ({ change, decision: decide(change.path, scope.allow) }))\n const refused = decisions.filter(entry => !entry.decision.allowed)\n if (refused.length) {\n const paths = refused.map(entry => entry.change.path)\n throw new MigrationWriteError(\n `${paths.length} path${paths.length === 1 ? '' : 's'} outside the approved scope: ${paths.slice(0, 5).join(', ')}${paths.length > 5 ? ', …' : ''}`,\n 'path_not_allowed',\n paths,\n )\n }\n\n // `base` defaults to the content branch on `ApplyPlanInput`, which is\n // exactly what a migration must not fork. Make the intent explicit rather\n // than inheriting an invariant written for content.\n const commit = await provider.applyPlan({ ...input, base: input.base ?? scope.base })\n\n for (const { change, decision } of decisions) {\n audit.push({\n step: input.step ?? 'unnamed',\n path: change.path,\n action: change.content === null ? 'delete' : 'write',\n pattern: decision.pattern!,\n actor: input.actor ?? { kind: 'system', id: 'migration' },\n at: now,\n commit: commit.sha,\n branch: input.branch,\n })\n }\n\n return commit\n },\n }\n}\n"],"mappings":";;;AAeA,MAAM,iBAAiB;AACvB,MAAM,WAAW;AAEjB,MAAM,mBAAmB;AAEzB,SAAS,oBAAoB,OAAwB;AACnD,MAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,EACrC,KAAI,MAAM,WAAW,EAAE,IAAI,iBAAkB,QAAO;AAEtD,QAAO;;;AAIT,SAAgB,cAAc,MAAkC;AAC9D,KAAI,SAAS,MAAM,KAAK,SAAS,KAAM,QAAO,KAAA;AAE9C,KAAI,oBAAoB,KAAK,CAAE,QAAO,KAAA;AAGtC,KAAI,KAAK,SAAS,KAAK,CAAE,QAAO,KAAA;AAGhC,KAAI,KAAK,SAAS,IAAI,CAAE,QAAO,KAAA;AAC/B,KAAI,KAAK,WAAW,IAAI,CAAE,QAAO,KAAA;CACjC,MAAM,WAAW,KAAK,MAAM,IAAI;AAChC,KAAI,SAAS,MAAK,YAAW,YAAY,MAAM,YAAY,OAAO,YAAY,KAAK,CAAE,QAAO,KAAA;AAC5F,QAAO,SAAS,KAAK,IAAI;;;;;;;;;;;AAY3B,SAAgB,eAAe,MAAc,SAA0B;AAGrE,KAAI,cAAc,QAAQ,WAAW,KAAK,IAAI,CAAC,KAAK,KAAA,EAAW,QAAO;CAEtE,MAAM,UAAU,QACb,QAAQ,mBAAmB,OAAO,CAElC,WAAW,OAAO,eAAe,CACjC,WAAW,MAAM,SAAS,CAC1B,WAAW,KAAK,QAAQ,CACxB,WAAW,gBAAgB,cAAc,CACzC,WAAW,UAAU,KAAK;AAE7B,QAAO,IAAI,OAAO,IAAI,QAAQ,GAAG,CAAC,KAAK,KAAK;;;;;;;;;;AAmB9C,SAAgB,OAAO,MAAc,UAA4C;CAC/E,MAAM,aAAa,cAAc,KAAK;AACtC,KAAI,eAAe,KAAA,EAAW,QAAO;EAAE,SAAS;EAAO,QAAQ;EAAa;AAC5E,MAAK,MAAM,WAAW,SACpB,KAAI,eAAe,YAAY,QAAQ,CAAE,QAAO;EAAE,SAAS;EAAM;EAAS;AAE5E,QAAO;EAAE,SAAS;EAAO,QAAQ;EAAmB;;;;;AC3DtD,MAAa,0BAA0B;AAiEvC,IAAa,sBAAb,cAAyC,MAAM;CAC7C,YACE,SACA,MAOA,OACA;AACA,QAAM,QAAQ;AATL,OAAA,OAAA;AAOA,OAAA,QAAA;AAGT,OAAK,OAAO;;;;;;;;;;;AAYhB,eAAsB,UAAU,OAAwC;CACtE,MAAM,UAAU,mBAAmB;EACjC,OAAO,CAAC,GAAG,IAAI,IAAI,MAAM,MAAM,CAAC,CAAC,UAAU;EAC3C,QAAQ,MAAM;EACd,MAAM,MAAM;EACb,CAAC;CACF,MAAM,SAAS,MAAM,OAAO,OAAO,OAAO,WAAW,IAAI,aAAa,CAAC,OAAO,QAAQ,CAAC;AACvF,QAAO,CAAC,GAAG,IAAI,WAAW,OAAO,CAAC,CAAC,KAAI,SAAQ,KAAK,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC,KAAK,GAAG;;AAG7F,SAAS,gBAAgB,MAAmC;AAC1D,QAAO,SAAS;;;;;;;;;;AAWlB,SAAgB,sBACd,UACA,OACA,UACA,QAA+B,EAAE,EAChB;CACjB,IAAI;CAEJ,MAAM,cAAc,OAAO,QAA+B;AACxD,eAAa,MAAM,UAAU,MAAM;AACnC,MAAI,SAAS,eAAe,SAC1B,OAAM,IAAI,oBACR,4HACA,qBACD;AAEH,MAAI,SAAS,cAAc,SAAS,cAAc,IAChD,OAAM,IAAI,oBAAoB,2BAA2B,SAAS,WAAW,IAAI,mBAAmB;AAEtG,MAAI,CAAC,MAAM,OAAO,WAAA,aAAmC,CACnD,OAAM,IAAI,oBACR,0BAA0B,wBAAwB,OAAO,MAAM,OAAO,gBACtE,uBACD;AAEH,MAAI,gBAAgB,MAAM,OAAO,IAAI,gBAAgB,MAAM,KAAK,CAC9D,OAAM,IAAI,oBACR,2CAA2C,mBAAmB,qEAC9D,wBACD;;AAIL,QAAO;EACL,IAAI,QAAQ;AACV,UAAO;;EAGT,MAAM,UAAU,OAA6C;GAC3D,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;AACpC,SAAM,YAAY,IAAI;AAEtB,OAAI,MAAM,WAAW,MAAM,OACzB,OAAM,IAAI,oBACR,wBAAwB,MAAM,OAAO,yBAAyB,MAAM,OAAO,KAC3E,uBACD;AAEH,OAAI,gBAAgB,MAAM,OAAO,IAAI,gBAAgB,MAAM,KAAK,CAC9D,OAAM,IAAI,oBACR,2CAA2C,mBAAmB,YAC9D,wBACD;GAMH,MAAM,YAAY,MAAM,QAAQ,KAAK,YAAwB;IAAE;IAAQ,UAAU,OAAO,OAAO,MAAM,MAAM,MAAM;IAAE,EAAE;GACrH,MAAM,UAAU,UAAU,QAAO,UAAS,CAAC,MAAM,SAAS,QAAQ;AAClE,OAAI,QAAQ,QAAQ;IAClB,MAAM,QAAQ,QAAQ,KAAI,UAAS,MAAM,OAAO,KAAK;AACrD,UAAM,IAAI,oBACR,GAAG,MAAM,OAAO,OAAO,MAAM,WAAW,IAAI,KAAK,IAAI,+BAA+B,MAAM,MAAM,GAAG,EAAE,CAAC,KAAK,KAAK,GAAG,MAAM,SAAS,IAAI,QAAQ,MAC9I,oBACA,MACD;;GAMH,MAAM,SAAS,MAAM,SAAS,UAAU;IAAE,GAAG;IAAO,MAAM,MAAM,QAAQ,MAAM;IAAM,CAAC;AAErF,QAAK,MAAM,EAAE,QAAQ,cAAc,UACjC,OAAM,KAAK;IACT,MAAM,MAAM,QAAQ;IACpB,MAAM,OAAO;IACb,QAAQ,OAAO,YAAY,OAAO,WAAW;IAC7C,SAAS,SAAS;IAClB,OAAO,MAAM,SAAS;KAAE,MAAM;KAAU,IAAI;KAAa;IACzD,IAAI;IACJ,QAAQ,OAAO;IACf,QAAQ,MAAM;IACf,CAAC;AAGJ,UAAO;;EAEV"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { _ as RepoReader, a as FileChange } from "../../index-
|
|
2
|
-
import { t as ContentEntry } from "../../content-manager-
|
|
3
|
-
import { a as ContentSaveEntryResult, i as ReconcileResult, n as ReconcileInput, o as ContentSavePlan, r as ReconcilePlan, s as OpPlan, t as planReconcile } from "../../index-
|
|
1
|
+
import { _ as RepoReader, a as FileChange } from "../../index-A5iw3yDk.mjs";
|
|
2
|
+
import { t as ContentEntry } from "../../content-manager-BqaMkMVk.mjs";
|
|
3
|
+
import { a as ContentSaveEntryResult, i as ReconcileResult, n as ReconcileInput, o as ContentSavePlan, r as ReconcilePlan, s as OpPlan, t as planReconcile } from "../../index-DKZNUqpP.mjs";
|
|
4
4
|
import { ContentrainConfig, ModelDefinition, Vocabulary } from "@contentrain/types";
|
|
5
5
|
|
|
6
6
|
//#region src/core/ops/apply-to-worktree.d.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as RepoReader } from "../../index-
|
|
1
|
+
import { _ as RepoReader } from "../../index-A5iw3yDk.mjs";
|
|
2
2
|
import { EntryMeta, FieldDef, ModelDefinition, ValidationError, ValidationResult } from "@contentrain/types";
|
|
3
3
|
|
|
4
4
|
//#region src/core/validator/entry.d.ts
|
package/dist/git/reconcile.d.mts
CHANGED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { ApplyPlanInput as ApplyPlanInput$1, Branch, Commit as Commit$1, CommitAuthor, FileChange, FileDiff, LOCAL_CAPABILITIES, MediaAsset, MediaIngestInput, MediaListOptions, MediaListResult, MediaProvider, MediaUpdateInput, MergeResult, ProviderCapabilities as ProviderCapabilities$1, RepoProvider as RepoProvider$1, RepoReader, RepoWriter as RepoWriter$1, WriteReadiness } from "@contentrain/types";
|
|
2
|
+
export { RepoReader as _, FileChange as a, MediaAsset as c, MediaListResult as d, MediaProvider as f, RepoProvider$1 as g, ProviderCapabilities$1 as h, CommitAuthor as i, MediaIngestInput as l, MergeResult as m, Branch as n, FileDiff as o, MediaUpdateInput as p, Commit$1 as r, LOCAL_CAPABILITIES as s, ApplyPlanInput$1 as t, MediaListOptions as u, RepoWriter$1 as v, WriteReadiness as y };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as RepoReader, a as FileChange } from "./index-
|
|
1
|
+
import { _ as RepoReader, a as FileChange } from "./index-A5iw3yDk.mjs";
|
|
2
2
|
import { ConflictItem, ConflictResolution, ContextSource } from "@contentrain/types";
|
|
3
3
|
|
|
4
4
|
//#region src/core/ops/types.d.ts
|
|
@@ -82,4 +82,4 @@ interface ReconcilePlan extends OpPlan<ReconcileResult> {
|
|
|
82
82
|
declare function planReconcile(input: ReconcileInput): Promise<ReconcilePlan>;
|
|
83
83
|
//#endregion
|
|
84
84
|
export { ContentSaveEntryResult as a, ReconcileResult as i, ReconcileInput as n, ContentSavePlan as o, ReconcilePlan as r, OpPlan as s, planReconcile as t };
|
|
85
|
-
//# sourceMappingURL=index-
|
|
85
|
+
//# sourceMappingURL=index-DKZNUqpP.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index-
|
|
1
|
+
{"version":3,"file":"index-DKZNUqpP.d.mts","names":[],"sources":["../src/core/ops/types.ts","../src/core/ops/reconcile/types.ts","../src/core/ops/reconcile/plan.ts"],"mappings":";;;;;;;AAUA;;;;;UAAiB,MAAA;EAEN;EAAT,OAAA,EAAS,UAAA;EAED;EAAR,MAAA,EAAQ,OAAA;EAEE;EAAV,UAAA;AAAA;AAAA,UAGe,sBAAA;EACf,MAAA;EACA,EAAA;EACA,IAAA;EACA,MAAA;EACA,UAAA;AAAA;AAAA,KAGU,eAAA,GAAkB,MAAA,CAAO,sBAAA;;;AAjBrC;;;;;;;;AAAA,UCGiB,cAAA;EACf,IAAA,EAAM,UAAA;EACN,IAAA,EAAM,UAAA;EACN,MAAA,EAAQ,UAAA;EDGO;ECDf,WAAA,GAAc,kBAAA;;EAEd,MAAA,GAAS,aAAA;AAAA;AAAA,UAGM,eAAA;EDDf;ECGA,YAAA;EDDA;ECGA,kBAAA;EACA,oBAAA;EDDU;ECGV,oBAAA;;EAEA,WAAA;AAAA;;;;AAnBF;;;UA4BiB,aAAA,SAAsB,MAAA,CAAO,eAAA;EAC5C,SAAA,EAAW,YAAA;AAAA;;;;;;ADhCb;;;;;;;;;iBEkDsB,aAAA,CAAc,KAAA,EAAO,cAAA,GAAiB,OAAA,CAAQ,aAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import "./transaction-BR2jg4uj.mjs";
|
|
|
15
15
|
import "./local-DbPlR0Br.mjs";
|
|
16
16
|
import "./detect-CI32ynpq.mjs";
|
|
17
17
|
import "./annotations-CXHPnS-M.mjs";
|
|
18
|
-
import { n as createServer } from "./server-
|
|
18
|
+
import { n as createServer } from "./server-Do2BcSBg.mjs";
|
|
19
19
|
import "./errors-BmLE-JSC.mjs";
|
|
20
20
|
import "./reconcile-EANDIp_T.mjs";
|
|
21
21
|
import "./scan-config-C5zzygeH.mjs";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as RepoReader, a as FileChange, g as RepoProvider, h as ProviderCapabilities, i as CommitAuthor, m as MergeResult, n as Branch, o as FileDiff, r as Commit, t as ApplyPlanInput } from "../../index-
|
|
1
|
+
import { _ as RepoReader, a as FileChange, g as RepoProvider, h as ProviderCapabilities, i as CommitAuthor, m as MergeResult, n as Branch, o as FileDiff, r as Commit, t as ApplyPlanInput } from "../../index-A5iw3yDk.mjs";
|
|
2
2
|
import { Octokit } from "@octokit/rest";
|
|
3
3
|
|
|
4
4
|
//#region src/providers/github/app-auth.d.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as RepoReader, g as RepoProvider, h as ProviderCapabilities, i as CommitAuthor, m as MergeResult, n as Branch, o as FileDiff, r as Commit, t as ApplyPlanInput } from "../../index-
|
|
1
|
+
import { _ as RepoReader, g as RepoProvider, h as ProviderCapabilities, i as CommitAuthor, m as MergeResult, n as Branch, o as FileDiff, r as Commit, t as ApplyPlanInput } from "../../index-A5iw3yDk.mjs";
|
|
2
2
|
import { Gitlab } from "@gitbeaker/rest";
|
|
3
3
|
|
|
4
4
|
//#region src/providers/gitlab/capabilities.d.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as RepoReader, a as FileChange, g as RepoProvider, h as ProviderCapabilities$1, i as CommitAuthor, m as MergeResult, n as Branch, o as FileDiff, r as Commit, y as WriteReadiness } from "../../index-
|
|
1
|
+
import { _ as RepoReader, a as FileChange, g as RepoProvider$1, h as ProviderCapabilities$1, i as CommitAuthor, m as MergeResult, n as Branch, o as FileDiff, r as Commit$1, y as WriteReadiness } from "../../index-A5iw3yDk.mjs";
|
|
2
2
|
import { BaseAdvance, RemotePush, SyncResult, WorkflowMode } from "@contentrain/types";
|
|
3
3
|
|
|
4
4
|
//#region src/providers/local/types.d.ts
|
|
@@ -34,7 +34,7 @@ interface LocalApplyPlanInput {
|
|
|
34
34
|
/** Override workflow for this call; defaults to the project's configured workflow. */
|
|
35
35
|
workflowOverride?: WorkflowMode;
|
|
36
36
|
}
|
|
37
|
-
interface LocalApplyResult extends Commit {
|
|
37
|
+
interface LocalApplyResult extends Commit$1 {
|
|
38
38
|
/**
|
|
39
39
|
* `auto-merged` — changes fast-forwarded onto the base branch and the
|
|
40
40
|
* developer's working tree was selectively re-synced.
|
|
@@ -72,7 +72,7 @@ interface LocalApplyResult extends Commit {
|
|
|
72
72
|
* `update-ref` in `transaction.mergeBranch`, so arbitrary merge targets
|
|
73
73
|
* would bypass that invariant.
|
|
74
74
|
*/
|
|
75
|
-
declare class LocalProvider implements RepoProvider {
|
|
75
|
+
declare class LocalProvider implements RepoProvider$1 {
|
|
76
76
|
readonly projectRoot: string;
|
|
77
77
|
readonly capabilities: ProviderCapabilities$1;
|
|
78
78
|
private readonly reader;
|
|
@@ -109,7 +109,7 @@ declare class LocalProvider implements RepoProvider {
|
|
|
109
109
|
changes: FileChange[];
|
|
110
110
|
message: string;
|
|
111
111
|
author: CommitAuthor;
|
|
112
|
-
}): Promise<Commit>;
|
|
112
|
+
}): Promise<Commit$1>;
|
|
113
113
|
}
|
|
114
114
|
//#endregion
|
|
115
115
|
//#region src/providers/local/reader.d.ts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/providers/local/types.ts","../../../src/providers/local/provider.ts","../../../src/providers/local/reader.ts"],"mappings":";;;;;UAIiB,kBAAA;EACf,IAAA;EACA,KAAA;EACA,MAAA;EACA,OAAA;AAAA;;;;;;AAYF;;;;UAAiB,mBAAA;EAYL;EAVV,MAAA;EAY+B;EAV/B,OAAA,EAAS,UAAA;EAFT;EAIA,OAAA;EAFS;EAIT,MAAA,GAAS,YAAA;EAAT;EAEA,IAAA;EAAA;EAEA,OAAA,GAAU,kBAAA;EAAA;EAEV,gBAAA,GAAmB,YAAA;AAAA;AAAA,UAGJ,gBAAA,SAAyB,
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/providers/local/types.ts","../../../src/providers/local/provider.ts","../../../src/providers/local/reader.ts"],"mappings":";;;;;UAIiB,kBAAA;EACf,IAAA;EACA,KAAA;EACA,MAAA;EACA,OAAA;AAAA;;;;;;AAYF;;;;UAAiB,mBAAA;EAYL;EAVV,MAAA;EAY+B;EAV/B,OAAA,EAAS,UAAA;EAFT;EAIA,OAAA;EAFS;EAIT,MAAA,GAAS,YAAA;EAAT;EAEA,IAAA;EAAA;EAEA,OAAA,GAAU,kBAAA;EAAA;EAEV,gBAAA,GAAmB,YAAA;AAAA;AAAA,UAGJ,gBAAA,SAAyB,QAAA;EAHT;AAGjC;;;;;EAOE,cAAA;EAPwC;EASxC,IAAA,GAAO,UAAA;EATuC;EAW9C,OAAA;EAJA;;;;;EAUA,YAAA,GAAe,WAAA;EAEf;EAAA,WAAA,GAAc,UAAA;AAAA;;;;;AApDhB;;;;;;;;;;AAgBA;;;cC8Ba,aAAA,YAAyB,cAAA;EAAA,SAIR,WAAA;EAAA,SAHnB,YAAA,EAAc,sBAAA;EAAA,iBACN,MAAA;cAEW,WAAA;EAI5B,QAAA,CAAS,IAAA,UAAc,GAAA,YAAe,OAAA;EAItC,aAAA,CAAc,IAAA,UAAc,GAAA,YAAe,OAAA;EAI3C,UAAA,CAAW,IAAA,UAAc,GAAA,YAAe,OAAA;ED1C/B;;;;;ECmDH,mBAAA,CAAA,GAAuB,OAAA,CAAQ,cAAA;EAO/B,SAAA,CAAU,KAAA,EAAO,mBAAA,GAAsB,OAAA,CAAQ,gBAAA;EA6BrD,YAAA,CAAa,MAAA,YAAkB,OAAA,CAAQ,MAAA;EAIjC,YAAA,CAAa,IAAA,UAAc,OAAA,YAAmB,OAAA;EAKpD,YAAA,CAAa,IAAA,WAAe,OAAA;EAI5B,aAAA,CAAc,MAAA,UAAgB,IAAA,YAAgB,OAAA,CAAQ,QAAA;EAKtD,WAAA,CAAY,MAAA,UAAgB,IAAA,WAAe,OAAA,CAAQ,WAAA;EAInD,QAAA,CAAS,MAAA,UAAgB,IAAA,YAAgB,OAAA;EAKzC,gBAAA,CAAA,GAAoB,OAAA;EAId,YAAA,CAAa,IAAA,UAAc,IAAA,WAAe,OAAA;EDxFjC;;;;;;;ECwGT,iBAAA,CAAkB,KAAA;IACtB,MAAA;IACA,IAAA;IACA,MAAA;IACA,OAAA,EAAS,UAAA;IACT,OAAA;IACA,MAAA,EAAQ,YAAA;EAAA,IACN,OAAA,CAAQ,QAAA;AAAA;;;;;;ADjKd;;;;;;;;cEWa,WAAA,YAAuB,UAAA;EAAA,SACN,WAAA;cAAA,WAAA;EAEtB,QAAA,CAAS,IAAA,UAAc,IAAA,YAAgB,OAAA;EAIvC,aAAA,CAAc,IAAA,UAAc,IAAA,YAAgB,OAAA;EAQ5C,UAAA,CAAW,IAAA,UAAc,IAAA,YAAgB,OAAA;AAAA"}
|
|
@@ -14,7 +14,7 @@ import "../../transaction-BR2jg4uj.mjs";
|
|
|
14
14
|
import "../../local-DbPlR0Br.mjs";
|
|
15
15
|
import "../../detect-CI32ynpq.mjs";
|
|
16
16
|
import "../../annotations-CXHPnS-M.mjs";
|
|
17
|
-
import { n as createServer } from "../../server-
|
|
17
|
+
import { n as createServer } from "../../server-Do2BcSBg.mjs";
|
|
18
18
|
import "../../errors-BmLE-JSC.mjs";
|
|
19
19
|
import "../../reconcile-EANDIp_T.mjs";
|
|
20
20
|
import "../../scan-config-C5zzygeH.mjs";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { g as RepoProvider } from "./index-
|
|
1
|
+
import { g as RepoProvider } from "./index-A5iw3yDk.mjs";
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
|
|
4
4
|
//#region src/server.d.ts
|
|
@@ -62,4 +62,4 @@ declare function createServer(projectRoot: string): McpServer;
|
|
|
62
62
|
declare function createServer(opts: CreateServerOptions): McpServer;
|
|
63
63
|
//#endregion
|
|
64
64
|
export { createServer as i, DEFAULT_INSTRUCTIONS as n, ToolProvider as r, CreateServerOptions as t };
|
|
65
|
-
//# sourceMappingURL=server-
|
|
65
|
+
//# sourceMappingURL=server-CUtmhlno.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-
|
|
1
|
+
{"version":3,"file":"server-CUtmhlno.d.mts","names":[],"sources":["../src/server.ts"],"mappings":";;;;;;AAYA;;;;;KAAY,YAAA,GAAe,YAAA;;;;;AA6B3B;cAVa,oBAAA;AAAA,UAUI,mBAAA;EAOQ;;;;;;EAAvB,QAAA,GAAW,YAAA;EAiEG;;;;;AAChB;EA3DE,WAAA;;;;;;EAMA,YAAA;AAAA;;;;;;;;;;;;;;;;;;;;;iBAoDc,YAAA,CAAa,WAAA,WAAsB,SAAA;AAAA,iBACnC,YAAA,CAAa,IAAA,EAAM,mBAAA,GAAsB,SAAA"}
|
|
@@ -3022,7 +3022,7 @@ function registerMediaTools(server, provider, _projectRoot) {
|
|
|
3022
3022
|
}
|
|
3023
3023
|
//#endregion
|
|
3024
3024
|
//#region package.json
|
|
3025
|
-
var version = "3.
|
|
3025
|
+
var version = "3.4.0";
|
|
3026
3026
|
//#endregion
|
|
3027
3027
|
//#region src/server.ts
|
|
3028
3028
|
/**
|
|
@@ -3097,4 +3097,4 @@ function resolveServerContext(input) {
|
|
|
3097
3097
|
//#endregion
|
|
3098
3098
|
export { createServer as n, DEFAULT_INSTRUCTIONS as t };
|
|
3099
3099
|
|
|
3100
|
-
//# sourceMappingURL=server-
|
|
3100
|
+
//# sourceMappingURL=server-Do2BcSBg.mjs.map
|