@ampless/runtime 1.0.0-alpha.27 → 1.0.0-alpha.29

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/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Post, MediaMetadata, Config, ThemeModule, ThemeManifest } from 'ampless';
1
+ import { Post, MediaMetadata, Config, ThemeModule, ThemeManifest, PluginPackageManifest } from 'ampless';
2
2
  export { Config, Post, ThemeManifest } from 'ampless';
3
3
  import { Metadata } from 'next';
4
4
  import { ReactNode } from 'react';
@@ -269,6 +269,45 @@ interface ThemeConfigApi {
269
269
  */
270
270
  declare function renderThemeCss(cssVars: Record<string, string>): string;
271
271
 
272
+ /**
273
+ * Resolve `<packageName>/package.json`, read it, and return the
274
+ * `amplessPlugin` field as a typed `PluginPackageManifest`. Returns
275
+ * `null` for any failure — the caller skips cross-check rather than
276
+ * aborting plugin loading.
277
+ *
278
+ * Failure modes that resolve to `null`:
279
+ * - Package not installed at this resolution root
280
+ * (`ERR_MODULE_NOT_FOUND`)
281
+ * - `package.json` not in the package's `exports`
282
+ * (`ERR_PACKAGE_PATH_NOT_EXPORTED`) — see the spec under
283
+ * `docs/tmp/plugin-extension-phase5.md` §B
284
+ * - `readFileSync` throws (e.g. ENOENT, permissions)
285
+ * - JSON parse error
286
+ * - `amplessPlugin` field absent or not an object
287
+ * - `amplessPlugin` field present but structurally invalid (e.g.
288
+ * `apiVersion` not a number, `capabilities` not an array of
289
+ * strings, `trustLevel` not one of the three allowed values) —
290
+ * see `isValidManifest`
291
+ *
292
+ * The structural check matters: without it, a downstream consumer
293
+ * like `crossCheckStaticManifest`'s `for ... of` over the manifest's
294
+ * `capabilities` would crash on `capabilities: {}` or `capabilities: 42`,
295
+ * which is at odds with the "non-apiVersion mismatches warn rather
296
+ * than throw" policy.
297
+ *
298
+ * After this function returns a non-null value, the caller can trust
299
+ * every field it inspects has the declared type; mismatches against
300
+ * the factory return value are still surfaced as warnings (or, for
301
+ * `apiVersion`, throws) by the caller itself.
302
+ */
303
+ declare function loadPackageManifest(packageName: string): PluginPackageManifest | null;
304
+ /**
305
+ * Maximum `apiVersion` value this runtime can host. Plugins declaring
306
+ * a higher value are rejected at constructor time — the runtime can't
307
+ * safely call into surfaces it doesn't know about yet.
308
+ */
309
+ declare const SUPPORTED_API_VERSION: 1;
310
+
272
311
  declare function renderBody(post: Post): string;
273
312
  /**
274
313
  * Convert a tiptap doc to its HTML form. Same renderer the public
@@ -450,4 +489,4 @@ interface Ampless {
450
489
  */
451
490
  declare function createAmpless(opts: CreateAmplessOpts): Ampless;
452
491
 
453
- export { type Ampless, type AmplessOutputs, COLOR_SCHEME_SETTING_KEY, type ColorScheme, type CreateAmplessOpts, DEFAULT_COLOR_SCHEME, type DataOutput, type EffectiveSiteSettings, type EffectiveThemeConfig, type ListPostsByTagOptions, type ListPostsOptions, type ListPostsResult, type MediaApi, type PluginHeadApi, type PluginSettingsApi, type PluginSettingsSnapshot, type PostsApi, type PublicMediaShape, type PublicPostConnectionShape, type PublicPostShape, type ResolvedAssetMeta, type ResolvedMedia, type ResolvedTheme, type SeoApi, type SiteSettingsApi, type StorageOutput, type StreamS3Options, type ThemeActiveApi, type ThemeConfigApi, type ThemesRegistry, _resetStreamS3Cache, createAmpless, createPluginHead, createPluginSettings, escapeJsonLdInlineBody, htmlToMarkdown, markdownToHtml, renderBody, renderThemeCss, streamS3Object, streamS3ObjectWithRunner, tiptapToHtml, tiptapToMarkdown, validateColorScheme };
492
+ export { type Ampless, type AmplessOutputs, COLOR_SCHEME_SETTING_KEY, type ColorScheme, type CreateAmplessOpts, DEFAULT_COLOR_SCHEME, type DataOutput, type EffectiveSiteSettings, type EffectiveThemeConfig, type ListPostsByTagOptions, type ListPostsOptions, type ListPostsResult, type MediaApi, type PluginHeadApi, type PluginSettingsApi, type PluginSettingsSnapshot, type PostsApi, type PublicMediaShape, type PublicPostConnectionShape, type PublicPostShape, type ResolvedAssetMeta, type ResolvedMedia, type ResolvedTheme, SUPPORTED_API_VERSION, type SeoApi, type SiteSettingsApi, type StorageOutput, type StreamS3Options, type ThemeActiveApi, type ThemeConfigApi, type ThemesRegistry, _resetStreamS3Cache, createAmpless, createPluginHead, createPluginSettings, escapeJsonLdInlineBody, htmlToMarkdown, loadPackageManifest, markdownToHtml, renderBody, renderThemeCss, streamS3Object, streamS3ObjectWithRunner, tiptapToHtml, tiptapToMarkdown, validateColorScheme };
package/dist/index.js CHANGED
@@ -234,6 +234,53 @@ import {
234
234
  isValidPluginKey,
235
235
  resolvePluginSettings
236
236
  } from "ampless";
237
+
238
+ // src/plugin-package-manifest.ts
239
+ import { readFileSync } from "fs";
240
+ import { fileURLToPath } from "url";
241
+ var TRUST_LEVELS = ["untrusted", "trusted", "privileged"];
242
+ function isValidManifest(value) {
243
+ if (typeof value !== "object" || value === null) return false;
244
+ const v = value;
245
+ if (typeof v.apiVersion !== "number") return false;
246
+ if (typeof v.name !== "string") return false;
247
+ if (typeof v.trustLevel !== "string") return false;
248
+ if (!TRUST_LEVELS.includes(v.trustLevel)) return false;
249
+ if (!Array.isArray(v.capabilities)) return false;
250
+ for (const c of v.capabilities) {
251
+ if (typeof c !== "string") return false;
252
+ }
253
+ return true;
254
+ }
255
+ function loadPackageManifest(packageName) {
256
+ let resolvedUrl;
257
+ try {
258
+ resolvedUrl = import.meta.resolve(`${packageName}/package.json`);
259
+ } catch {
260
+ return null;
261
+ }
262
+ let raw;
263
+ try {
264
+ raw = readFileSync(fileURLToPath(resolvedUrl), "utf8");
265
+ } catch {
266
+ return null;
267
+ }
268
+ let pkg;
269
+ try {
270
+ pkg = JSON.parse(raw);
271
+ } catch {
272
+ return null;
273
+ }
274
+ if (typeof pkg !== "object" || pkg === null || !("amplessPlugin" in pkg)) {
275
+ return null;
276
+ }
277
+ const manifest = pkg.amplessPlugin;
278
+ if (!isValidManifest(manifest)) return null;
279
+ return manifest;
280
+ }
281
+ var SUPPORTED_API_VERSION = 1;
282
+
283
+ // src/plugin-head.ts
237
284
  function isPlugin2(p) {
238
285
  return typeof p === "object" && p !== null && "apiVersion" in p;
239
286
  }
@@ -277,6 +324,50 @@ function warn(message) {
277
324
  if (!isDev()) return;
278
325
  console.warn(`[ampless plugin-head] ${message}`);
279
326
  }
327
+ function crossCheckStaticManifest(plugin, label) {
328
+ const packageName = plugin.packageName;
329
+ const manifest = loadPackageManifest(packageName);
330
+ if (!manifest) return;
331
+ if (typeof manifest.apiVersion !== "number") {
332
+ throw new Error(
333
+ `${label}: package.json#amplessPlugin.apiVersion is not a number (got ${JSON.stringify(manifest.apiVersion)}). Update the plugin's package.json or remove the amplessPlugin field.`
334
+ );
335
+ }
336
+ if (manifest.apiVersion > SUPPORTED_API_VERSION) {
337
+ throw new Error(
338
+ `${label}: package.json#amplessPlugin.apiVersion ${manifest.apiVersion} is newer than this runtime supports (max ${SUPPORTED_API_VERSION}). Upgrade @ampless/runtime, or pin an older version of the plugin.`
339
+ );
340
+ }
341
+ if (manifest.apiVersion !== plugin.apiVersion) {
342
+ throw new Error(
343
+ `${label}: apiVersion mismatch \u2014 package.json declares ${manifest.apiVersion}, factory declares ${plugin.apiVersion}. The two must agree.`
344
+ );
345
+ }
346
+ if (manifest.name !== plugin.name) {
347
+ warn(
348
+ `${label}: name mismatch \u2014 package.json#amplessPlugin.name is "${manifest.name}", factory returns name="${plugin.name}". Align them so admin UI / capability gates can identify the plugin consistently.`
349
+ );
350
+ }
351
+ if (manifest.trustLevel !== plugin.trust_level) {
352
+ warn(
353
+ `${label}: trustLevel mismatch \u2014 package.json declares "${manifest.trustLevel}", factory declares trust_level="${plugin.trust_level}". The processor IAM policies are wired off trust_level; drift here usually means the deployment lambda runs in the wrong context.`
354
+ );
355
+ }
356
+ const factoryCaps = Array.isArray(plugin.capabilities) ? plugin.capabilities : [];
357
+ const manifestCaps = manifest.capabilities;
358
+ if (!setsEqual(factoryCaps, manifestCaps)) {
359
+ warn(
360
+ `${label}: capabilities mismatch \u2014 package.json declares [${manifestCaps.join(", ")}], factory declares [${factoryCaps.join(", ")}]. The static manifest is what npm registry / admin UI surfaces show before the plugin loads, so it should match what the factory actually returns.`
361
+ );
362
+ }
363
+ }
364
+ function setsEqual(a, b) {
365
+ const sa = new Set(a);
366
+ const sb = new Set(b);
367
+ if (sa.size !== sb.size) return false;
368
+ for (const c of sb) if (!sa.has(c)) return false;
369
+ return true;
370
+ }
280
371
  function applyAttrs(target, attrs, ownerLabel) {
281
372
  if (!attrs) return;
282
373
  for (const [k, v] of Object.entries(attrs)) {
@@ -549,6 +640,9 @@ function createPluginHead(cmsConfig, pluginSettings) {
549
640
  }
550
641
  }
551
642
  validPlugins.push(plugin);
643
+ if (plugin.packageName) {
644
+ crossCheckStaticManifest(plugin, label);
645
+ }
552
646
  const caps = plugin.capabilities;
553
647
  if (caps) {
554
648
  if (caps.includes("publicHead") && !plugin.publicHead) {
@@ -1120,12 +1214,14 @@ function createAmpless(opts) {
1120
1214
  export {
1121
1215
  COLOR_SCHEME_SETTING_KEY,
1122
1216
  DEFAULT_COLOR_SCHEME,
1217
+ SUPPORTED_API_VERSION,
1123
1218
  _resetStreamS3Cache,
1124
1219
  createAmpless,
1125
1220
  createPluginHead,
1126
1221
  createPluginSettings,
1127
1222
  escapeJsonLdInlineBody,
1128
1223
  htmlToMarkdown,
1224
+ loadPackageManifest,
1129
1225
  markdownToHtml,
1130
1226
  renderBody,
1131
1227
  renderThemeCss,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ampless/runtime",
3
- "version": "1.0.0-alpha.27",
3
+ "version": "1.0.0-alpha.29",
4
4
  "description": "Public-side runtime for ampless: post fetching, theme dispatch, middleware, public route handlers",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,8 +49,8 @@
49
49
  "lucide-react": "^1.16.0",
50
50
  "marked": "^18.0.4",
51
51
  "tailwind-merge": "^3.6.0",
52
- "ampless": "1.0.0-alpha.21",
53
- "@ampless/plugin-og-image": "0.2.0-alpha.21"
52
+ "@ampless/plugin-og-image": "0.2.0-alpha.23",
53
+ "ampless": "1.0.0-alpha.23"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "next": "^15 || ^16",