canopycms 0.0.67 → 0.0.68-int.93
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/dist/ai/handler.js +8 -0
- package/dist/api/admin-branch-health.js +12 -7
- package/dist/api/admin.d.ts +8 -8
- package/dist/api/branch-status.js +6 -2
- package/dist/api/client.d.ts +12 -0
- package/dist/api/client.js +35 -11
- package/dist/api/content.d.ts +6 -5
- package/dist/api/content.js +38 -39
- package/dist/api/entries.js +6 -8
- package/dist/api/github-sync.d.ts +10 -1
- package/dist/api/github-sync.js +20 -10
- package/dist/api/reference-options.js +1 -1
- package/dist/api/request-url.d.ts +8 -0
- package/dist/api/request-url.js +15 -0
- package/dist/api/resolve-references.js +2 -2
- package/dist/api/settings-helpers.d.ts +1 -1
- package/dist/api/settings-helpers.js +1 -4
- package/dist/authorization/content.d.ts +5 -4
- package/dist/authorization/content.js +5 -5
- package/dist/authorization/path.d.ts +11 -6
- package/dist/authorization/path.js +11 -6
- package/dist/authorization/types.d.ts +2 -2
- package/dist/branch-health.js +3 -3
- package/dist/branch-schema-cache.d.ts +5 -3
- package/dist/branch-schema-cache.js +29 -11
- package/dist/branch-workspace.js +2 -2
- package/dist/cli/cli.js +285 -168
- package/dist/cli/generate-ai-content.js +190 -109
- package/dist/cli/github-app-manifest.d.ts +4 -3
- package/dist/cli/github-app-manifest.js +4 -3
- package/dist/cli/init.js +15 -10
- package/dist/cli/sync.js +15 -13
- package/dist/config/schemas/config.d.ts +6 -3
- package/dist/config/schemas/config.js +3 -1
- package/dist/config/schemas/field.js +1 -0
- package/dist/config/types.d.ts +11 -2
- package/dist/config/validation.d.ts +6 -0
- package/dist/config/validation.js +37 -0
- package/dist/content-listing.d.ts +2 -2
- package/dist/content-listing.js +3 -3
- package/dist/content-reader.js +4 -4
- package/dist/content-store.d.ts +2 -0
- package/dist/content-store.js +2 -1
- package/dist/content-tree.js +1 -1
- package/dist/context.d.ts +16 -7
- package/dist/context.js +116 -67
- package/dist/editor/Editor.js +4 -4
- package/dist/editor/FormRenderer.js +20 -5
- package/dist/editor/admin/SystemHealthPanel.js +37 -6
- package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
- package/dist/editor/components/NoEditPermissionNotice.js +17 -0
- package/dist/editor/hooks/useBranchesData.js +3 -1
- package/dist/editor/hooks/useDraftManager.d.ts +1 -1
- package/dist/editor/hooks/useDraftManager.js +2 -1
- package/dist/editor/hooks/useEntriesData.js +2 -2
- package/dist/editor/hooks/useEntryManager.js +17 -11
- package/dist/entry-schema-registry.js +2 -1
- package/dist/git-manager.d.ts +20 -0
- package/dist/git-manager.js +50 -4
- package/dist/github-service.d.ts +8 -0
- package/dist/github-service.js +5 -1
- package/dist/http/handler.js +24 -9
- package/dist/http/index.d.ts +2 -0
- package/dist/http/index.js +2 -0
- package/dist/http/router.d.ts +2 -0
- package/dist/http/router.js +2 -1
- package/dist/http/worker-not-ready.d.ts +9 -0
- package/dist/http/worker-not-ready.js +17 -0
- package/dist/operating-mode/client-unsafe-strategy.js +0 -6
- package/dist/operating-mode/types.d.ts +0 -3
- package/dist/paths/index.d.ts +1 -1
- package/dist/paths/index.js +1 -1
- package/dist/paths/normalize.d.ts +7 -0
- package/dist/paths/normalize.js +9 -0
- package/dist/reference-resolver.d.ts +3 -3
- package/dist/reference-resolver.js +3 -2
- package/dist/resolve-canopy-user.js +2 -1
- package/dist/services.d.ts +12 -5
- package/dist/services.js +56 -56
- package/dist/settings-workspace.js +36 -0
- package/dist/static/seo.d.ts +2 -14
- package/dist/static/seo.js +2 -25
- package/dist/submission-attribution.d.ts +73 -0
- package/dist/submission-attribution.js +221 -0
- package/dist/sync-core.js +7 -8
- package/dist/types.d.ts +23 -2
- package/dist/utils/content-write-lock.d.ts +3 -7
- package/dist/utils/content-write-lock.js +6 -4
- package/dist/utils/debug.d.ts +8 -0
- package/dist/utils/debug.js +10 -2
- package/dist/utils/git.d.ts +28 -0
- package/dist/utils/git.js +38 -0
- package/dist/utils/provisioning-lock.d.ts +15 -5
- package/dist/utils/provisioning-lock.js +31 -11
- package/dist/utils/request-timing.d.ts +25 -0
- package/dist/utils/request-timing.js +101 -0
- package/dist/utils/url-prefix.d.ts +15 -0
- package/dist/utils/url-prefix.js +26 -0
- package/dist/worker/canopy-state.d.ts +45 -0
- package/dist/worker/canopy-state.js +75 -0
- package/dist/worker/git-sync.d.ts +4 -2
- package/dist/worker/git-sync.js +111 -24
- package/dist/worker/provisioned-workspace.d.ts +35 -0
- package/dist/worker/provisioned-workspace.js +50 -0
- package/dist/worker/rebase.js +75 -12
- package/dist/worker/task-runner.js +39 -4
- package/package.json +1 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the API client ends request paths with `/`: on a `trailingSlash: true` Next host every
|
|
3
|
+
* unslashed call draws a 308 first. `withCanopy` sets `CANOPY_API_TRAILING_SLASH` in Next's `env`
|
|
4
|
+
* config, which Next substitutes for this literal member expression in server and browser bundles
|
|
5
|
+
* (`getNextConfigEnv`, `next/dist/build/define-env.js:54`). The try/catch covers a host whose
|
|
6
|
+
* bundler neither substitutes it nor shims `process` in the browser.
|
|
7
|
+
*/
|
|
8
|
+
export function readApiTrailingSlashEnv() {
|
|
9
|
+
try {
|
|
10
|
+
return process.env.CANOPY_API_TRAILING_SLASH === 'true';
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -4,6 +4,7 @@ import { defineEndpoint } from './route-builder.js';
|
|
|
4
4
|
import { ReferenceResolver } from '../reference-resolver.js';
|
|
5
5
|
import { buildResolvedReference } from '../entry-schema.js';
|
|
6
6
|
import { computeEntryUrl } from '../utils/entry-url.js';
|
|
7
|
+
import { entryLogicalPath } from '../paths/index.js';
|
|
7
8
|
import { branchNameSchema, contentIdSchema } from './validators.js';
|
|
8
9
|
/**
|
|
9
10
|
* Resolution does sequential per-ID file I/O, so the request body caps how much filesystem work
|
|
@@ -39,8 +40,7 @@ const resolveReferencesHandler = async (gc, ctx, req, params, body) => {
|
|
|
39
40
|
const result = await resolver.resolve(id);
|
|
40
41
|
if (result && result.exists && result.collection && result.slug) {
|
|
41
42
|
// Check path-level read permission before returning content
|
|
42
|
-
const
|
|
43
|
-
const access = checkAccess(resolvedPath.relativePath, 'read');
|
|
43
|
+
const access = checkAccess(entryLogicalPath(result.collection, result.slug), 'read');
|
|
44
44
|
if (!access.allowed)
|
|
45
45
|
continue;
|
|
46
46
|
// `resolveReferences: false` matches the server-side resolver (content-store.ts's
|
|
@@ -30,7 +30,7 @@ export interface CommitSettingsResult {
|
|
|
30
30
|
/**
|
|
31
31
|
* Commit and push settings changes based on the mode.
|
|
32
32
|
* Both prod and dev use commitToSettingsBranch.
|
|
33
|
-
*
|
|
33
|
+
* Settings changes are never reviewed through a PR (see commitToSettingsBranch).
|
|
34
34
|
*/
|
|
35
35
|
export declare function commitSettings(ctx: ApiContext, options: {
|
|
36
36
|
context: {
|
|
@@ -23,7 +23,7 @@ export async function getSettingsBranchContext(ctx) {
|
|
|
23
23
|
/**
|
|
24
24
|
* Commit and push settings changes based on the mode.
|
|
25
25
|
* Both prod and dev use commitToSettingsBranch.
|
|
26
|
-
*
|
|
26
|
+
* Settings changes are never reviewed through a PR (see commitToSettingsBranch).
|
|
27
27
|
*/
|
|
28
28
|
export async function commitSettings(ctx, options) {
|
|
29
29
|
const strategy = operatingStrategy(options.mode);
|
|
@@ -38,9 +38,6 @@ export async function commitSettings(ctx, options) {
|
|
|
38
38
|
branchRoot: options.branchRoot,
|
|
39
39
|
files: options.fileName,
|
|
40
40
|
message: options.message,
|
|
41
|
-
createPR: strategy.shouldCreateSettingsPR({
|
|
42
|
-
autoCreateSettingsPR: ctx.services.config.autoCreateSettingsPR,
|
|
43
|
-
}),
|
|
44
41
|
});
|
|
45
42
|
if (!result.pushed) {
|
|
46
43
|
console.warn(`${options.message} committed but not pushed:`, result.error);
|
|
@@ -6,12 +6,13 @@ import type { BranchContext } from '../types.js';
|
|
|
6
6
|
import type { PermissionLevel } from '../config/index.js';
|
|
7
7
|
import type { CanopyUser } from '../user.js';
|
|
8
8
|
import type { ContentAccessResult, ContentAccessDeps } from './types.js';
|
|
9
|
-
import type {
|
|
9
|
+
import type { LogicalPath } from '../paths/types.js';
|
|
10
10
|
/**
|
|
11
11
|
* A bound content-access checker: evaluates a single path/level synchronously
|
|
12
12
|
* against permissions that were already loaded when the checker was created.
|
|
13
|
+
* The path is the entry's logical path (see `checkPathAccess`).
|
|
13
14
|
*/
|
|
14
|
-
export type ContentAccessChecker = (
|
|
15
|
+
export type ContentAccessChecker = (logicalPath: LogicalPath, level: PermissionLevel) => ContentAccessResult;
|
|
15
16
|
/**
|
|
16
17
|
* Resolve the request-constant work once -- branch access, the
|
|
17
18
|
* settings/permissions root and the path-permission rules, all identical for a
|
|
@@ -26,5 +27,5 @@ export declare function createContentAccessChecker(deps: ContentAccessDeps, cont
|
|
|
26
27
|
* Single-path check; loads the path permissions from the branch root per call.
|
|
27
28
|
* @internal Exported for tests.
|
|
28
29
|
*/
|
|
29
|
-
export declare function checkContentAccess(deps: ContentAccessDeps, context: BranchContext, branchRoot: string,
|
|
30
|
-
export declare function createCheckContentAccess(deps: ContentAccessDeps): (context: BranchContext, branchRoot: string,
|
|
30
|
+
export declare function checkContentAccess(deps: ContentAccessDeps, context: BranchContext, branchRoot: string, logicalPath: LogicalPath, user: CanopyUser, level: PermissionLevel): Promise<ContentAccessResult>;
|
|
31
|
+
export declare function createCheckContentAccess(deps: ContentAccessDeps): (context: BranchContext, branchRoot: string, logicalPath: LogicalPath, user: CanopyUser, level: PermissionLevel) => Promise<ContentAccessResult>;
|
|
@@ -28,9 +28,9 @@ export async function createContentAccessChecker(deps, context, branchRoot, user
|
|
|
28
28
|
}
|
|
29
29
|
const rules = await deps.loadPathPermissions(permissionsRoot, deps.mode);
|
|
30
30
|
const pathChecker = createCheckPathAccess(rules, deps.defaultPathAccess);
|
|
31
|
-
return (
|
|
31
|
+
return (logicalPath, level) => {
|
|
32
32
|
const path = pathChecker({
|
|
33
|
-
|
|
33
|
+
logicalPath,
|
|
34
34
|
user,
|
|
35
35
|
level,
|
|
36
36
|
});
|
|
@@ -45,10 +45,10 @@ export async function createContentAccessChecker(deps, context, branchRoot, user
|
|
|
45
45
|
* Single-path check; loads the path permissions from the branch root per call.
|
|
46
46
|
* @internal Exported for tests.
|
|
47
47
|
*/
|
|
48
|
-
export async function checkContentAccess(deps, context, branchRoot,
|
|
48
|
+
export async function checkContentAccess(deps, context, branchRoot, logicalPath, user, level) {
|
|
49
49
|
const check = await createContentAccessChecker(deps, context, branchRoot, user);
|
|
50
|
-
return check(
|
|
50
|
+
return check(logicalPath, level);
|
|
51
51
|
}
|
|
52
52
|
export function createCheckContentAccess(deps) {
|
|
53
|
-
return (context, branchRoot,
|
|
53
|
+
return (context, branchRoot, logicalPath, user, level) => checkContentAccess(deps, context, branchRoot, logicalPath, user, level);
|
|
54
54
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PathPermission, DefaultPathAccess, PermissionLevel } from '../config/index.js';
|
|
2
2
|
import type { CanopyUser } from '../user.js';
|
|
3
3
|
import type { PathPermissionResult } from './types.js';
|
|
4
|
-
import type {
|
|
4
|
+
import type { LogicalPath } from '../paths/types.js';
|
|
5
5
|
/**
|
|
6
6
|
* Resolve `defaultPathAccess` to an 'allow'/'deny' verdict for one permission
|
|
7
7
|
* level. String form applies to every level; object form looks up the level,
|
|
@@ -11,19 +11,24 @@ import type { PhysicalPath } from '../paths/types.js';
|
|
|
11
11
|
*/
|
|
12
12
|
export declare function resolveDefaultPathAccess(defaultAccess: DefaultPathAccess, level: PermissionLevel): 'allow' | 'deny';
|
|
13
13
|
/**
|
|
14
|
-
* Evaluate access for a
|
|
15
|
-
*
|
|
14
|
+
* Evaluate access for a path against config-defined rules. First matching rule wins;
|
|
15
|
+
* defaultAccess applies when none matches.
|
|
16
|
+
*
|
|
17
|
+
* Rule globs and the checked path are both LOGICAL paths: content-root-prefixed and
|
|
18
|
+
* id-free, the space the Permission Manager writes rules in. An entry's logical path is
|
|
19
|
+
* `<collection logical path>/<slug>` (`entryLogicalPath`), e.g. `content/blog/my-post`, never
|
|
20
|
+
* its on-disk `content/blog.<id>/post.my-post.<id>.json`.
|
|
16
21
|
* @internal Exported for tests.
|
|
17
22
|
*/
|
|
18
|
-
export declare function checkPathAccess({ rules,
|
|
23
|
+
export declare function checkPathAccess({ rules, logicalPath, user, defaultAccess, level, }: {
|
|
19
24
|
rules: PathPermission[];
|
|
20
|
-
|
|
25
|
+
logicalPath: LogicalPath;
|
|
21
26
|
user: CanopyUser;
|
|
22
27
|
defaultAccess: DefaultPathAccess;
|
|
23
28
|
level: PermissionLevel;
|
|
24
29
|
}): PathPermissionResult;
|
|
25
30
|
export declare function createCheckPathAccess(rules: PathPermission[], defaultAccess: DefaultPathAccess): (input: {
|
|
26
|
-
|
|
31
|
+
logicalPath: LogicalPath;
|
|
27
32
|
user: CanopyUser;
|
|
28
33
|
level: PermissionLevel;
|
|
29
34
|
}) => PathPermissionResult;
|
|
@@ -5,8 +5,8 @@ function normalize(p) {
|
|
|
5
5
|
const normalized = p.split(path.sep).join('/');
|
|
6
6
|
return normalized.replace(/^\.?\/*/, '');
|
|
7
7
|
}
|
|
8
|
-
function matchesRule(rule,
|
|
9
|
-
return minimatch(
|
|
8
|
+
function matchesRule(rule, logicalPath) {
|
|
9
|
+
return minimatch(logicalPath, rule.path, { dot: true });
|
|
10
10
|
}
|
|
11
11
|
function isAllowedByTarget(target, user) {
|
|
12
12
|
const hasUserConstraint = !!target.allowedUsers?.length;
|
|
@@ -32,12 +32,17 @@ export function resolveDefaultPathAccess(defaultAccess, level) {
|
|
|
32
32
|
return defaultAccess[level] ?? 'deny';
|
|
33
33
|
}
|
|
34
34
|
/**
|
|
35
|
-
* Evaluate access for a
|
|
36
|
-
*
|
|
35
|
+
* Evaluate access for a path against config-defined rules. First matching rule wins;
|
|
36
|
+
* defaultAccess applies when none matches.
|
|
37
|
+
*
|
|
38
|
+
* Rule globs and the checked path are both LOGICAL paths: content-root-prefixed and
|
|
39
|
+
* id-free, the space the Permission Manager writes rules in. An entry's logical path is
|
|
40
|
+
* `<collection logical path>/<slug>` (`entryLogicalPath`), e.g. `content/blog/my-post`, never
|
|
41
|
+
* its on-disk `content/blog.<id>/post.my-post.<id>.json`.
|
|
37
42
|
* @internal Exported for tests.
|
|
38
43
|
*/
|
|
39
|
-
export function checkPathAccess({ rules,
|
|
40
|
-
const normalizedPath = normalize(
|
|
44
|
+
export function checkPathAccess({ rules, logicalPath, user, defaultAccess, level, }) {
|
|
45
|
+
const normalizedPath = normalize(logicalPath);
|
|
41
46
|
// Only Admins bypass all path permissions
|
|
42
47
|
if (isAdmin(user.groups)) {
|
|
43
48
|
return { allowed: true, reason: 'admin' };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A path used in permission rules, e.g. "content/posts"
|
|
3
|
-
* SECURITY CRITICAL: always validated to prevent path traversal.
|
|
2
|
+
* A logical-path glob used in permission rules, e.g. "content/posts/**" (see
|
|
3
|
+
* `PathPermission.path`). SECURITY CRITICAL: always validated to prevent path traversal.
|
|
4
4
|
*/
|
|
5
5
|
export type PermissionPath = string & {
|
|
6
6
|
readonly __brand: 'PermissionPath';
|
package/dist/branch-health.js
CHANGED
|
@@ -5,16 +5,16 @@ import { ContentIdIndex } from './content-id-index.js';
|
|
|
5
5
|
import { sanitizeBranchName } from './paths/branch-name.js';
|
|
6
6
|
import { getErrorMessage, isNodeError, isNotFoundError } from './utils/error.js';
|
|
7
7
|
import { isRebaseInProgress } from './utils/git.js';
|
|
8
|
+
import { branchProvisioningLockName } from './utils/provisioning-lock.js';
|
|
8
9
|
/**
|
|
9
|
-
* The provisioning lock marker's path for a branch directory
|
|
10
|
-
* `branch-workspace.ts`'s `ensureGitWorkspace()` passes exactly.
|
|
10
|
+
* The provisioning lock marker's path for a branch directory.
|
|
11
11
|
*
|
|
12
12
|
* `acquireProvisioningLock` passes the name as `lockfilePath`, overriding
|
|
13
13
|
* proper-lockfile's default `${target}.lock`, so the marker (a mkdir-based
|
|
14
14
|
* directory) sits at exactly this path — no extra `.lock` suffix.
|
|
15
15
|
*/
|
|
16
16
|
function provisioningLockPath(baseRoot, dirName) {
|
|
17
|
-
return path.join(baseRoot,
|
|
17
|
+
return path.join(baseRoot, branchProvisioningLockName(dirName));
|
|
18
18
|
}
|
|
19
19
|
/** Stat the provisioning lock marker, if present. Never throws. */
|
|
20
20
|
async function readProvisioningLock(baseRoot, dirName) {
|
|
@@ -2,6 +2,8 @@ import type { RootCollectionConfig } from './config/index.js';
|
|
|
2
2
|
import type { FlatSchemaItem } from './config/types.js';
|
|
3
3
|
import type { OperatingMode } from './operating-mode/index.js';
|
|
4
4
|
import type { EntrySchemaRegistry, SchemaResolutionResult } from './schema/types.js';
|
|
5
|
+
/** File name of the schema cache inside {@link schemaCacheDir}. */
|
|
6
|
+
export declare const SCHEMA_CACHE_FILE = "schema-cache.json";
|
|
5
7
|
/** resource-generation.ts resource key for the schema cache's marker. */
|
|
6
8
|
export declare const SCHEMA_GENERATION_RESOURCE = "schema";
|
|
7
9
|
/** @internal Exported for tests. */
|
|
@@ -18,8 +20,8 @@ export interface BranchSchemaCacheEntry {
|
|
|
18
20
|
generation: string | null;
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
21
|
-
* Per-branch schema cache:
|
|
22
|
-
*
|
|
23
|
+
* Per-branch schema cache: one file in {@link schemaCacheDir} with no in-memory
|
|
24
|
+
* layer, so it stays coherent across Lambda invocations.
|
|
23
25
|
*
|
|
24
26
|
* Freshness follows the generation-marker protocol owned by
|
|
25
27
|
* resource-generation.ts, and this is one of that protocol's durable-snapshot
|
|
@@ -39,7 +41,7 @@ export declare class BranchSchemaCache {
|
|
|
39
41
|
/**
|
|
40
42
|
* Whether to skip the on-disk cache for this branchRoot.
|
|
41
43
|
*
|
|
42
|
-
* Never write
|
|
44
|
+
* Never write a schema cache at the project root, whichever entrypoint
|
|
43
45
|
* produced the cwd branchRoot. branchRoot equals process.cwd() only in the
|
|
44
46
|
* synthetic contexts static deployments and build phases use; a real branch
|
|
45
47
|
* root is always nested under the workspace.
|
|
@@ -5,10 +5,14 @@ import { flattenSchema } from './config/flatten.js';
|
|
|
5
5
|
import { validateReferenceEntryTypes } from './validation/entry-type-reference-validator.js';
|
|
6
6
|
import { isBuildMode } from './build-mode.js';
|
|
7
7
|
import { bumpResourceGeneration, readResourceGeneration, isGenerationCurrent, } from './resource-generation.js';
|
|
8
|
+
import { timeRequestPhase } from './utils/request-timing.js';
|
|
9
|
+
import { CANOPY_META_DIR } from './utils/git.js';
|
|
8
10
|
/** Bump when BranchSchemaCacheEntry shape changes to auto-invalidate stale caches */
|
|
9
11
|
const SCHEMA_CACHE_VERSION = 3;
|
|
10
12
|
/** Minimum interval between mtime staleness checks (ms) */
|
|
11
13
|
const MTIME_CHECK_DEBOUNCE_MS = 1000;
|
|
14
|
+
/** File name of the schema cache inside {@link schemaCacheDir}. */
|
|
15
|
+
export const SCHEMA_CACHE_FILE = 'schema-cache.json';
|
|
12
16
|
/** resource-generation.ts resource key for the schema cache's marker. */
|
|
13
17
|
export const SCHEMA_GENERATION_RESOURCE = 'schema';
|
|
14
18
|
/**
|
|
@@ -43,8 +47,22 @@ async function isStaleByMtime(dir, cachedAt) {
|
|
|
43
47
|
return false;
|
|
44
48
|
}
|
|
45
49
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
50
|
+
* Directory holding a branch's schema cache: `{branchRoot}/.git/canopycms` when
|
|
51
|
+
* the branch root is a full clone (every canopycms branch workspace is), else
|
|
52
|
+
* `{branchRoot}/.canopy-meta`. Under `.git/` because nothing there is ever
|
|
53
|
+
* tracked or reported by `git status`, so the cache can neither dirty the
|
|
54
|
+
* workspace nor enter a commit, even where an adopter has committed
|
|
55
|
+
* `.canopy-meta/`. It also lives and dies with the clone, so a re-clone never
|
|
56
|
+
* reads its predecessor's snapshot.
|
|
57
|
+
*/
|
|
58
|
+
async function schemaCacheDir(branchRoot) {
|
|
59
|
+
const gitDir = path.join(branchRoot, '.git');
|
|
60
|
+
const isClone = await fs.stat(gitDir).then((stat) => stat.isDirectory(), () => false);
|
|
61
|
+
return isClone ? path.join(gitDir, 'canopycms') : path.join(branchRoot, CANOPY_META_DIR);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Per-branch schema cache: one file in {@link schemaCacheDir} with no in-memory
|
|
65
|
+
* layer, so it stays coherent across Lambda invocations.
|
|
48
66
|
*
|
|
49
67
|
* Freshness follows the generation-marker protocol owned by
|
|
50
68
|
* resource-generation.ts, and this is one of that protocol's durable-snapshot
|
|
@@ -65,7 +83,7 @@ export class BranchSchemaCache {
|
|
|
65
83
|
/**
|
|
66
84
|
* Whether to skip the on-disk cache for this branchRoot.
|
|
67
85
|
*
|
|
68
|
-
* Never write
|
|
86
|
+
* Never write a schema cache at the project root, whichever entrypoint
|
|
69
87
|
* produced the cwd branchRoot. branchRoot equals process.cwd() only in the
|
|
70
88
|
* synthetic contexts static deployments and build phases use; a real branch
|
|
71
89
|
* root is always nested under the workspace.
|
|
@@ -75,7 +93,7 @@ export class BranchSchemaCache {
|
|
|
75
93
|
}
|
|
76
94
|
/** Get schema for a branch, from the cache when fresh, else resolved fresh. */
|
|
77
95
|
async getSchema(branchRoot, entrySchemaRegistry, contentRootName = 'content') {
|
|
78
|
-
return this.loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName);
|
|
96
|
+
return timeRequestPhase('schema', () => this.loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName));
|
|
79
97
|
}
|
|
80
98
|
/**
|
|
81
99
|
* Resolve the schema from disk. Protected rather than a direct resolveSchema
|
|
@@ -89,8 +107,7 @@ export class BranchSchemaCache {
|
|
|
89
107
|
const contentRoot = path.join(branchRoot, contentRootName);
|
|
90
108
|
const skipDiskCache = this.skipDiskCache(branchRoot);
|
|
91
109
|
if (!skipDiskCache) {
|
|
92
|
-
const
|
|
93
|
-
const cachePath = path.join(cacheDir, 'schema-cache.json');
|
|
110
|
+
const cachePath = path.join(await schemaCacheDir(branchRoot), SCHEMA_CACHE_FILE);
|
|
94
111
|
let cacheData = null;
|
|
95
112
|
try {
|
|
96
113
|
const cacheContent = await fs.readFile(cachePath, 'utf-8');
|
|
@@ -149,7 +166,8 @@ export class BranchSchemaCache {
|
|
|
149
166
|
const read = skipDiskCache
|
|
150
167
|
? null
|
|
151
168
|
: await readResourceGeneration(branchRoot, SCHEMA_GENERATION_RESOURCE);
|
|
152
|
-
|
|
169
|
+
// Nested under `schema`, so a request summary names a cache miss (`…schema>resolve`).
|
|
170
|
+
const result = await timeRequestPhase('resolve', () => this.resolveFresh(contentRoot, entrySchemaRegistry));
|
|
153
171
|
// Validate schema has content
|
|
154
172
|
if (!isValidSchema(result.schema)) {
|
|
155
173
|
throw new Error(`No schema found in ${contentRoot}. Create .collection.json files ` +
|
|
@@ -166,11 +184,11 @@ export class BranchSchemaCache {
|
|
|
166
184
|
// Use configured contentRoot name as base path for logical paths
|
|
167
185
|
const flatSchema = flattenSchema(result.schema, contentRootName);
|
|
168
186
|
if (!skipDiskCache) {
|
|
169
|
-
const cacheDir =
|
|
170
|
-
const cachePath = path.join(cacheDir,
|
|
187
|
+
const cacheDir = await schemaCacheDir(branchRoot);
|
|
188
|
+
const cachePath = path.join(cacheDir, SCHEMA_CACHE_FILE);
|
|
171
189
|
// Opportunistic cleanup of the retired .stale marker file, which a
|
|
172
190
|
// mid-flight deploy can leave behind. Not load-bearing.
|
|
173
|
-
await fs.unlink(path.join(
|
|
191
|
+
await fs.unlink(path.join(branchRoot, CANOPY_META_DIR, 'schema-cache.stale')).catch(() => { });
|
|
174
192
|
if (read && read.ok) {
|
|
175
193
|
const newCache = {
|
|
176
194
|
version: SCHEMA_CACHE_VERSION,
|
|
@@ -180,7 +198,7 @@ export class BranchSchemaCache {
|
|
|
180
198
|
generation: read.token,
|
|
181
199
|
};
|
|
182
200
|
// Temp file then rename, with the temp file unlinked on a failed
|
|
183
|
-
// rename so a transient error leaves no stray `.tmp`
|
|
201
|
+
// rename so a transient error leaves no stray `.tmp` beside the cache.
|
|
184
202
|
await fs.mkdir(cacheDir, { recursive: true });
|
|
185
203
|
const tmpPath = path.join(cacheDir, `schema-cache.tmp.${Date.now()}.${Math.random()}.json`);
|
|
186
204
|
await fs.writeFile(tmpPath, JSON.stringify(newCache, null, 2), 'utf-8');
|
package/dist/branch-workspace.js
CHANGED
|
@@ -6,7 +6,7 @@ import { operatingStrategy } from './operating-mode/index.js';
|
|
|
6
6
|
import { GitManager } from './git-manager.js';
|
|
7
7
|
import { createDebugLogger } from './utils/debug.js';
|
|
8
8
|
import { resolveBaseBranch } from './utils/git.js';
|
|
9
|
-
import { acquireProvisioningLock } from './utils/provisioning-lock.js';
|
|
9
|
+
import { acquireProvisioningLock, branchProvisioningLockName } from './utils/provisioning-lock.js';
|
|
10
10
|
const log = createDebugLogger({ prefix: 'BranchWorkspace' });
|
|
11
11
|
// In-memory lock to prevent concurrent workspace initialization
|
|
12
12
|
const workspaceInitLocks = new Map();
|
|
@@ -41,7 +41,7 @@ export class BranchWorkspaceManager {
|
|
|
41
41
|
branchName: options.branchName,
|
|
42
42
|
mode: options.mode,
|
|
43
43
|
});
|
|
44
|
-
releaseLock = await acquireProvisioningLock(path.dirname(options.branchRoot),
|
|
44
|
+
releaseLock = await acquireProvisioningLock(path.dirname(options.branchRoot), branchProvisioningLockName(path.basename(options.branchRoot)));
|
|
45
45
|
await GitManager.initializeWorkspace({
|
|
46
46
|
workspacePath: options.branchRoot,
|
|
47
47
|
branchName: options.branchName,
|