canopycms 0.0.67 → 0.0.68-int.100
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 +21 -14
- package/dist/ai/handler.js +8 -0
- package/dist/api/admin-branch-health.js +17 -20
- package/dist/api/admin.d.ts +30 -9
- package/dist/api/admin.js +32 -2
- package/dist/api/assets.js +84 -76
- package/dist/api/branch-create-window.d.ts +12 -0
- package/dist/api/branch-create-window.js +13 -0
- package/dist/api/branch-review.d.ts +3 -3
- package/dist/api/branch-review.js +14 -5
- package/dist/api/branch-status.d.ts +5 -5
- package/dist/api/branch-status.js +26 -6
- package/dist/api/branch-withdraw.d.ts +2 -2
- package/dist/api/branch-withdraw.js +7 -2
- package/dist/api/branch.d.ts +15 -2
- package/dist/api/branch.js +111 -92
- package/dist/api/client.d.ts +18 -6
- package/dist/api/client.js +79 -12
- package/dist/api/content.d.ts +6 -5
- package/dist/api/content.js +44 -41
- package/dist/api/entries.js +16 -15
- package/dist/api/github-sync.d.ts +10 -1
- package/dist/api/github-sync.js +20 -10
- package/dist/api/index.d.ts +1 -1
- package/dist/api/reference-options.js +1 -1
- package/dist/api/resolve-references.js +14 -40
- package/dist/api/settings-helpers.d.ts +1 -1
- package/dist/api/settings-helpers.js +1 -4
- package/dist/api/user.d.ts +3 -0
- package/dist/api/user.js +3 -0
- package/dist/assets/asset-url.d.ts +22 -16
- package/dist/assets/asset-url.js +42 -25
- package/dist/assets/factory.d.ts +5 -0
- package/dist/assets/factory.js +5 -0
- package/dist/assets/index.d.ts +1 -1
- package/dist/assets/materialize.d.ts +122 -0
- package/dist/assets/materialize.js +305 -0
- package/dist/assets/store-local.d.ts +4 -0
- package/dist/assets/store-local.js +11 -0
- package/dist/assets/store-s3.d.ts +10 -1
- package/dist/assets/store-s3.js +55 -3
- package/dist/assets/transform-directives.d.ts +52 -13
- package/dist/assets/transform-directives.js +87 -27
- package/dist/assets/transform.d.ts +3 -3
- package/dist/assets/transform.js +13 -8
- package/dist/assets/types.d.ts +14 -1
- package/dist/auth/file-based-auth-cache.js +7 -8
- 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.d.ts +7 -1
- package/dist/branch-health.js +8 -3
- package/dist/branch-metadata.d.ts +5 -5
- package/dist/branch-metadata.js +44 -36
- package/dist/branch-provisioning.d.ts +167 -0
- package/dist/branch-provisioning.js +501 -0
- package/dist/branch-schema-cache.d.ts +5 -3
- package/dist/branch-schema-cache.js +29 -11
- package/dist/branch-sparse.d.ts +23 -0
- package/dist/branch-sparse.js +118 -0
- package/dist/branch-workspace.d.ts +42 -3
- package/dist/branch-workspace.js +240 -94
- package/dist/build/asset-refs.d.ts +93 -0
- package/dist/build/asset-refs.js +251 -0
- package/dist/build-identity.d.ts +3 -0
- package/dist/build-identity.js +13 -0
- package/dist/cli/asset-refs.d.ts +30 -0
- package/dist/cli/asset-refs.js +168 -0
- package/dist/cli/cli.d.ts +2 -1
- package/dist/cli/cli.js +4843 -1870
- package/dist/cli/generate-ai-content.js +1584 -444
- package/dist/cli/github-app-manifest.d.ts +4 -3
- package/dist/cli/github-app-manifest.js +4 -3
- package/dist/cli/init.js +30 -27
- package/dist/cli/migrate.js +12 -8
- package/dist/cli/sync.js +83 -37
- package/dist/cli/template-files/Dockerfile.cms.template +8 -0
- package/dist/cli/template-files/cdk-app.ts.template +4 -0
- package/dist/cli/template-files/cms-stack.ts.template +23 -12
- package/dist/cli/template-files/deploy-cms.yml.template +2 -0
- package/dist/config/helpers.js +1 -2
- package/dist/config/schemas/config.d.ts +19 -32
- package/dist/config/schemas/config.js +9 -11
- package/dist/config/schemas/field.js +1 -0
- package/dist/config/schemas/media.d.ts +4 -13
- package/dist/config/schemas/media.js +5 -8
- package/dist/config/schemas/url.d.ts +4 -20
- package/dist/config/schemas/url.js +6 -22
- package/dist/config/types.d.ts +30 -20
- package/dist/config/validation.d.ts +6 -0
- package/dist/config/validation.js +37 -0
- package/dist/content-listing.d.ts +12 -12
- package/dist/content-listing.js +10 -6
- package/dist/content-reader.d.ts +11 -2
- package/dist/content-reader.js +22 -14
- package/dist/content-store.d.ts +37 -9
- package/dist/content-store.js +70 -43
- package/dist/content-tree.d.ts +1 -1
- package/dist/content-tree.js +2 -2
- package/dist/context.d.ts +27 -9
- package/dist/context.js +154 -70
- package/dist/editor/BranchManager.d.ts +7 -1
- package/dist/editor/BranchManager.js +29 -14
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +2 -6
- package/dist/editor/Editor.d.ts +4 -3
- package/dist/editor/Editor.js +24 -20
- package/dist/editor/FormRenderer.js +34 -18
- package/dist/editor/admin/SystemHealthPanel.js +47 -7
- package/dist/editor/components/BranchesDrawer.d.ts +9 -0
- package/dist/editor/components/BranchesDrawer.js +4 -0
- package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
- package/dist/editor/components/NoEditPermissionNotice.js +17 -0
- package/dist/editor/context/AssetContext.d.ts +18 -35
- package/dist/editor/context/AssetContext.js +20 -29
- package/dist/editor/context/index.d.ts +1 -1
- package/dist/editor/context/index.js +1 -1
- package/dist/editor/editor-config.d.ts +1 -2
- package/dist/editor/editor-config.js +0 -23
- package/dist/editor/editor-utils.d.ts +24 -21
- package/dist/editor/editor-utils.js +75 -62
- package/dist/editor/fields/BlockField.d.ts +1 -0
- package/dist/editor/fields/BlockField.js +6 -4
- package/dist/editor/fields/CodeField.d.ts +1 -0
- package/dist/editor/fields/CodeField.js +2 -2
- package/dist/editor/fields/DateTimeField.d.ts +1 -0
- package/dist/editor/fields/DateTimeField.js +2 -2
- package/dist/editor/fields/FieldDescription.d.ts +16 -0
- package/dist/editor/fields/FieldDescription.js +11 -0
- package/dist/editor/fields/ImageField.d.ts +1 -0
- package/dist/editor/fields/ImageField.js +3 -2
- package/dist/editor/fields/InlineGroupField.js +4 -1
- package/dist/editor/fields/MarkdownField.d.ts +1 -0
- package/dist/editor/fields/MarkdownField.js +115 -20
- package/dist/editor/fields/NumberField.d.ts +1 -0
- package/dist/editor/fields/NumberField.js +2 -2
- package/dist/editor/fields/NumberListField.d.ts +1 -0
- package/dist/editor/fields/NumberListField.js +2 -2
- package/dist/editor/fields/ObjectField.d.ts +1 -0
- package/dist/editor/fields/ObjectField.js +5 -2
- package/dist/editor/fields/ReferenceField.d.ts +1 -0
- package/dist/editor/fields/ReferenceField.js +5 -4
- package/dist/editor/fields/SelectField.d.ts +1 -0
- package/dist/editor/fields/SelectField.js +2 -2
- package/dist/editor/fields/StringListField.d.ts +1 -0
- package/dist/editor/fields/StringListField.js +2 -2
- package/dist/editor/fields/TextField.d.ts +1 -0
- package/dist/editor/fields/TextField.js +2 -2
- package/dist/editor/fields/ToggleField.d.ts +1 -0
- package/dist/editor/fields/ToggleField.js +7 -4
- package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
- package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -2
- package/dist/editor/fields/mdx-jsx-support.d.ts +1 -0
- package/dist/editor/fields/mdx-jsx-support.js +137 -0
- package/dist/editor/hooks/create-branch-request.d.ts +20 -0
- package/dist/editor/hooks/create-branch-request.js +52 -0
- package/dist/editor/hooks/useBranchActions.d.ts +13 -2
- package/dist/editor/hooks/useBranchActions.js +48 -17
- package/dist/editor/hooks/useBranchManager.d.ts +11 -0
- package/dist/editor/hooks/useBranchManager.js +80 -2
- package/dist/editor/hooks/useBranchesData.d.ts +6 -0
- package/dist/editor/hooks/useBranchesData.js +9 -3
- package/dist/editor/hooks/useCommentSystem.js +25 -6
- package/dist/editor/hooks/useDraftManager.d.ts +16 -1
- package/dist/editor/hooks/useDraftManager.js +193 -36
- package/dist/editor/hooks/useEntriesData.js +3 -3
- package/dist/editor/hooks/useEntryManager.d.ts +2 -1
- package/dist/editor/hooks/useEntryManager.js +34 -19
- package/dist/editor/media/AssetCard.d.ts +1 -1
- package/dist/editor/media/crop-math.d.ts +3 -4
- package/dist/editor/media/crop-math.js +11 -22
- package/dist/editor/media/editor-image-src.d.ts +7 -0
- package/dist/editor/media/editor-image-src.js +12 -0
- package/dist/editor/preview-asset-base.d.ts +8 -0
- package/dist/editor/preview-asset-base.js +13 -0
- package/dist/editor/preview-bridge.d.ts +8 -1
- package/dist/editor/preview-bridge.js +15 -2
- package/dist/editor/preview-path.d.ts +10 -0
- package/dist/editor/preview-path.js +20 -0
- package/dist/editor/theme.js +2 -2
- package/dist/entry-schema-registry.d.ts +4 -4
- package/dist/entry-schema-registry.js +10 -6
- package/dist/entry-schema.d.ts +27 -4
- package/dist/entry-schema.js +20 -3
- package/dist/git-manager.d.ts +173 -6
- package/dist/git-manager.js +594 -76
- package/dist/github-service.d.ts +8 -0
- package/dist/github-service.js +5 -1
- package/dist/http/handler.js +37 -13
- 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/types.d.ts +2 -0
- package/dist/http/worker-not-ready.d.ts +10 -0
- package/dist/http/worker-not-ready.js +24 -0
- package/dist/operating-mode/client-unsafe-strategy.js +0 -6
- package/dist/operating-mode/types.d.ts +1 -4
- package/dist/paths/branch-name.d.ts +5 -0
- package/dist/paths/branch-name.js +9 -0
- package/dist/paths/branch.d.ts +6 -2
- package/dist/paths/branch.js +16 -10
- package/dist/paths/index.d.ts +3 -3
- package/dist/paths/index.js +3 -3
- package/dist/paths/normalize.d.ts +7 -0
- package/dist/paths/normalize.js +9 -0
- package/dist/paths/validation.js +0 -2
- package/dist/reference-resolver.d.ts +5 -21
- package/dist/reference-resolver.js +9 -44
- package/dist/resolve-canopy-user.js +2 -1
- package/dist/schema/schema-store.d.ts +54 -31
- package/dist/schema/schema-store.js +77 -31
- package/dist/server.d.ts +24 -9
- package/dist/server.js +22 -8
- package/dist/services.d.ts +18 -6
- package/dist/services.js +85 -70
- package/dist/settings-workspace.d.ts +23 -3
- package/dist/settings-workspace.js +197 -45
- 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.d.ts +12 -1
- package/dist/sync-core.js +31 -13
- package/dist/task-queue/worker-status.d.ts +8 -0
- package/dist/task-queue/worker-status.js +17 -0
- package/dist/types.d.ts +31 -2
- package/dist/utils/content-write-lock.d.ts +18 -7
- package/dist/utils/content-write-lock.js +18 -9
- package/dist/utils/debug.d.ts +8 -0
- package/dist/utils/debug.js +10 -2
- package/dist/utils/git.d.ts +32 -0
- package/dist/utils/git.js +42 -0
- package/dist/utils/occ-json-write.js +10 -2
- package/dist/utils/provision-log.d.ts +25 -0
- package/dist/utils/provision-log.js +49 -0
- package/dist/utils/provisioning-lock.d.ts +35 -5
- package/dist/utils/provisioning-lock.js +73 -12
- package/dist/utils/request-timing.d.ts +25 -0
- package/dist/utils/request-timing.js +101 -0
- package/dist/utils/sanitize-href.d.ts +8 -22
- package/dist/utils/sanitize-href.js +11 -28
- package/dist/utils/url-prefix.d.ts +30 -0
- package/dist/utils/url-prefix.js +61 -0
- package/dist/validation/entry-validator.js +9 -3
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/worker/canopy-state.d.ts +45 -0
- package/dist/worker/canopy-state.js +75 -0
- package/dist/worker/cms-worker.d.ts +8 -3
- package/dist/worker/cms-worker.js +38 -2
- package/dist/worker/git-sync.d.ts +30 -12
- package/dist/worker/git-sync.js +253 -58
- package/dist/worker/history-rewrite.d.ts +1 -1
- package/dist/worker/history-rewrite.js +1 -1
- package/dist/worker/provisioned-workspace.d.ts +35 -0
- package/dist/worker/provisioned-workspace.js +50 -0
- package/dist/worker/rebase.d.ts +1 -1
- package/dist/worker/rebase.js +95 -23
- package/dist/worker/remote-git-maintenance.d.ts +3 -0
- package/dist/worker/remote-git-maintenance.js +11 -0
- package/dist/worker/sparse-cone.d.ts +20 -0
- package/dist/worker/sparse-cone.js +132 -0
- package/dist/worker/task-runner.js +39 -4
- package/dist/worker/worker-context.d.ts +6 -1
- package/package.json +2 -1
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writing transform outputs into the store. Server-only.
|
|
3
|
+
*
|
|
4
|
+
* `storeTransform` computes one key and is shared by the authenticated raw route
|
|
5
|
+
* (api/assets.ts), `materializeAssets` (the batch run that writes every key a build references
|
|
6
|
+
* before that build is released) and canopycms-cdk's lazy transform Lambda.
|
|
7
|
+
*/
|
|
8
|
+
import { getErrorMessage, isNodeError } from '../utils/error.js';
|
|
9
|
+
import { ASSET_PREFIXES } from './asset-prefixes.js';
|
|
10
|
+
import { loadSharp } from './sharp-loader.js';
|
|
11
|
+
import { applyTransform } from './transform.js';
|
|
12
|
+
import { canonicalizeTransformPath } from './transform-directives.js';
|
|
13
|
+
/** Every transform output is stored under a content-addressed key, so it never changes. */
|
|
14
|
+
export const TRANSFORM_CACHE_CONTROL = 'public, max-age=31536000, immutable';
|
|
15
|
+
/**
|
|
16
|
+
* The object tag on every derivative the lazy transform Lambda writes. Its bucket's `assets/t/`
|
|
17
|
+
* expiry filters on it, so what `materializeAssets` writes, which the Lambda's allowlist may refuse
|
|
18
|
+
* to recompute, is never expired. canopycms-cdk's `AssetSupport` copies these two strings as
|
|
19
|
+
* literals.
|
|
20
|
+
*/
|
|
21
|
+
export const LAZY_TRANSFORM_TAG = { key: 'canopy-transform', value: 'lazy' };
|
|
22
|
+
/**
|
|
23
|
+
* Compute the transform `parsed` names from the asset's original and write it under
|
|
24
|
+
* `canonicalKey`. `parsed` must come from `canonicalizeTransformPath`, so the stored pixels
|
|
25
|
+
* always match their key. A rejection is a fact about the asset or the URL and is never worth
|
|
26
|
+
* retrying; a thrown error belongs to the store or the environment. A 404's `error` names what
|
|
27
|
+
* was missing, so a caller answering a browser replaces it.
|
|
28
|
+
*/
|
|
29
|
+
export async function storeTransform(store, parsed, canonicalKey, options = {}) {
|
|
30
|
+
const meta = await store.getMeta(parsed.hash32);
|
|
31
|
+
if (!meta) {
|
|
32
|
+
return { ok: false, status: 404, error: `Asset ${parsed.hash32} has no meta in the store` };
|
|
33
|
+
}
|
|
34
|
+
if (meta.kind !== 'raster') {
|
|
35
|
+
return { ok: false, status: 400, error: 'Not a raster asset - svg/pdf are served statically' };
|
|
36
|
+
}
|
|
37
|
+
// The slug is decorative in the URL but load-bearing in the stored key, so it must equal the
|
|
38
|
+
// asset's real slug — the parser only enforces `[a-z0-9-]+`, and any other string that passes
|
|
39
|
+
// it aliases the same image into a new cache key.
|
|
40
|
+
if (parsed.slug !== meta.slug) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
status: 404,
|
|
44
|
+
error: `Slug '${parsed.slug}' is not asset ${parsed.hash32}'s slug '${meta.slug}'`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// Without an explicit `f=`, the transform keeps the source format, so the URL's `{ext}` must be
|
|
48
|
+
// the source's real ext. The parser cannot check this: it does not know the source format.
|
|
49
|
+
const requestedFormat = parsed.directives.identity ? undefined : parsed.directives.format;
|
|
50
|
+
if (requestedFormat === undefined && parsed.ext !== meta.ext) {
|
|
51
|
+
return { ok: false, status: 400, error: 'Extension does not match the source format' };
|
|
52
|
+
}
|
|
53
|
+
const original = await store.readOriginal(parsed.hash32, meta.ext);
|
|
54
|
+
if (!original) {
|
|
55
|
+
return { ok: false, status: 404, error: `Asset ${parsed.hash32} has no original in the store` };
|
|
56
|
+
}
|
|
57
|
+
const transformed = await applyTransform({ data: original.data, ext: original.ext }, parsed.directives);
|
|
58
|
+
if (!transformed.ok) {
|
|
59
|
+
return { ok: false, status: transformed.status, error: transformed.error };
|
|
60
|
+
}
|
|
61
|
+
await store.putPublicObject({
|
|
62
|
+
key: canonicalKey,
|
|
63
|
+
data: transformed.data,
|
|
64
|
+
contentType: transformed.contentType,
|
|
65
|
+
cacheControl: TRANSFORM_CACHE_CONTROL,
|
|
66
|
+
tags: options.tags,
|
|
67
|
+
});
|
|
68
|
+
return { ok: true, data: transformed.data, contentType: transformed.contentType };
|
|
69
|
+
}
|
|
70
|
+
/** sharp cannot load in this process, so nothing missing can be produced here. */
|
|
71
|
+
export class SharpUnavailableError extends Error {
|
|
72
|
+
constructor(cause) {
|
|
73
|
+
super(`sharp failed to load, so the missing transform outputs cannot be produced in this ` +
|
|
74
|
+
`environment: ${getErrorMessage(cause)}. Install sharp for this platform and rerun.`);
|
|
75
|
+
this.name = 'SharpUnavailableError';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const DEFAULT_CONCURRENCY = 8;
|
|
79
|
+
const DEFAULT_TRANSFORM_CONCURRENCY = 2;
|
|
80
|
+
// eslint-disable-next-line security/detect-non-literal-regexp -- built from a constant
|
|
81
|
+
const STATIC_KEY_RE = new RegExp(`^${ASSET_PREFIXES.public}/[a-f0-9]{32}/[a-z0-9-]+\\.[a-z0-9]{1,10}$`);
|
|
82
|
+
const DEFAULT_LIST_THRESHOLD = 100;
|
|
83
|
+
const DEFAULT_ATTEMPTS = 3;
|
|
84
|
+
const DEFAULT_BASE_DELAY_MS = 500;
|
|
85
|
+
const TRANSIENT_NODE_CODES = new Set([
|
|
86
|
+
'ECONNRESET',
|
|
87
|
+
'ECONNREFUSED',
|
|
88
|
+
'ETIMEDOUT',
|
|
89
|
+
'EPIPE',
|
|
90
|
+
'EAI_AGAIN',
|
|
91
|
+
'EBUSY',
|
|
92
|
+
'EMFILE',
|
|
93
|
+
]);
|
|
94
|
+
const TRANSIENT_ERROR_NAMES = new Set([
|
|
95
|
+
'TimeoutError',
|
|
96
|
+
'RequestTimeout',
|
|
97
|
+
'RequestTimeoutException',
|
|
98
|
+
'SlowDown',
|
|
99
|
+
'Throttling',
|
|
100
|
+
'ThrottlingException',
|
|
101
|
+
'ServiceUnavailable',
|
|
102
|
+
'InternalError',
|
|
103
|
+
]);
|
|
104
|
+
/**
|
|
105
|
+
* @internal Exported for tests. Throttling, 5xx and dropped connections are transient. Any other
|
|
106
|
+
* 4xx (AccessDenied, NoSuchBucket) is a configuration fault that a retry only delays.
|
|
107
|
+
*/
|
|
108
|
+
export function isTransientStoreError(err) {
|
|
109
|
+
if (!(err instanceof Error))
|
|
110
|
+
return false;
|
|
111
|
+
if (isNodeError(err) && err.code && TRANSIENT_NODE_CODES.has(err.code))
|
|
112
|
+
return true;
|
|
113
|
+
const shaped = err;
|
|
114
|
+
if (shaped.$retryable)
|
|
115
|
+
return true;
|
|
116
|
+
if (TRANSIENT_ERROR_NAMES.has(err.name))
|
|
117
|
+
return true;
|
|
118
|
+
const status = shaped.$metadata?.httpStatusCode;
|
|
119
|
+
return status !== undefined && (status === 429 || status >= 500);
|
|
120
|
+
}
|
|
121
|
+
/** Run `fn` over `items` with at most `limit` in flight. `fn` must not reject. */
|
|
122
|
+
async function forEachBounded(items, limit, fn) {
|
|
123
|
+
let next = 0;
|
|
124
|
+
const worker = async () => {
|
|
125
|
+
while (next < items.length) {
|
|
126
|
+
const item = items[next++];
|
|
127
|
+
await fn(item);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Make every referenced transform key exist in `store`, and check every static key does. A key
|
|
134
|
+
* already stored is left alone:
|
|
135
|
+
* keys are content-addressed, so an existing object is the right one. sharp is loaded only when
|
|
136
|
+
* something is missing, and a sharp that cannot load throws `SharpUnavailableError` rather than
|
|
137
|
+
* failing each key.
|
|
138
|
+
*/
|
|
139
|
+
export async function materializeAssets(options) {
|
|
140
|
+
const { store } = options;
|
|
141
|
+
const concurrency = Math.max(1, options.concurrency ?? DEFAULT_CONCURRENCY);
|
|
142
|
+
const transformConcurrency = Math.max(1, options.transformConcurrency ?? DEFAULT_TRANSFORM_CONCURRENCY);
|
|
143
|
+
const listThreshold = options.listThreshold ?? DEFAULT_LIST_THRESHOLD;
|
|
144
|
+
const attempts = Math.max(1, options.attempts ?? DEFAULT_ATTEMPTS);
|
|
145
|
+
const baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
146
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
147
|
+
const withRetry = async (operation) => {
|
|
148
|
+
for (let attempt = 1;; attempt++) {
|
|
149
|
+
try {
|
|
150
|
+
return await operation();
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
if (attempt >= attempts || !isTransientStoreError(err))
|
|
154
|
+
throw err;
|
|
155
|
+
await sleep(baseDelayMs * 2 ** (attempt - 1) * (1 + Math.random()));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
const references = new Map();
|
|
160
|
+
const staticKeys = new Set((options.statics ?? []).map((target) => target.key));
|
|
161
|
+
for (const target of [...options.targets, ...(options.statics ?? [])]) {
|
|
162
|
+
const entry = references.get(target.key) ?? { routes: new Set(), files: new Set() };
|
|
163
|
+
target.routes.forEach((route) => entry.routes.add(route));
|
|
164
|
+
target.files.forEach((file) => entry.files.add(file));
|
|
165
|
+
references.set(target.key, entry);
|
|
166
|
+
}
|
|
167
|
+
const outcomes = new Map();
|
|
168
|
+
const fail = (key, failure, error) => outcomes.set(key, { status: 'failed', failure, error });
|
|
169
|
+
// The refs file is an editable file on disk, so every key is re-validated here.
|
|
170
|
+
const transformPrefix = `${ASSET_PREFIXES.transform}/`;
|
|
171
|
+
const valid = [];
|
|
172
|
+
const validStatics = [];
|
|
173
|
+
for (const key of references.keys()) {
|
|
174
|
+
if (staticKeys.has(key)) {
|
|
175
|
+
if (STATIC_KEY_RE.test(key))
|
|
176
|
+
validStatics.push(key);
|
|
177
|
+
else
|
|
178
|
+
fail(key, 'content', 'Not a static asset key (expected assets/{hash32}/{slug}.{ext})');
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (!key.startsWith(transformPrefix)) {
|
|
182
|
+
fail(key, 'content', `Not a transform key (expected a ${transformPrefix} prefix)`);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const segments = key.slice(transformPrefix.length).split('/');
|
|
186
|
+
const canonical = canonicalizeTransformPath(segments, 'any');
|
|
187
|
+
if (!canonical.ok) {
|
|
188
|
+
fail(key, 'content', `Invalid transform key: ${canonical.error}`);
|
|
189
|
+
}
|
|
190
|
+
else if (!canonical.isCanonical) {
|
|
191
|
+
fail(key, 'content', `Not canonical; the stored key would be ${transformPrefix}${canonical.canonicalPath}`);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
valid.push({ key, directives: segments[0], parsed: canonical });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const byDirectives = new Map();
|
|
198
|
+
for (const target of valid) {
|
|
199
|
+
const group = byDirectives.get(target.directives) ?? [];
|
|
200
|
+
group.push(target);
|
|
201
|
+
byDirectives.set(target.directives, group);
|
|
202
|
+
}
|
|
203
|
+
const toHead = [];
|
|
204
|
+
for (const [directives, group] of byDirectives) {
|
|
205
|
+
if (store.listPublicObjectKeys && group.length >= listThreshold) {
|
|
206
|
+
const listPrefix = `${transformPrefix}${directives}/`;
|
|
207
|
+
const listKeys = store.listPublicObjectKeys.bind(store);
|
|
208
|
+
try {
|
|
209
|
+
const found = await withRetry(async () => {
|
|
210
|
+
const keys = [];
|
|
211
|
+
for await (const key of listKeys(listPrefix))
|
|
212
|
+
keys.push(key);
|
|
213
|
+
return keys;
|
|
214
|
+
});
|
|
215
|
+
const listed = new Set(found);
|
|
216
|
+
for (const target of group) {
|
|
217
|
+
if (listed.has(target.key))
|
|
218
|
+
outcomes.set(target.key, { status: 'existed' });
|
|
219
|
+
}
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// A listing that keeps failing costs only speed: HEAD the group instead, and let each
|
|
224
|
+
// HEAD report its own failure.
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
toHead.push(...group);
|
|
228
|
+
}
|
|
229
|
+
await forEachBounded(toHead, concurrency, async (target) => {
|
|
230
|
+
try {
|
|
231
|
+
if (await withRetry(() => store.hasPublicObject(target.key))) {
|
|
232
|
+
outcomes.set(target.key, { status: 'existed' });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
fail(target.key, 'store', `Existence check failed: ${getErrorMessage(err)}`);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
await forEachBounded(validStatics, concurrency, async (key) => {
|
|
240
|
+
try {
|
|
241
|
+
// A HEAD cannot tell a missing key from a missing bucket, and a static key has no
|
|
242
|
+
// transform's meta read to tell them apart afterwards; a GET names which.
|
|
243
|
+
const exists = (await withRetry(() => store.hasPublicObject(key))) ||
|
|
244
|
+
(await withRetry(() => store.readPublicObject(key))) !== null;
|
|
245
|
+
if (exists) {
|
|
246
|
+
outcomes.set(key, { status: 'existed' });
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
fail(key, 'content', 'No stored object; an svg or pdf is written at upload only');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
fail(key, 'store', `Existence check failed: ${getErrorMessage(err)}`);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
const missing = valid.filter((target) => !outcomes.has(target.key));
|
|
257
|
+
if (missing.length > 0) {
|
|
258
|
+
try {
|
|
259
|
+
await loadSharp();
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
throw new SharpUnavailableError(err);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
await forEachBounded(missing, transformConcurrency, async (target) => {
|
|
266
|
+
try {
|
|
267
|
+
const result = await withRetry(() => storeTransform(store, target.parsed, target.key));
|
|
268
|
+
if (result.ok) {
|
|
269
|
+
outcomes.set(target.key, { status: 'created' });
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
fail(target.key, 'content', `${result.status}: ${result.error}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
fail(target.key, 'store', getErrorMessage(err));
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
const results = [...references.keys()].sort().map((key) => {
|
|
280
|
+
const refs = references.get(key);
|
|
281
|
+
const outcome = outcomes.get(key) ?? {
|
|
282
|
+
status: 'failed',
|
|
283
|
+
failure: 'store',
|
|
284
|
+
error: 'No outcome recorded',
|
|
285
|
+
};
|
|
286
|
+
return {
|
|
287
|
+
key,
|
|
288
|
+
routes: [...(refs?.routes ?? [])].sort(),
|
|
289
|
+
files: [...(refs?.files ?? [])].sort(),
|
|
290
|
+
...outcome,
|
|
291
|
+
};
|
|
292
|
+
});
|
|
293
|
+
const count = (predicate) => results.filter(predicate).length;
|
|
294
|
+
return {
|
|
295
|
+
summary: {
|
|
296
|
+
total: results.length,
|
|
297
|
+
existed: count((r) => r.status === 'existed'),
|
|
298
|
+
created: count((r) => r.status === 'created'),
|
|
299
|
+
failed: count((r) => r.status === 'failed'),
|
|
300
|
+
contentFailures: count((r) => r.status === 'failed' && r.failure === 'content'),
|
|
301
|
+
storeFailures: count((r) => r.status === 'failed' && r.failure === 'store'),
|
|
302
|
+
},
|
|
303
|
+
results,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
@@ -48,6 +48,7 @@ export declare class LocalAssetStore implements AssetStore {
|
|
|
48
48
|
data: Uint8Array;
|
|
49
49
|
contentType: string;
|
|
50
50
|
}): Promise<void>;
|
|
51
|
+
/** Ignores the `ext` hint: a directory read costs nothing here and needs no permission. */
|
|
51
52
|
readOriginal(hash32: string): Promise<{
|
|
52
53
|
data: Uint8Array;
|
|
53
54
|
ext: string;
|
|
@@ -59,8 +60,11 @@ export declare class LocalAssetStore implements AssetStore {
|
|
|
59
60
|
contentType: string;
|
|
60
61
|
contentDisposition?: string;
|
|
61
62
|
cacheControl?: string;
|
|
63
|
+
/** Ignored: tags exist for bucket lifecycle rules, and a local store has none. */
|
|
64
|
+
tags?: Readonly<Record<string, string>>;
|
|
62
65
|
}): Promise<void>;
|
|
63
66
|
readPublicObject(key: string): Promise<PublicObject | null>;
|
|
67
|
+
hasPublicObject(key: string): Promise<boolean>;
|
|
64
68
|
/**
|
|
65
69
|
* Atomic-exclusive create: opens with the `wx` flag so two concurrent
|
|
66
70
|
* writers racing on the same hash32 have exactly one winner ('created')
|
|
@@ -113,6 +113,7 @@ export class LocalAssetStore {
|
|
|
113
113
|
await this.writeFile(filePath, input.data);
|
|
114
114
|
await this.writeHeadersSidecar(filePath, { contentType: input.contentType });
|
|
115
115
|
}
|
|
116
|
+
/** Ignores the `ext` hint: a directory read costs nothing here and needs no permission. */
|
|
116
117
|
async readOriginal(hash32) {
|
|
117
118
|
const dir = this.resolveKey(ASSET_PREFIXES.originals);
|
|
118
119
|
let entries;
|
|
@@ -157,6 +158,16 @@ export class LocalAssetStore {
|
|
|
157
158
|
cacheControl: headers.cacheControl,
|
|
158
159
|
};
|
|
159
160
|
}
|
|
161
|
+
async hasPublicObject(key) {
|
|
162
|
+
try {
|
|
163
|
+
return (await fs.stat(this.resolveKey(key))).isFile();
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
if (isNotFoundError(err))
|
|
167
|
+
return false;
|
|
168
|
+
throw err;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
160
171
|
/**
|
|
161
172
|
* Atomic-exclusive create: opens with the `wx` flag so two concurrent
|
|
162
173
|
* writers racing on the same hash32 have exactly one winner ('created')
|
|
@@ -60,7 +60,7 @@ export declare class S3AssetStore implements AssetStore {
|
|
|
60
60
|
data: Uint8Array;
|
|
61
61
|
contentType: string;
|
|
62
62
|
}): Promise<void>;
|
|
63
|
-
readOriginal(hash32: string): Promise<{
|
|
63
|
+
readOriginal(hash32: string, ext?: string): Promise<{
|
|
64
64
|
data: Uint8Array;
|
|
65
65
|
ext: string;
|
|
66
66
|
contentType?: string;
|
|
@@ -71,8 +71,17 @@ export declare class S3AssetStore implements AssetStore {
|
|
|
71
71
|
contentType: string;
|
|
72
72
|
contentDisposition?: string;
|
|
73
73
|
cacheControl?: string;
|
|
74
|
+
tags?: Readonly<Record<string, string>>;
|
|
74
75
|
}): Promise<void>;
|
|
75
76
|
readPublicObject(key: string): Promise<PublicObject | null>;
|
|
77
|
+
/**
|
|
78
|
+
* A HEAD. S3 answers a missing key with 403, not 404, unless the caller may `s3:ListBucket`,
|
|
79
|
+
* so a role without it sees an error here rather than `false`.
|
|
80
|
+
*/
|
|
81
|
+
hasPublicObject(key: string): Promise<boolean>;
|
|
82
|
+
listPublicObjectKeys(prefix: string): AsyncIterable<string>;
|
|
83
|
+
/** HEADs first: signing is local and succeeds for a missing key too. */
|
|
84
|
+
presignPublicObjectRead(key: string): Promise<string | null>;
|
|
76
85
|
putMetaIfAbsent(hash32: string, meta: AssetMeta): Promise<'created' | 'already-exists'>;
|
|
77
86
|
getMeta(hash32: string): Promise<AssetMeta | null>;
|
|
78
87
|
listMeta(input?: {
|
package/dist/assets/store-s3.js
CHANGED
|
@@ -5,12 +5,15 @@
|
|
|
5
5
|
* and writes objects under the five asset prefixes.
|
|
6
6
|
*/
|
|
7
7
|
import { randomUUID } from 'node:crypto';
|
|
8
|
-
import { DeleteObjectCommand, GetObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client, } from '@aws-sdk/client-s3';
|
|
8
|
+
import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client, paginateListObjectsV2, } from '@aws-sdk/client-s3';
|
|
9
9
|
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
|
|
10
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
10
11
|
import { isHttpUrlOrSameOriginPath } from '../utils/sanitize-href.js';
|
|
11
12
|
import { ASSET_PREFIXES, createKeyBuilders } from './keys.js';
|
|
12
13
|
const DEFAULT_MAX_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
13
14
|
const PRESIGN_EXPIRY_SECONDS = 15 * 60;
|
|
15
|
+
/** A presigned read is followed by the browser at once; it is never stored or cached. */
|
|
16
|
+
const PRESIGNED_READ_EXPIRY_SECONDS = 5 * 60;
|
|
14
17
|
function matchesAwsError(err, name, httpStatusCode) {
|
|
15
18
|
if (!(err instanceof Error))
|
|
16
19
|
return false;
|
|
@@ -18,7 +21,12 @@ function matchesAwsError(err, name, httpStatusCode) {
|
|
|
18
21
|
return shaped.name === name || shaped.$metadata?.httpStatusCode === httpStatusCode;
|
|
19
22
|
}
|
|
20
23
|
const isPreconditionFailed = (err) => matchesAwsError(err, 'PreconditionFailed', 412);
|
|
21
|
-
|
|
24
|
+
/**
|
|
25
|
+
* A missing key, never a missing bucket: S3 answers both with 404, and only a GET's error code names
|
|
26
|
+
* which. A HEAD's 404 has no body, so `hasPublicObject` reads either as absent; a caller that must
|
|
27
|
+
* tell them apart follows with a GET (assets/materialize.ts does).
|
|
28
|
+
*/
|
|
29
|
+
const isNoSuchKey = (err) => matchesAwsError(err, 'NoSuchKey', 404) && err.name !== 'NoSuchBucket';
|
|
22
30
|
export class S3AssetStore {
|
|
23
31
|
constructor(options) {
|
|
24
32
|
this.capabilities = { directUpload: true };
|
|
@@ -97,7 +105,13 @@ export class S3AssetStore {
|
|
|
97
105
|
ContentType: input.contentType,
|
|
98
106
|
}));
|
|
99
107
|
}
|
|
100
|
-
async readOriginal(hash32) {
|
|
108
|
+
async readOriginal(hash32, ext) {
|
|
109
|
+
if (ext !== undefined) {
|
|
110
|
+
const direct = await this.getObject(this.keys.originalKey(hash32, ext));
|
|
111
|
+
const data = await direct?.Body?.transformToByteArray();
|
|
112
|
+
if (data)
|
|
113
|
+
return { data, ext, contentType: direct?.ContentType };
|
|
114
|
+
}
|
|
101
115
|
const prefix = this.keys.originalPrefix(hash32);
|
|
102
116
|
const listed = await this.client.send(new ListObjectsV2Command({ Bucket: this.bucket, Prefix: prefix, MaxKeys: 1 }));
|
|
103
117
|
const foundKey = listed.Contents?.[0]?.Key;
|
|
@@ -112,6 +126,11 @@ export class S3AssetStore {
|
|
|
112
126
|
return { data, ext: foundKey.slice(prefix.length), contentType: result.ContentType };
|
|
113
127
|
}
|
|
114
128
|
async putPublicObject(input) {
|
|
129
|
+
// `encodeURIComponent` writes a space as `%20`, which reads the same to any decoder;
|
|
130
|
+
// URLSearchParams writes `+`.
|
|
131
|
+
const tagging = Object.entries(input.tags ?? {})
|
|
132
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
|
133
|
+
.join('&');
|
|
115
134
|
await this.client.send(new PutObjectCommand({
|
|
116
135
|
Bucket: this.bucket,
|
|
117
136
|
Key: input.key,
|
|
@@ -119,6 +138,7 @@ export class S3AssetStore {
|
|
|
119
138
|
ContentType: input.contentType,
|
|
120
139
|
ContentDisposition: input.contentDisposition,
|
|
121
140
|
CacheControl: input.cacheControl,
|
|
141
|
+
Tagging: tagging || undefined,
|
|
122
142
|
}));
|
|
123
143
|
}
|
|
124
144
|
async readPublicObject(key) {
|
|
@@ -135,6 +155,38 @@ export class S3AssetStore {
|
|
|
135
155
|
cacheControl: result.CacheControl,
|
|
136
156
|
};
|
|
137
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* A HEAD. S3 answers a missing key with 403, not 404, unless the caller may `s3:ListBucket`,
|
|
160
|
+
* so a role without it sees an error here rather than `false`.
|
|
161
|
+
*/
|
|
162
|
+
async hasPublicObject(key) {
|
|
163
|
+
try {
|
|
164
|
+
await this.client.send(new HeadObjectCommand({ Bucket: this.bucket, Key: key }));
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
if (isNoSuchKey(err))
|
|
169
|
+
return false;
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async *listPublicObjectKeys(prefix) {
|
|
174
|
+
const pages = paginateListObjectsV2({ client: this.client }, { Bucket: this.bucket, Prefix: prefix });
|
|
175
|
+
for await (const page of pages) {
|
|
176
|
+
for (const object of page.Contents ?? []) {
|
|
177
|
+
if (object.Key)
|
|
178
|
+
yield object.Key;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** HEADs first: signing is local and succeeds for a missing key too. */
|
|
183
|
+
async presignPublicObjectRead(key) {
|
|
184
|
+
if (!(await this.hasPublicObject(key)))
|
|
185
|
+
return null;
|
|
186
|
+
return getSignedUrl(this.client, new GetObjectCommand({ Bucket: this.bucket, Key: key }), {
|
|
187
|
+
expiresIn: PRESIGNED_READ_EXPIRY_SECONDS,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
138
190
|
async putMetaIfAbsent(hash32, meta) {
|
|
139
191
|
try {
|
|
140
192
|
await this.client.send(new PutObjectCommand({
|
|
@@ -4,17 +4,16 @@
|
|
|
4
4
|
* NO imports (not even other files in this directory) so it can be imported
|
|
5
5
|
* from client bundles (via `assetUrl`/`assetSrcSet` in asset-url.ts, exported
|
|
6
6
|
* off the package's main entry) as well as from the server-only transform
|
|
7
|
-
* engine (transform.ts) and the
|
|
7
|
+
* engine (transform.ts) and the lazy transform Lambda, without
|
|
8
8
|
* ever pulling in node:crypto, sharp, or any other server-only dependency.
|
|
9
9
|
*
|
|
10
10
|
* `{directives}` is either the literal identity token (`orig`) or a
|
|
11
11
|
* comma-separated list of `key=value` pairs drawn from:
|
|
12
|
-
* w={int} output width
|
|
13
|
-
*
|
|
14
|
-
* time via `withoutEnlargement`, not here)
|
|
12
|
+
* w={int} output width, accepted per `TransformWidthPolicy` (upscaling
|
|
13
|
+
* is rejected at transform time via `withoutEnlargement`, not here)
|
|
15
14
|
* f={fmt} output format: webp | jpeg | png - when present, the URL's
|
|
16
15
|
* `{ext}` must equal it exactly
|
|
17
|
-
* q={int} quality
|
|
16
|
+
* q={int} quality, a multiple of 5 in [30, 95] under either policy
|
|
18
17
|
* c={rect} normalized crop rect `x:y:w:h`, four floats in [0,1] with
|
|
19
18
|
* x+w<=1 and y+h<=1, w>0 and h>0 (colon-separated - commas are
|
|
20
19
|
* the directive separator)
|
|
@@ -56,6 +55,18 @@ export type ParseTransformPathResult = ({
|
|
|
56
55
|
readonly ok: false;
|
|
57
56
|
readonly error: string;
|
|
58
57
|
};
|
|
58
|
+
/**
|
|
59
|
+
* Which widths a transform path accepts. The policy only narrows what parses; a width that parses
|
|
60
|
+
* under both has the same canonical spelling under both.
|
|
61
|
+
*
|
|
62
|
+
* - `allowlist`: multiples of 160 in [160, 4096], plus the small rungs 32, 48, 64, 96 and 128. For
|
|
63
|
+
* the opt-in anonymous on-demand path, where every accepted width is a new stored object an
|
|
64
|
+
* anonymous caller can mint.
|
|
65
|
+
* - `any`: any integer in [1, 8192]. For every path where the URLs come from site code or a
|
|
66
|
+
* signed-in editor: the authenticated route, `assetUrl`/`assetSrcSet`, the collector and the
|
|
67
|
+
* materializer.
|
|
68
|
+
*/
|
|
69
|
+
export type TransformWidthPolicy = 'allowlist' | 'any';
|
|
59
70
|
/**
|
|
60
71
|
* Decompression-bomb cap: the max `width * height` (post-decode pixel count)
|
|
61
72
|
* any raster the pipeline handles may have - checked both at upload finalize
|
|
@@ -63,10 +74,12 @@ export type ParseTransformPathResult = ({
|
|
|
63
74
|
* time (transform.ts, via sharp's `limitInputPixels`). Shared here (rather
|
|
64
75
|
* than duplicated in each server-only module) so it stays a single number,
|
|
65
76
|
* and because this file is dependency-free/isomorphic it can be imported by
|
|
66
|
-
* both without pulling sharp or file-type into anything.
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
77
|
+
* both without pulling sharp or file-type into anything. 24 MP (a 6000x4000
|
|
78
|
+
* camera frame) is sized by measured peak memory: the worst case, a full-size
|
|
79
|
+
* WebP encode of an RGBA WebP source, peaks at 1246 MiB here and grows about
|
|
80
|
+
* 37 MiB per MP, so this is the largest cap that fits the 2048 MB CMS and
|
|
81
|
+
* transform Lambdas with allocator headroom. For an animation the cap bounds
|
|
82
|
+
* all decoded frames together.
|
|
70
83
|
*/
|
|
71
84
|
export declare const MAX_INPUT_PIXELS: number;
|
|
72
85
|
/**
|
|
@@ -87,8 +100,8 @@ export declare const MAX_INPUT_PIXELS: number;
|
|
|
87
100
|
* ignore) rather than by two constants that happen to match today.
|
|
88
101
|
*/
|
|
89
102
|
export declare const MAX_ANIMATED_FRAMES = 60;
|
|
90
|
-
/** True if `
|
|
91
|
-
export declare function isAllowedTransformWidth(width: number): boolean;
|
|
103
|
+
/** True if `policy` accepts `width`; see `TransformWidthPolicy`. */
|
|
104
|
+
export declare function isAllowedTransformWidth(width: number, policy: TransformWidthPolicy): boolean;
|
|
92
105
|
/**
|
|
93
106
|
* True when x, y, w, h describe a valid normalized crop rect: all four values
|
|
94
107
|
* finite and in [0,1], w and h strictly positive, and the rect fits within
|
|
@@ -98,16 +111,42 @@ export declare function isAllowedTransformWidth(width: number): boolean;
|
|
|
98
111
|
* identical constraints.
|
|
99
112
|
*/
|
|
100
113
|
export declare function isValidCropRect(x: number, y: number, w: number, h: number): boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Round a crop rect to `CROP_PRECISION` decimals, shrinking `w`/`h` to the
|
|
116
|
+
* space left when rounding pushes `x+w` or `y+h` past 1 (0.66665 + 0.33335
|
|
117
|
+
* rounds to 0.6667 + 0.3334). The result can still be degenerate (a `w` or `h`
|
|
118
|
+
* that rounds to 0), which `isValidCropRect` rejects.
|
|
119
|
+
*/
|
|
120
|
+
export declare function roundCropRect(rect: CropRect): CropRect;
|
|
101
121
|
/**
|
|
102
122
|
* Parse the three path segments after the `assets/t/` prefix:
|
|
103
123
|
* `[directivesRaw, hash32, "{slug}.{ext}"]`. The raw route strips the
|
|
104
124
|
* `assets/t/` prefix and splits the remaining key by `/` before calling this.
|
|
105
125
|
*/
|
|
106
|
-
export declare function parseTransformPath(segments: readonly string[]): ParseTransformPathResult;
|
|
126
|
+
export declare function parseTransformPath(segments: readonly string[], policy: TransformWidthPolicy): ParseTransformPathResult;
|
|
107
127
|
/**
|
|
108
128
|
* Canonical string form of a directive set, so equivalent directive sets
|
|
109
129
|
* (different key order, different float formatting) always map to the same
|
|
110
|
-
* cache key. Order is fixed alphabetically by key: c, f, q, w
|
|
130
|
+
* cache key. Order is fixed alphabetically by key: c, f, q, w, and crop is
|
|
131
|
+
* rounded by `roundCropRect`.
|
|
111
132
|
*/
|
|
112
133
|
export declare function formatDirectives(directives: TransformDirectives): string;
|
|
134
|
+
export type CanonicalTransformPathResult = ({
|
|
135
|
+
readonly ok: true;
|
|
136
|
+
/** `{directives}/{hash32}/{slug}.{ext}` in canonical form, without the `assets/t/` prefix. */
|
|
137
|
+
readonly canonicalPath: string;
|
|
138
|
+
/** True when the requested segments already were `canonicalPath`, byte for byte. */
|
|
139
|
+
readonly isCanonical: boolean;
|
|
140
|
+
} & ParsedTransformPath) | {
|
|
141
|
+
readonly ok: false;
|
|
142
|
+
readonly error: string;
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Parse transform-path segments and resolve them to the one spelling their
|
|
146
|
+
* output is stored under. The returned `directives` are parsed back from that
|
|
147
|
+
* canonical spelling, never taken from the request, so a transform computed
|
|
148
|
+
* from them always matches its key. A crop that rounds to zero extent has no
|
|
149
|
+
* canonical spelling and is rejected.
|
|
150
|
+
*/
|
|
151
|
+
export declare function canonicalizeTransformPath(segments: readonly string[], policy: TransformWidthPolicy): CanonicalTransformPathResult;
|
|
113
152
|
export {};
|