canopycms 0.0.59 → 0.0.61-int.74
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/api/__test__/mock-client.d.ts +29 -0
- package/dist/api/__test__/mock-client.d.ts.map +1 -1
- package/dist/api/__test__/mock-client.js +55 -4
- package/dist/api/__test__/mock-client.js.map +1 -1
- package/dist/api/admin-branch-health.d.ts +65 -0
- package/dist/api/admin-branch-health.d.ts.map +1 -0
- package/dist/api/admin-branch-health.js +448 -0
- package/dist/api/admin-branch-health.js.map +1 -0
- package/dist/api/admin.d.ts +136 -0
- package/dist/api/admin.d.ts.map +1 -0
- package/dist/api/admin.js +338 -0
- package/dist/api/admin.js.map +1 -0
- package/dist/api/assets.d.ts.map +1 -1
- package/dist/api/assets.js +4 -0
- package/dist/api/assets.js.map +1 -1
- package/dist/api/branch-merge.d.ts +6 -0
- package/dist/api/branch-merge.d.ts.map +1 -1
- package/dist/api/branch-merge.js +15 -9
- package/dist/api/branch-merge.js.map +1 -1
- package/dist/api/branch-status.d.ts.map +1 -1
- package/dist/api/branch-status.js +15 -5
- package/dist/api/branch-status.js.map +1 -1
- package/dist/api/branch-withdraw.d.ts +6 -0
- package/dist/api/branch-withdraw.d.ts.map +1 -1
- package/dist/api/branch-withdraw.js +34 -7
- package/dist/api/branch-withdraw.js.map +1 -1
- package/dist/api/branch.d.ts +11 -1
- package/dist/api/branch.d.ts.map +1 -1
- package/dist/api/branch.js +70 -4
- package/dist/api/branch.js.map +1 -1
- package/dist/api/client.d.ts +34 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +47 -0
- package/dist/api/client.js.map +1 -1
- package/dist/api/comments.d.ts.map +1 -1
- package/dist/api/comments.js +4 -0
- package/dist/api/comments.js.map +1 -1
- package/dist/api/content.d.ts.map +1 -1
- package/dist/api/content.js +4 -4
- package/dist/api/content.js.map +1 -1
- package/dist/api/entries.d.ts.map +1 -1
- package/dist/api/entries.js +23 -4
- package/dist/api/entries.js.map +1 -1
- package/dist/api/github-sync.d.ts.map +1 -1
- package/dist/api/github-sync.js +14 -3
- package/dist/api/github-sync.js.map +1 -1
- package/dist/api/groups.d.ts +3 -0
- package/dist/api/groups.d.ts.map +1 -1
- package/dist/api/groups.js +89 -68
- package/dist/api/groups.js.map +1 -1
- package/dist/api/guards.d.ts +7 -1
- package/dist/api/guards.d.ts.map +1 -1
- package/dist/api/guards.js +54 -0
- package/dist/api/guards.js.map +1 -1
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +0 -1
- package/dist/api/index.js.map +1 -1
- package/dist/api/permissions.d.ts +1 -0
- package/dist/api/permissions.d.ts.map +1 -1
- package/dist/api/permissions.js +23 -20
- package/dist/api/permissions.js.map +1 -1
- package/dist/api/schema.d.ts.map +1 -1
- package/dist/api/schema.js +36 -22
- package/dist/api/schema.js.map +1 -1
- package/dist/authorization/branch.d.ts +14 -1
- package/dist/authorization/branch.d.ts.map +1 -1
- package/dist/authorization/branch.js +5 -3
- package/dist/authorization/branch.js.map +1 -1
- package/dist/authorization/groups/index.d.ts +1 -1
- package/dist/authorization/groups/index.d.ts.map +1 -1
- package/dist/authorization/groups/index.js +1 -1
- package/dist/authorization/groups/index.js.map +1 -1
- package/dist/authorization/groups/loader.d.ts +25 -4
- package/dist/authorization/groups/loader.d.ts.map +1 -1
- package/dist/authorization/groups/loader.js +39 -20
- package/dist/authorization/groups/loader.js.map +1 -1
- package/dist/authorization/groups/schema.d.ts +8 -7
- package/dist/authorization/groups/schema.d.ts.map +1 -1
- package/dist/authorization/groups/schema.js +8 -4
- package/dist/authorization/groups/schema.js.map +1 -1
- package/dist/authorization/index.d.ts +5 -3
- package/dist/authorization/index.d.ts.map +1 -1
- package/dist/authorization/index.js +8 -3
- package/dist/authorization/index.js.map +1 -1
- package/dist/authorization/path.d.ts +8 -0
- package/dist/authorization/path.d.ts.map +1 -1
- package/dist/authorization/path.js +16 -1
- package/dist/authorization/path.js.map +1 -1
- package/dist/authorization/permissions/index.d.ts +1 -1
- package/dist/authorization/permissions/index.d.ts.map +1 -1
- package/dist/authorization/permissions/index.js +1 -1
- package/dist/authorization/permissions/index.js.map +1 -1
- package/dist/authorization/permissions/loader.d.ts +18 -4
- package/dist/authorization/permissions/loader.d.ts.map +1 -1
- package/dist/authorization/permissions/loader.js +35 -24
- package/dist/authorization/permissions/loader.js.map +1 -1
- package/dist/authorization/permissions/schema.d.ts +8 -7
- package/dist/authorization/permissions/schema.d.ts.map +1 -1
- package/dist/authorization/permissions/schema.js +8 -4
- package/dist/authorization/permissions/schema.js.map +1 -1
- package/dist/authorization/protected-branch.d.ts +39 -0
- package/dist/authorization/protected-branch.d.ts.map +1 -0
- package/dist/authorization/protected-branch.js +43 -0
- package/dist/authorization/protected-branch.js.map +1 -0
- package/dist/authorization/settings-file-store.d.ts +127 -0
- package/dist/authorization/settings-file-store.d.ts.map +1 -0
- package/dist/authorization/settings-file-store.js +156 -0
- package/dist/authorization/settings-file-store.js.map +1 -0
- package/dist/branch-health.d.ts +78 -0
- package/dist/branch-health.d.ts.map +1 -0
- package/dist/branch-health.js +151 -0
- package/dist/branch-health.js.map +1 -0
- package/dist/branch-metadata.d.ts +31 -0
- package/dist/branch-metadata.d.ts.map +1 -1
- package/dist/branch-metadata.js +43 -1
- package/dist/branch-metadata.js.map +1 -1
- package/dist/branch-registry.d.ts.map +1 -1
- package/dist/branch-registry.js +13 -1
- package/dist/branch-registry.js.map +1 -1
- package/dist/cli/cli.js +398 -200
- package/dist/cli/generate-ai-content.js +72 -13
- package/dist/cli/init.js +84 -2
- package/dist/cli/migrate.d.ts.map +1 -1
- package/dist/cli/migrate.js +39 -11
- package/dist/cli/migrate.js.map +1 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +4 -1
- package/dist/cli/sync.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/schemas/config.d.ts +55 -5
- package/dist/config/schemas/config.d.ts.map +1 -1
- package/dist/config/schemas/config.js +18 -1
- package/dist/config/schemas/config.js.map +1 -1
- package/dist/config/types.d.ts +11 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/content-store.d.ts +16 -0
- package/dist/content-store.d.ts.map +1 -1
- package/dist/content-store.js +22 -6
- package/dist/content-store.js.map +1 -1
- package/dist/context.d.ts +3 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +25 -3
- package/dist/context.js.map +1 -1
- package/dist/editor/BranchManager.d.ts +11 -0
- package/dist/editor/BranchManager.d.ts.map +1 -1
- package/dist/editor/BranchManager.js +34 -10
- package/dist/editor/BranchManager.js.map +1 -1
- package/dist/editor/CommentsPanel.d.ts.map +1 -1
- package/dist/editor/CommentsPanel.js +4 -12
- package/dist/editor/CommentsPanel.js.map +1 -1
- package/dist/editor/Editor.d.ts.map +1 -1
- package/dist/editor/Editor.js +46 -10
- package/dist/editor/Editor.js.map +1 -1
- package/dist/editor/EntryNavigator.d.ts +7 -0
- package/dist/editor/EntryNavigator.d.ts.map +1 -1
- package/dist/editor/EntryNavigator.js +17 -5
- package/dist/editor/EntryNavigator.js.map +1 -1
- package/dist/editor/FormRenderer.d.ts.map +1 -1
- package/dist/editor/FormRenderer.js +4 -0
- package/dist/editor/FormRenderer.js.map +1 -1
- package/dist/editor/admin/SystemHealthPanel.d.ts +6 -0
- package/dist/editor/admin/SystemHealthPanel.d.ts.map +1 -0
- package/dist/editor/admin/SystemHealthPanel.js +234 -0
- package/dist/editor/admin/SystemHealthPanel.js.map +1 -0
- package/dist/editor/admin/useSystemHealth.d.ts +40 -0
- package/dist/editor/admin/useSystemHealth.d.ts.map +1 -0
- package/dist/editor/admin/useSystemHealth.js +205 -0
- package/dist/editor/admin/useSystemHealth.js.map +1 -0
- package/dist/editor/comments/FieldWrapper.d.ts.map +1 -1
- package/dist/editor/comments/FieldWrapper.js +1 -1
- package/dist/editor/comments/FieldWrapper.js.map +1 -1
- package/dist/editor/comments/InlineCommentThread.d.ts.map +1 -1
- package/dist/editor/comments/InlineCommentThread.js +2 -21
- package/dist/editor/comments/InlineCommentThread.js.map +1 -1
- package/dist/editor/comments/ThreadCarousel.d.ts +2 -0
- package/dist/editor/comments/ThreadCarousel.d.ts.map +1 -1
- package/dist/editor/comments/ThreadCarousel.js +5 -2
- package/dist/editor/comments/ThreadCarousel.js.map +1 -1
- package/dist/editor/components/EditorHeader.d.ts +11 -0
- package/dist/editor/components/EditorHeader.d.ts.map +1 -1
- package/dist/editor/components/EditorHeader.js +75 -41
- package/dist/editor/components/EditorHeader.js.map +1 -1
- package/dist/editor/components/EditorSidebar.d.ts +7 -1
- package/dist/editor/components/EditorSidebar.d.ts.map +1 -1
- package/dist/editor/components/EditorSidebar.js +3 -3
- package/dist/editor/components/EditorSidebar.js.map +1 -1
- package/dist/editor/fields/ImageField.d.ts.map +1 -1
- package/dist/editor/fields/ImageField.js +4 -1
- package/dist/editor/fields/ImageField.js.map +1 -1
- package/dist/editor/fields/StringListField.d.ts +20 -0
- package/dist/editor/fields/StringListField.d.ts.map +1 -0
- package/dist/editor/fields/StringListField.js +25 -0
- package/dist/editor/fields/StringListField.js.map +1 -0
- package/dist/editor/hooks/useBranchActions.d.ts.map +1 -1
- package/dist/editor/hooks/useBranchActions.js +8 -2
- package/dist/editor/hooks/useBranchActions.js.map +1 -1
- package/dist/editor/hooks/useBranchManager.d.ts +14 -3
- package/dist/editor/hooks/useBranchManager.d.ts.map +1 -1
- package/dist/editor/hooks/useBranchManager.js +18 -1
- package/dist/editor/hooks/useBranchManager.js.map +1 -1
- package/dist/editor/hooks/useDraftManager.d.ts +7 -0
- package/dist/editor/hooks/useDraftManager.d.ts.map +1 -1
- package/dist/editor/hooks/useDraftManager.js +57 -3
- package/dist/editor/hooks/useDraftManager.js.map +1 -1
- package/dist/editor/hooks/useEntryManager.d.ts.map +1 -1
- package/dist/editor/hooks/useEntryManager.js +25 -7
- package/dist/editor/hooks/useEntryManager.js.map +1 -1
- package/dist/editor/hooks/useGroupManager.d.ts.map +1 -1
- package/dist/editor/hooks/useGroupManager.js +13 -2
- package/dist/editor/hooks/useGroupManager.js.map +1 -1
- package/dist/editor/hooks/usePermissionManager.d.ts.map +1 -1
- package/dist/editor/hooks/usePermissionManager.js +13 -2
- package/dist/editor/hooks/usePermissionManager.js.map +1 -1
- package/dist/editor/media/AssetCard.d.ts.map +1 -1
- package/dist/editor/media/AssetCard.js +23 -2
- package/dist/editor/media/AssetCard.js.map +1 -1
- package/dist/editor/relative-time.d.ts +8 -0
- package/dist/editor/relative-time.d.ts.map +1 -0
- package/dist/editor/relative-time.js +26 -0
- package/dist/editor/relative-time.js.map +1 -0
- package/dist/git-manager.d.ts.map +1 -1
- package/dist/git-manager.js +16 -2
- package/dist/git-manager.js.map +1 -1
- package/dist/http/handler.d.ts.map +1 -1
- package/dist/http/handler.js +20 -9
- package/dist/http/handler.js.map +1 -1
- package/dist/http/router.d.ts.map +1 -1
- package/dist/http/router.js +2 -0
- package/dist/http/router.js.map +1 -1
- package/dist/paths/branch-name.d.ts +20 -0
- package/dist/paths/branch-name.d.ts.map +1 -0
- package/dist/paths/branch-name.js +24 -0
- package/dist/paths/branch-name.js.map +1 -0
- package/dist/paths/branch.d.ts +2 -8
- package/dist/paths/branch.d.ts.map +1 -1
- package/dist/paths/branch.js +6 -12
- package/dist/paths/branch.js.map +1 -1
- package/dist/paths/index.d.ts +2 -1
- package/dist/paths/index.d.ts.map +1 -1
- package/dist/paths/index.js +4 -1
- package/dist/paths/index.js.map +1 -1
- package/dist/schema/schema-store.d.ts +98 -0
- package/dist/schema/schema-store.d.ts.map +1 -1
- package/dist/schema/schema-store.js +187 -29
- package/dist/schema/schema-store.js.map +1 -1
- package/dist/services.js +9 -1
- package/dist/services.js.map +1 -1
- package/dist/task-queue/index.d.ts +2 -2
- package/dist/task-queue/index.d.ts.map +1 -1
- package/dist/task-queue/index.js +1 -1
- package/dist/task-queue/index.js.map +1 -1
- package/dist/task-queue/task-queue.d.ts +37 -1
- package/dist/task-queue/task-queue.d.ts.map +1 -1
- package/dist/task-queue/task-queue.js +104 -0
- package/dist/task-queue/task-queue.js.map +1 -1
- package/dist/task-queue/types.d.ts +11 -0
- package/dist/task-queue/types.d.ts.map +1 -1
- package/dist/types.d.ts +54 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/occ-json-write.d.ts.map +1 -1
- package/dist/utils/occ-json-write.js +7 -0
- package/dist/utils/occ-json-write.js.map +1 -1
- package/dist/utils/provisioning-lock.d.ts +19 -0
- package/dist/utils/provisioning-lock.d.ts.map +1 -1
- package/dist/utils/provisioning-lock.js +26 -0
- package/dist/utils/provisioning-lock.js.map +1 -1
- package/dist/validation/reference-validator.d.ts +17 -1
- package/dist/validation/reference-validator.d.ts.map +1 -1
- package/dist/validation/reference-validator.js +33 -10
- package/dist/validation/reference-validator.js.map +1 -1
- package/dist/worker/cms-worker.d.ts +74 -0
- package/dist/worker/cms-worker.d.ts.map +1 -1
- package/dist/worker/cms-worker.js +542 -29
- package/dist/worker/cms-worker.js.map +1 -1
- package/dist/worker/task-queue.d.ts +3 -3
- package/dist/worker/task-queue.d.ts.map +1 -1
- package/dist/worker/task-queue.js +2 -2
- package/dist/worker/task-queue.js.map +1 -1
- package/dist/worker/worker-status.d.ts +48 -0
- package/dist/worker/worker-status.d.ts.map +1 -0
- package/dist/worker/worker-status.js +53 -0
- package/dist/worker/worker-status.js.map +1 -0
- package/package.json +1 -1
|
@@ -11,8 +11,8 @@ import type { PermissionPath } from '../types';
|
|
|
11
11
|
* Any path containing '..' or other traversal sequences will be rejected.
|
|
12
12
|
*/
|
|
13
13
|
export declare const PermissionsFileSchema: z.ZodObject<{
|
|
14
|
-
version: z.
|
|
15
|
-
|
|
14
|
+
version: z.ZodOptional<z.ZodNumber>;
|
|
15
|
+
writeId: z.ZodOptional<z.ZodString>;
|
|
16
16
|
updatedAt: z.ZodString;
|
|
17
17
|
updatedBy: z.ZodType<CanopyUserId>;
|
|
18
18
|
pathPermissions: z.ZodArray<z.ZodObject<{
|
|
@@ -81,7 +81,6 @@ export declare const PermissionsFileSchema: z.ZodObject<{
|
|
|
81
81
|
} | undefined;
|
|
82
82
|
}>, "many">;
|
|
83
83
|
}, "strip", z.ZodTypeAny, {
|
|
84
|
-
version: 1;
|
|
85
84
|
updatedAt: string;
|
|
86
85
|
updatedBy: string;
|
|
87
86
|
pathPermissions: {
|
|
@@ -101,9 +100,9 @@ export declare const PermissionsFileSchema: z.ZodObject<{
|
|
|
101
100
|
allowedGroups?: string[] | undefined;
|
|
102
101
|
} | undefined;
|
|
103
102
|
}[];
|
|
104
|
-
|
|
103
|
+
version?: number | undefined;
|
|
104
|
+
writeId?: string | undefined;
|
|
105
105
|
}, {
|
|
106
|
-
version: 1;
|
|
107
106
|
updatedAt: string;
|
|
108
107
|
updatedBy: string;
|
|
109
108
|
pathPermissions: {
|
|
@@ -123,11 +122,13 @@ export declare const PermissionsFileSchema: z.ZodObject<{
|
|
|
123
122
|
allowedGroups?: string[] | undefined;
|
|
124
123
|
} | undefined;
|
|
125
124
|
}[];
|
|
126
|
-
|
|
125
|
+
version?: number | undefined;
|
|
126
|
+
writeId?: string | undefined;
|
|
127
127
|
}>;
|
|
128
128
|
export type PermissionsFile = z.infer<typeof PermissionsFileSchema>;
|
|
129
129
|
/**
|
|
130
|
-
* Default permissions file
|
|
130
|
+
* Default permissions file. Omits `version`/`writeId` — the writer
|
|
131
|
+
* (mutateSettingsJsonFile via writeOccJsonFile) manages those.
|
|
131
132
|
*/
|
|
132
133
|
export declare function createDefaultPermissionsFile(userId: CanopyUserId): PermissionsFile;
|
|
133
134
|
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/authorization/permissions/schema.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,YAAY,EAAiB,MAAM,aAAa,CAAA;AAE9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAyB9C;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB;;;;
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/authorization/permissions/schema.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,YAAY,EAAiB,MAAM,aAAa,CAAA;AAE9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAyB9C;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB;;;;eAQP,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAShD,CAAA;AAEF,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAA;AAEnE;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,YAAY,GAAG,eAAe,CAMlF"}
|
|
@@ -31,8 +31,12 @@ const permissionPathSchema = z
|
|
|
31
31
|
* Any path containing '..' or other traversal sequences will be rejected.
|
|
32
32
|
*/
|
|
33
33
|
export const PermissionsFileSchema = z.object({
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
// Managed by writeOccJsonFile (see authorization/settings-file-store.ts) —
|
|
35
|
+
// the single OCC counter for this file. Optional so a hand-written file
|
|
36
|
+
// with no version field parses as version 0. A legacy `contentVersion`
|
|
37
|
+
// field (if present) is silently stripped by this non-strict zod parse.
|
|
38
|
+
version: z.number().int().nonnegative().optional(),
|
|
39
|
+
writeId: z.string().optional(),
|
|
36
40
|
updatedAt: z.string().datetime(),
|
|
37
41
|
updatedBy: z.string(),
|
|
38
42
|
pathPermissions: z.array(z.object({
|
|
@@ -43,11 +47,11 @@ export const PermissionsFileSchema = z.object({
|
|
|
43
47
|
})),
|
|
44
48
|
});
|
|
45
49
|
/**
|
|
46
|
-
* Default permissions file
|
|
50
|
+
* Default permissions file. Omits `version`/`writeId` — the writer
|
|
51
|
+
* (mutateSettingsJsonFile via writeOccJsonFile) manages those.
|
|
47
52
|
*/
|
|
48
53
|
export function createDefaultPermissionsFile(userId) {
|
|
49
54
|
return {
|
|
50
|
-
version: 1,
|
|
51
55
|
updatedAt: new Date().toISOString(),
|
|
52
56
|
updatedBy: userId,
|
|
53
57
|
pathPermissions: [],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/authorization/permissions/schema.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AAGnD,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IACtC,YAAY,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAA6B,CAAC,CAAC,QAAQ,EAAE;IACvE,aAAa,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAA8B,CAAC,CAAC,QAAQ,EAAE;CAC1E,CAAC,CAAA;AAEF;;GAEG;AACH,MAAM,oBAAoB,GAAG,CAAC;KAC3B,MAAM,EAAE;KACR,GAAG,CAAC,CAAC,CAAC;KACN,SAAS,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IACtB,MAAM,MAAM,GAAG,mBAAmB,CAAC,GAAG,CAAC,CAAA;IACvC,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACf,GAAG,CAAC,QAAQ,CAAC;YACX,IAAI,EAAE,CAAC,CAAC,YAAY,CAAC,MAAM;YAC3B,OAAO,EAAE,MAAM,CAAC,KAAK;SACtB,CAAC,CAAA;QACF,OAAO,CAAC,CAAC,KAAK,CAAA;IAChB,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAA;AACpB,CAAC,CAAyC,CAAA;AAE5C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C,OAAO,EAAE,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/authorization/permissions/schema.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AAGnD,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IACtC,YAAY,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAA6B,CAAC,CAAC,QAAQ,EAAE;IACvE,aAAa,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAA8B,CAAC,CAAC,QAAQ,EAAE;CAC1E,CAAC,CAAA;AAEF;;GAEG;AACH,MAAM,oBAAoB,GAAG,CAAC;KAC3B,MAAM,EAAE;KACR,GAAG,CAAC,CAAC,CAAC;KACN,SAAS,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IACtB,MAAM,MAAM,GAAG,mBAAmB,CAAC,GAAG,CAAC,CAAA;IACvC,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACf,GAAG,CAAC,QAAQ,CAAC;YACX,IAAI,EAAE,CAAC,CAAC,YAAY,CAAC,MAAM;YAC3B,OAAO,EAAE,MAAM,CAAC,KAAK;SACtB,CAAC,CAAA;QACF,OAAO,CAAC,CAAC,KAAK,CAAA;IAChB,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAA;AACpB,CAAC,CAAyC,CAAA;AAE5C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C,2EAA2E;IAC3E,wEAAwE;IACxE,uEAAuE;IACvE,wEAAwE;IACxE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC,QAAQ,EAAE;IAClD,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC9B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,SAAS,EAAE,CAAC,CAAC,MAAM,EAA6B;IAChD,eAAe,EAAE,CAAC,CAAC,KAAK,CACtB,CAAC,CAAC,MAAM,CAAC;QACP,IAAI,EAAE,oBAAoB;QAC1B,IAAI,EAAE,sBAAsB,CAAC,QAAQ,EAAE;QACvC,IAAI,EAAE,sBAAsB,CAAC,QAAQ,EAAE;QACvC,MAAM,EAAE,sBAAsB,CAAC,QAAQ,EAAE;KAC1C,CAAC,CACH;CACF,CAAC,CAAA;AAIF;;;GAGG;AACH,MAAM,UAAU,4BAA4B,CAAC,MAAoB;IAC/D,OAAO;QACL,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,SAAS,EAAE,MAAM;QACjB,eAAe,EAAE,EAAE;KACpB,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protected base branch predicate.
|
|
3
|
+
*
|
|
4
|
+
* The base branch (the PR base — usually `main`) can never be submitted for
|
|
5
|
+
* review (both modes: submitting it would push straight to itself, bypassing
|
|
6
|
+
* review) and is read-only in the editor in prod (dev needs the base branch
|
|
7
|
+
* editable since the developer always lands there — see ARCHITECTURE.md
|
|
8
|
+
* "Protected Base Branch"). This is the single source of truth other modules
|
|
9
|
+
* key off of; do not re-derive the comparison elsewhere.
|
|
10
|
+
*/
|
|
11
|
+
import type { CanopyConfig } from '../config';
|
|
12
|
+
export interface BranchProtection {
|
|
13
|
+
/** True when branchName resolves to the configured base branch. */
|
|
14
|
+
isProtected: boolean;
|
|
15
|
+
/** True when the branch must never be submitted for review (both modes). */
|
|
16
|
+
submitBlocked: boolean;
|
|
17
|
+
/** True when the branch is read-only in the editor (prod only). */
|
|
18
|
+
readOnly: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Determine whether `branchName` is the protected base branch for `config`.
|
|
22
|
+
*
|
|
23
|
+
* Comparison is sanitization-aware: branch metadata names are sanitized
|
|
24
|
+
* (`sanitizeBranchName`) but `config.defaultBaseBranch` holds the raw git
|
|
25
|
+
* name, so both sides are sanitized before comparing.
|
|
26
|
+
*
|
|
27
|
+
* `recordedBaseBranch` (a branch's own `baseBranch` field, i.e. its recorded
|
|
28
|
+
* fork point) is an additional, independent protection clause: a branch whose
|
|
29
|
+
* fork point equals its own name IS a base workspace, regardless of what
|
|
30
|
+
* `config.defaultBaseBranch` says right now. This matters because in dev
|
|
31
|
+
* mode, `config.defaultBaseBranch` tracks live git HEAD (`refreshActiveBranch`)
|
|
32
|
+
* and can drift to a different branch after the base workspace was created --
|
|
33
|
+
* without this clause, that drift would silently un-protect the branch the
|
|
34
|
+
* base workspace was actually forked from. The clause is purely additive: it
|
|
35
|
+
* only ever adds protection the config clause didn't already grant, so a
|
|
36
|
+
* normal editing branch (`baseBranch !== name`) is never falsely protected.
|
|
37
|
+
*/
|
|
38
|
+
export declare function getBranchProtection(config: Pick<CanopyConfig, 'mode' | 'defaultBaseBranch'>, branchName: string, recordedBaseBranch?: string): BranchProtection;
|
|
39
|
+
//# sourceMappingURL=protected-branch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protected-branch.d.ts","sourceRoot":"","sources":["../../src/authorization/protected-branch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAM7C,MAAM,WAAW,gBAAgB;IAC/B,mEAAmE;IACnE,WAAW,EAAE,OAAO,CAAA;IACpB,4EAA4E;IAC5E,aAAa,EAAE,OAAO,CAAA;IACtB,mEAAmE;IACnE,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,mBAAmB,CAAC,EACxD,UAAU,EAAE,MAAM,EAClB,kBAAkB,CAAC,EAAE,MAAM,GAC1B,gBAAgB,CAWlB"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protected base branch predicate.
|
|
3
|
+
*
|
|
4
|
+
* The base branch (the PR base — usually `main`) can never be submitted for
|
|
5
|
+
* review (both modes: submitting it would push straight to itself, bypassing
|
|
6
|
+
* review) and is read-only in the editor in prod (dev needs the base branch
|
|
7
|
+
* editable since the developer always lands there — see ARCHITECTURE.md
|
|
8
|
+
* "Protected Base Branch"). This is the single source of truth other modules
|
|
9
|
+
* key off of; do not re-derive the comparison elsewhere.
|
|
10
|
+
*/
|
|
11
|
+
// branch-name, NOT branch: this module is client-reachable (editor bundle →
|
|
12
|
+
// api/guards.ts → here), and paths/branch.ts drags node:fs into the graph,
|
|
13
|
+
// which breaks adopters' production `next build`.
|
|
14
|
+
import { sanitizeBranchName } from '../paths/branch-name.js';
|
|
15
|
+
/**
|
|
16
|
+
* Determine whether `branchName` is the protected base branch for `config`.
|
|
17
|
+
*
|
|
18
|
+
* Comparison is sanitization-aware: branch metadata names are sanitized
|
|
19
|
+
* (`sanitizeBranchName`) but `config.defaultBaseBranch` holds the raw git
|
|
20
|
+
* name, so both sides are sanitized before comparing.
|
|
21
|
+
*
|
|
22
|
+
* `recordedBaseBranch` (a branch's own `baseBranch` field, i.e. its recorded
|
|
23
|
+
* fork point) is an additional, independent protection clause: a branch whose
|
|
24
|
+
* fork point equals its own name IS a base workspace, regardless of what
|
|
25
|
+
* `config.defaultBaseBranch` says right now. This matters because in dev
|
|
26
|
+
* mode, `config.defaultBaseBranch` tracks live git HEAD (`refreshActiveBranch`)
|
|
27
|
+
* and can drift to a different branch after the base workspace was created --
|
|
28
|
+
* without this clause, that drift would silently un-protect the branch the
|
|
29
|
+
* base workspace was actually forked from. The clause is purely additive: it
|
|
30
|
+
* only ever adds protection the config clause didn't already grant, so a
|
|
31
|
+
* normal editing branch (`baseBranch !== name`) is never falsely protected.
|
|
32
|
+
*/
|
|
33
|
+
export function getBranchProtection(config, branchName, recordedBaseBranch) {
|
|
34
|
+
const sanitizedName = sanitizeBranchName(branchName);
|
|
35
|
+
const isProtected = sanitizedName === sanitizeBranchName(config.defaultBaseBranch ?? 'main') ||
|
|
36
|
+
(recordedBaseBranch !== undefined && sanitizedName === sanitizeBranchName(recordedBaseBranch));
|
|
37
|
+
return {
|
|
38
|
+
isProtected,
|
|
39
|
+
submitBlocked: isProtected,
|
|
40
|
+
readOnly: isProtected && config.mode === 'prod',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=protected-branch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protected-branch.js","sourceRoot":"","sources":["../../src/authorization/protected-branch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,4EAA4E;AAC5E,2EAA2E;AAC3E,kDAAkD;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAWzD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,mBAAmB,CACjC,MAAwD,EACxD,UAAkB,EAClB,kBAA2B;IAE3B,MAAM,aAAa,GAAG,kBAAkB,CAAC,UAAU,CAAC,CAAA;IACpD,MAAM,WAAW,GACf,aAAa,KAAK,kBAAkB,CAAC,MAAM,CAAC,iBAAiB,IAAI,MAAM,CAAC;QACxE,CAAC,kBAAkB,KAAK,SAAS,IAAI,aAAa,KAAK,kBAAkB,CAAC,kBAAkB,CAAC,CAAC,CAAA;IAEhG,OAAO;QACL,WAAW;QACX,aAAa,EAAE,WAAW;QAC1B,QAAQ,EAAE,WAAW,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM;KAChD,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-host layered locking for settings JSON files (permissions.json,
|
|
3
|
+
* groups.json) in the settings workspace — a single global orphan-git-branch
|
|
4
|
+
* checkout at `{settingsRoot}` shared by every branch (see
|
|
5
|
+
* `api/settings-helpers.ts`'s `getSettingsBranchContext`).
|
|
6
|
+
*
|
|
7
|
+
* Without this helper, the write path is a classic unprotected TOCTOU: load
|
|
8
|
+
* -> compare -> mutate -> write with no lock spanning the cycle, so two warm
|
|
9
|
+
* Lambda containers (separate NFS clients on EFS) can each read the same
|
|
10
|
+
* pre-mutation file and have the second write silently clobber the first.
|
|
11
|
+
* `mutateSettingsJsonFile` closes that with the standard 3-layer recipe from
|
|
12
|
+
* docs/concurrency.md, structured identically to {@link CommentStore}'s
|
|
13
|
+
* `withMutation` (see comment-store.ts) and `BranchMetadataFileManager.save()`
|
|
14
|
+
* (see branch-metadata.ts):
|
|
15
|
+
*
|
|
16
|
+
* 1. {@link withLock} - an in-process FIFO mutex keyed by the resolved file
|
|
17
|
+
* path. Deterministic same-process serialization.
|
|
18
|
+
* 2. {@link withOccFileLock} - a server-enforced, cross-process/cross-host
|
|
19
|
+
* lock (proper-lockfile, mkdir-based), immune to NFS client dentry/
|
|
20
|
+
* attribute caching. This is the actual fix for a lost permission/group
|
|
21
|
+
* edit across two warm Lambda containers on EFS.
|
|
22
|
+
* 3. {@link withOccRetry} around {@link writeOccJsonFile} - version/writeId
|
|
23
|
+
* based optimistic concurrency control, reloading the file fresh on
|
|
24
|
+
* EVERY retry attempt. With layers 1-2 in place this is defense-in-depth
|
|
25
|
+
* (e.g. a stale process from a rolling deploy writing without the lock),
|
|
26
|
+
* not the primary safety mechanism.
|
|
27
|
+
*
|
|
28
|
+
* `writeOccJsonFile`'s managed `version`/`writeId` pair is now THE single
|
|
29
|
+
* counter for these files: the old hand-rolled `version: 1` format literal
|
|
30
|
+
* and the separate `contentVersion` field are gone. A pre-existing file on
|
|
31
|
+
* disk that still says `"version": 1` simply reads as OCC version 1 and
|
|
32
|
+
* continues incrementing from there; a leftover `contentVersion` key is
|
|
33
|
+
* silently stripped by the (non-strict, extra-keys-stripped) zod parse.
|
|
34
|
+
*
|
|
35
|
+
* Three caveats specific to these files, on top of the generic guarantee
|
|
36
|
+
* documented on `utils/occ-json-write.ts`:
|
|
37
|
+
*
|
|
38
|
+
* (a) UNLIKE comments.json/branch.json (which never leave the branch
|
|
39
|
+
* workspace), permissions.json/groups.json are git-committed on the
|
|
40
|
+
* settings orphan branch. `commitSettings()` (api/settings-helpers.ts)
|
|
41
|
+
* calls `commitToSettingsBranch`, whose `pullCurrentBranch()` merge runs
|
|
42
|
+
* AFTER this helper has released its lock, and that merge can rewrite
|
|
43
|
+
* the file's `version` from upstream. So, unlike branch.json, `version`
|
|
44
|
+
* here is NOT guaranteed monotonic — it remains a correctness aid (it
|
|
45
|
+
* still catches the common same-host and short-window cross-host races)
|
|
46
|
+
* but is advisory/defense-in-depth, not a hard guarantee. The LOCKFILE
|
|
47
|
+
* (layer 2) is the actual cross-host correctness mechanism.
|
|
48
|
+
* (b) The git commit+push deliberately happens OUTSIDE this lock (mirrors
|
|
49
|
+
* branch-metadata.ts keeping registry invalidation outside the lock it
|
|
50
|
+
* protects): committing/pushing is comparatively slow network/process
|
|
51
|
+
* I/O, and holding a mkdir-based lock across it would serialize
|
|
52
|
+
* unrelated requests behind that I/O for no correctness gain — only the
|
|
53
|
+
* write to the working tree needs the lock.
|
|
54
|
+
* (c) Because commit+push is outside the lock, two writers' save-then-commit
|
|
55
|
+
* sequences can interleave (A saves+commits+pushes, B's save lands and
|
|
56
|
+
* commits before A's push is visible, or similar), and the second
|
|
57
|
+
* `git commit` can run against an already-clean tree. Verified benign
|
|
58
|
+
* with simple-git 3.36: its task-error detection depends on stderr
|
|
59
|
+
* output, and a clean-tree `git commit` exits 1 with "nothing to
|
|
60
|
+
* commit" on STDOUT only, so `git.commit()` resolves rather than
|
|
61
|
+
* throwing. Re-verify this on any simple-git upgrade.
|
|
62
|
+
*/
|
|
63
|
+
import { type OccWriteResult } from '../utils/occ-json-write';
|
|
64
|
+
/**
|
|
65
|
+
* Thrown when the lock/OCC-retry stack in {@link mutateSettingsJsonFile}
|
|
66
|
+
* exhausts every attempt without landing a write — the file is busy with
|
|
67
|
+
* another writer. Callers translate this into a 409 ("try again").
|
|
68
|
+
*/
|
|
69
|
+
export declare class SettingsFileConflictError extends Error {
|
|
70
|
+
constructor(message?: string);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Thrown by a caller's `mutate` callback when an app-level
|
|
74
|
+
* `expectedContentVersion` sent by a client doesn't match the file's current
|
|
75
|
+
* `version`. A different concern from {@link SettingsFileConflictError}:
|
|
76
|
+
* this is a real edit conflict the user must resolve by reloading, not
|
|
77
|
+
* transient lock contention a retry can fix — and indeed it never IS
|
|
78
|
+
* retried, since {@link withOccRetry} only recognizes
|
|
79
|
+
* {@link OccWriteConflictError} as retryable, so this propagates on the
|
|
80
|
+
* very first attempt.
|
|
81
|
+
*/
|
|
82
|
+
export declare class SettingsVersionConflictError extends Error {
|
|
83
|
+
constructor(message?: string);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Structural shape `mutateSettingsJsonFile` needs from a parsed settings
|
|
87
|
+
* file: just enough to read the OCC version off it without resorting to
|
|
88
|
+
* `any`. Concrete file types (e.g. `PermissionsFile`, `GroupsFile`) satisfy
|
|
89
|
+
* this automatically since `version` is optional on both.
|
|
90
|
+
*/
|
|
91
|
+
interface VersionedSettingsFile {
|
|
92
|
+
version?: number;
|
|
93
|
+
}
|
|
94
|
+
export interface MutateSettingsFileOptions<TFile extends VersionedSettingsFile> {
|
|
95
|
+
/** Path to the settings JSON file (resolved internally; need not be absolute). */
|
|
96
|
+
filePath: string;
|
|
97
|
+
/** JSON.parse + zod-parse the raw file contents. Throws propagate untouched (never retried). */
|
|
98
|
+
parse: (raw: string) => TFile;
|
|
99
|
+
/**
|
|
100
|
+
* Compute the next payload from the current parsed file (`null` on
|
|
101
|
+
* ENOENT) and the version to write it under. Return `null` for a
|
|
102
|
+
* deliberate no-op — the write is skipped entirely. Called once per
|
|
103
|
+
* retry attempt against freshly reloaded state, so it must be safe to
|
|
104
|
+
* call more than once; anything it throws (besides the retried
|
|
105
|
+
* `OccWriteConflictError`, which it should never throw itself) propagates
|
|
106
|
+
* out of `mutateSettingsJsonFile` untouched.
|
|
107
|
+
*/
|
|
108
|
+
mutate: (current: TFile | null, version: number) => Record<string, unknown> | null | Promise<Record<string, unknown> | null>;
|
|
109
|
+
/** Forwarded to writeOccJsonFile. Pass 0 in tests. */
|
|
110
|
+
settleMs?: number;
|
|
111
|
+
/** Forwarded to withOccRetry. */
|
|
112
|
+
maxAttempts?: number;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Run a load -> mutate -> write cycle for a settings JSON file under the
|
|
116
|
+
* full lock + OCC-retry stack described in the module doc comment above. A
|
|
117
|
+
* conflict that survives every retry surfaces as
|
|
118
|
+
* {@link SettingsFileConflictError}; everything else — including
|
|
119
|
+
* {@link SettingsVersionConflictError} thrown by `mutate`, and any
|
|
120
|
+
* parse/validation error — propagates untouched.
|
|
121
|
+
*
|
|
122
|
+
* Returns the `writeOccJsonFile` result, or `null` if `mutate` chose a
|
|
123
|
+
* no-op (no write happened).
|
|
124
|
+
*/
|
|
125
|
+
export declare function mutateSettingsJsonFile<TFile extends VersionedSettingsFile>(opts: MutateSettingsFileOptions<TFile>): Promise<OccWriteResult | null>;
|
|
126
|
+
export {};
|
|
127
|
+
//# sourceMappingURL=settings-file-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-file-store.d.ts","sourceRoot":"","sources":["../../src/authorization/settings-file-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AAMH,OAAO,EAKL,KAAK,cAAc,EACpB,MAAM,yBAAyB,CAAA;AAGhC;;;;GAIG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;gBACtC,OAAO,SAAwC;CAI5D;AAED;;;;;;;;;GASG;AACH,qBAAa,4BAA6B,SAAQ,KAAK;gBACzC,OAAO,SAAyE;CAI7F;AAED;;;;;GAKG;AACH,UAAU,qBAAqB;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,yBAAyB,CAAC,KAAK,SAAS,qBAAqB;IAC5E,kFAAkF;IAClF,QAAQ,EAAE,MAAM,CAAA;IAChB,gGAAgG;IAChG,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,KAAK,CAAA;IAC7B;;;;;;;;OAQG;IACH,MAAM,EAAE,CACN,OAAO,EAAE,KAAK,GAAG,IAAI,EACrB,OAAO,EAAE,MAAM,KACZ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAA;IAC7E,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iCAAiC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAgCD;;;;;;;;;;GAUG;AACH,wBAAsB,sBAAsB,CAAC,KAAK,SAAS,qBAAqB,EAC9E,IAAI,EAAE,yBAAyB,CAAC,KAAK,CAAC,GACrC,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CA6BhC"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-host layered locking for settings JSON files (permissions.json,
|
|
3
|
+
* groups.json) in the settings workspace — a single global orphan-git-branch
|
|
4
|
+
* checkout at `{settingsRoot}` shared by every branch (see
|
|
5
|
+
* `api/settings-helpers.ts`'s `getSettingsBranchContext`).
|
|
6
|
+
*
|
|
7
|
+
* Without this helper, the write path is a classic unprotected TOCTOU: load
|
|
8
|
+
* -> compare -> mutate -> write with no lock spanning the cycle, so two warm
|
|
9
|
+
* Lambda containers (separate NFS clients on EFS) can each read the same
|
|
10
|
+
* pre-mutation file and have the second write silently clobber the first.
|
|
11
|
+
* `mutateSettingsJsonFile` closes that with the standard 3-layer recipe from
|
|
12
|
+
* docs/concurrency.md, structured identically to {@link CommentStore}'s
|
|
13
|
+
* `withMutation` (see comment-store.ts) and `BranchMetadataFileManager.save()`
|
|
14
|
+
* (see branch-metadata.ts):
|
|
15
|
+
*
|
|
16
|
+
* 1. {@link withLock} - an in-process FIFO mutex keyed by the resolved file
|
|
17
|
+
* path. Deterministic same-process serialization.
|
|
18
|
+
* 2. {@link withOccFileLock} - a server-enforced, cross-process/cross-host
|
|
19
|
+
* lock (proper-lockfile, mkdir-based), immune to NFS client dentry/
|
|
20
|
+
* attribute caching. This is the actual fix for a lost permission/group
|
|
21
|
+
* edit across two warm Lambda containers on EFS.
|
|
22
|
+
* 3. {@link withOccRetry} around {@link writeOccJsonFile} - version/writeId
|
|
23
|
+
* based optimistic concurrency control, reloading the file fresh on
|
|
24
|
+
* EVERY retry attempt. With layers 1-2 in place this is defense-in-depth
|
|
25
|
+
* (e.g. a stale process from a rolling deploy writing without the lock),
|
|
26
|
+
* not the primary safety mechanism.
|
|
27
|
+
*
|
|
28
|
+
* `writeOccJsonFile`'s managed `version`/`writeId` pair is now THE single
|
|
29
|
+
* counter for these files: the old hand-rolled `version: 1` format literal
|
|
30
|
+
* and the separate `contentVersion` field are gone. A pre-existing file on
|
|
31
|
+
* disk that still says `"version": 1` simply reads as OCC version 1 and
|
|
32
|
+
* continues incrementing from there; a leftover `contentVersion` key is
|
|
33
|
+
* silently stripped by the (non-strict, extra-keys-stripped) zod parse.
|
|
34
|
+
*
|
|
35
|
+
* Three caveats specific to these files, on top of the generic guarantee
|
|
36
|
+
* documented on `utils/occ-json-write.ts`:
|
|
37
|
+
*
|
|
38
|
+
* (a) UNLIKE comments.json/branch.json (which never leave the branch
|
|
39
|
+
* workspace), permissions.json/groups.json are git-committed on the
|
|
40
|
+
* settings orphan branch. `commitSettings()` (api/settings-helpers.ts)
|
|
41
|
+
* calls `commitToSettingsBranch`, whose `pullCurrentBranch()` merge runs
|
|
42
|
+
* AFTER this helper has released its lock, and that merge can rewrite
|
|
43
|
+
* the file's `version` from upstream. So, unlike branch.json, `version`
|
|
44
|
+
* here is NOT guaranteed monotonic — it remains a correctness aid (it
|
|
45
|
+
* still catches the common same-host and short-window cross-host races)
|
|
46
|
+
* but is advisory/defense-in-depth, not a hard guarantee. The LOCKFILE
|
|
47
|
+
* (layer 2) is the actual cross-host correctness mechanism.
|
|
48
|
+
* (b) The git commit+push deliberately happens OUTSIDE this lock (mirrors
|
|
49
|
+
* branch-metadata.ts keeping registry invalidation outside the lock it
|
|
50
|
+
* protects): committing/pushing is comparatively slow network/process
|
|
51
|
+
* I/O, and holding a mkdir-based lock across it would serialize
|
|
52
|
+
* unrelated requests behind that I/O for no correctness gain — only the
|
|
53
|
+
* write to the working tree needs the lock.
|
|
54
|
+
* (c) Because commit+push is outside the lock, two writers' save-then-commit
|
|
55
|
+
* sequences can interleave (A saves+commits+pushes, B's save lands and
|
|
56
|
+
* commits before A's push is visible, or similar), and the second
|
|
57
|
+
* `git commit` can run against an already-clean tree. Verified benign
|
|
58
|
+
* with simple-git 3.36: its task-error detection depends on stderr
|
|
59
|
+
* output, and a clean-tree `git commit` exits 1 with "nothing to
|
|
60
|
+
* commit" on STDOUT only, so `git.commit()` resolves rather than
|
|
61
|
+
* throwing. Re-verify this on any simple-git upgrade.
|
|
62
|
+
*/
|
|
63
|
+
import fs from 'node:fs/promises';
|
|
64
|
+
import path from 'node:path';
|
|
65
|
+
import { withLock } from '../utils/async-mutex.js';
|
|
66
|
+
import { writeOccJsonFile, withOccRetry, withOccFileLock, OccWriteConflictError, } from '../utils/occ-json-write.js';
|
|
67
|
+
import { isNotFoundError } from '../utils/error.js';
|
|
68
|
+
/**
|
|
69
|
+
* Thrown when the lock/OCC-retry stack in {@link mutateSettingsJsonFile}
|
|
70
|
+
* exhausts every attempt without landing a write — the file is busy with
|
|
71
|
+
* another writer. Callers translate this into a 409 ("try again").
|
|
72
|
+
*/
|
|
73
|
+
export class SettingsFileConflictError extends Error {
|
|
74
|
+
constructor(message = 'Settings are busy, please try again') {
|
|
75
|
+
super(message);
|
|
76
|
+
this.name = 'SettingsFileConflictError';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Thrown by a caller's `mutate` callback when an app-level
|
|
81
|
+
* `expectedContentVersion` sent by a client doesn't match the file's current
|
|
82
|
+
* `version`. A different concern from {@link SettingsFileConflictError}:
|
|
83
|
+
* this is a real edit conflict the user must resolve by reloading, not
|
|
84
|
+
* transient lock contention a retry can fix — and indeed it never IS
|
|
85
|
+
* retried, since {@link withOccRetry} only recognizes
|
|
86
|
+
* {@link OccWriteConflictError} as retryable, so this propagates on the
|
|
87
|
+
* very first attempt.
|
|
88
|
+
*/
|
|
89
|
+
export class SettingsVersionConflictError extends Error {
|
|
90
|
+
constructor(message = 'Settings were modified by another user. Please reload and try again.') {
|
|
91
|
+
super(message);
|
|
92
|
+
this.name = 'SettingsVersionConflictError';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Reload the file fresh and report the version to feed both `mutate()` and
|
|
97
|
+
* `writeOccJsonFile`'s `expectedVersion`.
|
|
98
|
+
*
|
|
99
|
+
* ENOENT is the ONLY case that maps to a `null` `occExpectedVersion` (the
|
|
100
|
+
* create-via-link path in {@link writeOccJsonFile}): an EXISTING file —
|
|
101
|
+
* even one hand-written without a `version` field — maps to `0` and takes
|
|
102
|
+
* the rename-based update path instead. Conflating the two would make
|
|
103
|
+
* `writeOccJsonFile` attempt a `link()` create against a file that already
|
|
104
|
+
* exists, spuriously failing with EEXIST instead of doing a normal
|
|
105
|
+
* versioned update. (Same contract as `CommentStore`'s `loadWithVersion` in
|
|
106
|
+
* comment-store.ts.)
|
|
107
|
+
*/
|
|
108
|
+
async function loadCurrent(filePath, parse) {
|
|
109
|
+
let raw;
|
|
110
|
+
try {
|
|
111
|
+
raw = await fs.readFile(filePath, 'utf-8');
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
if (isNotFoundError(err)) {
|
|
115
|
+
return { current: null, occExpectedVersion: null };
|
|
116
|
+
}
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
const parsed = parse(raw);
|
|
120
|
+
return { current: parsed, occExpectedVersion: parsed.version ?? 0 };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Run a load -> mutate -> write cycle for a settings JSON file under the
|
|
124
|
+
* full lock + OCC-retry stack described in the module doc comment above. A
|
|
125
|
+
* conflict that survives every retry surfaces as
|
|
126
|
+
* {@link SettingsFileConflictError}; everything else — including
|
|
127
|
+
* {@link SettingsVersionConflictError} thrown by `mutate`, and any
|
|
128
|
+
* parse/validation error — propagates untouched.
|
|
129
|
+
*
|
|
130
|
+
* Returns the `writeOccJsonFile` result, or `null` if `mutate` chose a
|
|
131
|
+
* no-op (no write happened).
|
|
132
|
+
*/
|
|
133
|
+
export async function mutateSettingsJsonFile(opts) {
|
|
134
|
+
const resolved = path.resolve(opts.filePath);
|
|
135
|
+
try {
|
|
136
|
+
return await withLock(resolved, () => withOccFileLock(resolved, () => withOccRetry(async () => {
|
|
137
|
+
const { current, occExpectedVersion } = await loadCurrent(resolved, opts.parse);
|
|
138
|
+
const version = occExpectedVersion ?? 0;
|
|
139
|
+
const payload = await opts.mutate(current, version);
|
|
140
|
+
if (payload === null) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
return writeOccJsonFile(resolved, payload, {
|
|
144
|
+
expectedVersion: occExpectedVersion,
|
|
145
|
+
settleMs: opts.settleMs,
|
|
146
|
+
});
|
|
147
|
+
}, { maxAttempts: opts.maxAttempts })));
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
if (err instanceof OccWriteConflictError) {
|
|
151
|
+
throw new SettingsFileConflictError();
|
|
152
|
+
}
|
|
153
|
+
throw err;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=settings-file-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-file-store.js","sourceRoot":"","sources":["../../src/authorization/settings-file-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AAEH,OAAO,EAAE,MAAM,kBAAkB,CAAA;AACjC,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAC/C,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,eAAe,EACf,qBAAqB,GAEtB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAEhD;;;;GAIG;AACH,MAAM,OAAO,yBAA0B,SAAQ,KAAK;IAClD,YAAY,OAAO,GAAG,qCAAqC;QACzD,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAA;IACzC,CAAC;CACF;AAED;;;;;;;;;GASG;AACH,MAAM,OAAO,4BAA6B,SAAQ,KAAK;IACrD,YAAY,OAAO,GAAG,sEAAsE;QAC1F,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,8BAA8B,CAAA;IAC5C,CAAC;CACF;AAoCD;;;;;;;;;;;;GAYG;AACH,KAAK,UAAU,WAAW,CACxB,QAAgB,EAChB,KAA6B;IAE7B,IAAI,GAAW,CAAA;IACf,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;IAC5C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,eAAe,CAAC,GAAG,CAAC,EAAE,CAAC;YACzB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,kBAAkB,EAAE,IAAI,EAAE,CAAA;QACpD,CAAC;QACD,MAAM,GAAG,CAAA;IACX,CAAC;IACD,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,CAAA;IACzB,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,CAAC,OAAO,IAAI,CAAC,EAAE,CAAA;AACrE,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,IAAsC;IAEtC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IAE5C,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE,CACnC,eAAe,CAAC,QAAQ,EAAE,GAAG,EAAE,CAC7B,YAAY,CACV,KAAK,IAAI,EAAE;YACT,MAAM,EAAE,OAAO,EAAE,kBAAkB,EAAE,GAAG,MAAM,WAAW,CAAC,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;YAC/E,MAAM,OAAO,GAAG,kBAAkB,IAAI,CAAC,CAAA;YACvC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;YACnD,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;gBACrB,OAAO,IAAI,CAAA;YACb,CAAC;YACD,OAAO,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE;gBACzC,eAAe,EAAE,kBAAkB;gBACnC,QAAQ,EAAE,IAAI,CAAC,QAAQ;aACxB,CAAC,CAAA;QACJ,CAAC,EACD,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAClC,CACF,CACF,CAAA;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,qBAAqB,EAAE,CAAC;YACzC,MAAM,IAAI,yBAAyB,EAAE,CAAA;QACvC,CAAC;QACD,MAAM,GAAG,CAAA;IACX,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { BranchMetadata } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Admin-facing health classification of every directory under a branches
|
|
4
|
+
* root (`baseRoot`), including ones the {@link BranchRegistry} quarantines
|
|
5
|
+
* out of normal listings. Two stuck states have no in-product recovery
|
|
6
|
+
* without this: a branch dir whose `.canopy-meta/branch.json` is corrupt
|
|
7
|
+
* (registry now silently drops it — see branch-registry.ts's
|
|
8
|
+
* `scanBranchDirectories` quarantine), and an orphan dir with no
|
|
9
|
+
* `branch.json` at all (left behind by a partial delete crash — see
|
|
10
|
+
* `api/branch.ts`'s delete handler). Both are invisible to admins in prod,
|
|
11
|
+
* who have no filesystem access.
|
|
12
|
+
*
|
|
13
|
+
* `scanBranchHealth` mirrors the registry's own directory-listing rules
|
|
14
|
+
* (skip non-directories and dot-prefixed names) so its classification never
|
|
15
|
+
* disagrees with what the registry itself would show as "missing".
|
|
16
|
+
*/
|
|
17
|
+
export type BranchHealthKind = 'healthy' | 'corrupt-metadata' | 'orphan';
|
|
18
|
+
export interface BranchHealthEntry {
|
|
19
|
+
dirName: string;
|
|
20
|
+
kind: BranchHealthKind;
|
|
21
|
+
/** True when this directory is the sanitized base-branch directory. */
|
|
22
|
+
isBaseBranch?: boolean;
|
|
23
|
+
/** healthy only */
|
|
24
|
+
branch?: BranchMetadata;
|
|
25
|
+
/** corrupt-metadata only: message describing why the file failed to load. */
|
|
26
|
+
parseError?: string;
|
|
27
|
+
/** corrupt-metadata only: branch.json's mtime, ISO. Omitted if branch.json itself couldn't be stat'd. */
|
|
28
|
+
metaMtime?: string;
|
|
29
|
+
/** orphan only: whether a `.git` directory is present (partial clone vs. fully-provisioned). */
|
|
30
|
+
hasGitDir?: boolean;
|
|
31
|
+
/** orphan only: the directory's own mtime, ISO. */
|
|
32
|
+
dirMtime?: string;
|
|
33
|
+
/** orphan only: age of the directory's mtime in ms, clamped to >= 0. */
|
|
34
|
+
ageMs?: number;
|
|
35
|
+
/**
|
|
36
|
+
* [H1] Present iff the dir's provisioning init-lock marker exists on disk,
|
|
37
|
+
* for orphan and corrupt-metadata entries only. Presence alone means
|
|
38
|
+
* nothing -- a crashed provisioner's lock lingers forever (proper-lockfile
|
|
39
|
+
* stale locks are only reaped by a later acquisition attempt, and a health
|
|
40
|
+
* scan never acquires). Freshness (`ageMs`) is the actual signal admin
|
|
41
|
+
* actions gate on.
|
|
42
|
+
*/
|
|
43
|
+
provisioningLock?: {
|
|
44
|
+
mtime: string;
|
|
45
|
+
ageMs: number;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The on-disk path of the cross-process provisioning lock marker for a
|
|
50
|
+
* given branch directory, matching `branch-workspace.ts`'s
|
|
51
|
+
* `ensureGitWorkspace()` exactly:
|
|
52
|
+
*
|
|
53
|
+
* ```ts
|
|
54
|
+
* acquireProvisioningLock(
|
|
55
|
+
* path.dirname(branchRoot), // === baseRoot
|
|
56
|
+
* `.${path.basename(branchRoot)}.init.lock`,
|
|
57
|
+
* )
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* `acquireProvisioningLock` passes this name as `lockfilePath`, which
|
|
61
|
+
* overrides proper-lockfile's default `${target}.lock` naming, so the lock
|
|
62
|
+
* marker itself (a directory, mkdir-based) lives at exactly this path -- no
|
|
63
|
+
* extra `.lock` suffix.
|
|
64
|
+
*/
|
|
65
|
+
export declare function provisioningLockPath(baseRoot: string, dirName: string): string;
|
|
66
|
+
/**
|
|
67
|
+
* Scan every directory under `baseRoot` and classify it as healthy,
|
|
68
|
+
* corrupt-metadata, or orphan. Never throws for a single bad directory --
|
|
69
|
+
* one dir's unreadable/unparseable metadata must not take down the whole
|
|
70
|
+
* scan (same rationale as the registry's own quarantine behavior).
|
|
71
|
+
*
|
|
72
|
+
* Tolerates a missing `baseRoot` (returns `[]`) so the admin endpoint can
|
|
73
|
+
* call this unconditionally without a pre-existence check.
|
|
74
|
+
*/
|
|
75
|
+
export declare function scanBranchHealth(baseRoot: string, opts: {
|
|
76
|
+
baseBranchName: string;
|
|
77
|
+
}): Promise<BranchHealthEntry[]>;
|
|
78
|
+
//# sourceMappingURL=branch-health.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"branch-health.d.ts","sourceRoot":"","sources":["../src/branch-health.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAK7C;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,kBAAkB,GAAG,QAAQ,CAAA;AAExE,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,gBAAgB,CAAA;IACtB,uEAAuE;IACvE,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,mBAAmB;IACnB,MAAM,CAAC,EAAE,cAAc,CAAA;IACvB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yGAAyG;IACzG,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gGAAgG;IAChG,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;CACpD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE9E;AA2BD;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;IAAE,cAAc,EAAE,MAAM,CAAA;CAAE,GAC/B,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAmG9B"}
|