@conduction/nextcloud-vue 1.0.0-beta.21 → 1.0.0-beta.22

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.
@@ -43033,7 +43033,7 @@ var isObject = function isObject(obj) {
43033
43033
  }; // Strict object type check
43034
43034
  // Only returns true for plain JavaScript objects
43035
43035
 
43036
- var isPlainObject$2 = function isPlainObject(obj) {
43036
+ var isPlainObject$3 = function isPlainObject(obj) {
43037
43037
  return Object.prototype.toString.call(obj) === '[object Object]';
43038
43038
  };
43039
43039
  var isDate = function isDate(value) {
@@ -43115,7 +43115,7 @@ var cloneDeep = function cloneDeep(obj) {
43115
43115
  }, []);
43116
43116
  }
43117
43117
 
43118
- if (isPlainObject$2(obj)) {
43118
+ if (isPlainObject$3(obj)) {
43119
43119
  return keys(obj).reduce(function (result, key) {
43120
43120
  return _objectSpread$8(_objectSpread$8({}, result), {}, _defineProperty$b({}, key, cloneDeep(obj[key], obj[key])));
43121
43121
  }, {});
@@ -43323,7 +43323,7 @@ var kebabCase = function kebabCase(str) {
43323
43323
 
43324
43324
  var toString = function toString(val) {
43325
43325
  var spaces = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 2;
43326
- return isUndefinedOrNull(val) ? '' : isArray(val) || isPlainObject$2(val) && val.toString === Object.prototype.toString ? JSON.stringify(val, null, spaces) : String(val);
43326
+ return isUndefinedOrNull(val) ? '' : isArray(val) || isPlainObject$3(val) && val.toString === Object.prototype.toString ? JSON.stringify(val, null, spaces) : String(val);
43327
43327
  }; // Remove leading white space from a string
43328
43328
 
43329
43329
  var ELEMENT_PROTO = Element$1.prototype;
@@ -43591,7 +43591,7 @@ var BVTransition = /*#__PURE__*/extend({
43591
43591
  props = _ref.props;
43592
43592
  var transProps = props.transProps;
43593
43593
 
43594
- if (!isPlainObject$2(transProps)) {
43594
+ if (!isPlainObject$3(transProps)) {
43595
43595
  transProps = props.noFade ? NO_FADE_PROPS : FADE_PROPS;
43596
43596
 
43597
43597
  if (props.appear) {
@@ -43652,7 +43652,7 @@ var encode = function encode(str) {
43652
43652
  // See: https://github.com/vuejs/vue-router/blob/dev/src/util/query.js
43653
43653
 
43654
43654
  var stringifyQueryObj = function stringifyQueryObj(obj) {
43655
- if (!isPlainObject$2(obj)) {
43655
+ if (!isPlainObject$3(obj)) {
43656
43656
  return '';
43657
43657
  }
43658
43658
 
@@ -43743,7 +43743,7 @@ var computeHref = function computeHref() {
43743
43743
  } // Fallback to `to.path' + `to.query` + `to.hash` prop (if `to` is an object)
43744
43744
 
43745
43745
 
43746
- if (isPlainObject$2(to) && (to.path || to.query || to.hash)) {
43746
+ if (isPlainObject$3(to) && (to.path || to.query || to.hash)) {
43747
43747
  var path = toString(to.path);
43748
43748
  var query = stringifyQueryObj(to.query);
43749
43749
  var hash = toString(to.hash);
@@ -55925,6 +55925,29 @@ function useContextMenu() {
55925
55925
  }
55926
55926
  }
55927
55927
 
55928
+ /**
55929
+ * Pattern matching the `manifest-resolve-sentinel` capability's
55930
+ * sentinel — `@resolve:<key>` where `<key>` is lowercase alphanumeric
55931
+ * with `_` / `-` separators. The full string IS the sentinel; partial
55932
+ * substitution like `prefix-@resolve:foo` is NOT supported and is left
55933
+ * as a plain string for downstream renderers.
55934
+ *
55935
+ * Build-time validation accepts this pattern as a valid `string` for
55936
+ * any `string`-typed field UNDER `pages[].config`, regardless of any
55937
+ * narrower per-field constraint. Other paths reject it explicitly.
55938
+ */
55939
+ const SENTINEL_PATTERN$1 = /^@resolve:[a-z][a-z0-9_-]*$/;
55940
+
55941
+ /**
55942
+ * Test whether a string is a manifest `@resolve:` sentinel.
55943
+ *
55944
+ * @param {*} value Candidate value.
55945
+ * @return {boolean} True when the value is a fully-matched sentinel.
55946
+ */
55947
+ function isSentinel(value) {
55948
+ return typeof value === 'string' && SENTINEL_PATTERN$1.test(value)
55949
+ }
55950
+
55928
55951
  /**
55929
55952
  * Validate a manifest object against the manifest JSON Schema.
55930
55953
  *
@@ -55942,6 +55965,13 @@ function useContextMenu() {
55942
55965
  * - `pages[].component` is required when `type` is "custom".
55943
55966
  * - Per-type `config` shape rules for the built-in types `logs`,
55944
55967
  * `settings`, `chat`, `files` (REQ from manifest-page-type-extensions).
55968
+ * - The `manifest-resolve-sentinel` sentinel `@resolve:<key>` is
55969
+ * permissively accepted under `pages[].config.*` and explicitly
55970
+ * REJECTED in `version`, `dependencies[]`, `menu[].route`,
55971
+ * `menu[].id`, `pages[].id`, `pages[].route`, `pages[].component`,
55972
+ * `pages[].headerComponent`, `pages[].actionsComponent`,
55973
+ * `pages[].slots.*` — those are router invariants or registry
55974
+ * keys.
55945
55975
  *
55946
55976
  * The richer schema constraints (`additionalProperties: false`, `format`
55947
55977
  * URI, etc.) are enforced by the BE / hydra CI validators that consume
@@ -55961,7 +55991,7 @@ function useContextMenu() {
55961
55991
  function validateManifest(manifest, options = {}) {
55962
55992
  const errors = [];
55963
55993
 
55964
- if (!isPlainObject$1(manifest)) {
55994
+ if (!isPlainObject$2(manifest)) {
55965
55995
  return { valid: false, errors: ['manifest must be an object'] }
55966
55996
  }
55967
55997
 
@@ -55969,6 +55999,10 @@ function validateManifest(manifest, options = {}) {
55969
55999
 
55970
56000
  if (typeof manifest.version !== 'string') {
55971
56001
  errors.push('/version must be a string');
56002
+ } else if (isSentinel(manifest.version)) {
56003
+ // `manifest-resolve-sentinel` REQ-MRS-004: sentinel is a router /
56004
+ // registry invariant violation when used here.
56005
+ errors.push(`/version "${manifest.version}" must not be a @resolve: sentinel (sentinels are only valid under pages[].config.*)`);
55972
56006
  } else if (!versionPattern.test(manifest.version)) {
55973
56007
  errors.push(`/version "${manifest.version}" must match semver pattern`);
55974
56008
  }
@@ -55977,12 +56011,19 @@ function validateManifest(manifest, options = {}) {
55977
56011
  errors.push('/menu must be an array');
55978
56012
  } else {
55979
56013
  manifest.menu.forEach((item, index) => {
55980
- if (!isPlainObject$1(item)) {
56014
+ if (!isPlainObject$2(item)) {
55981
56015
  errors.push(`/menu/${index} must be an object`);
55982
56016
  return
55983
56017
  }
55984
- if (typeof item.id !== 'string') errors.push(`/menu/${index}/id must be a string`);
56018
+ if (typeof item.id !== 'string') {
56019
+ errors.push(`/menu/${index}/id must be a string`);
56020
+ } else if (isSentinel(item.id)) {
56021
+ errors.push(`/menu/${index}/id must not be a @resolve: sentinel (sentinels are only valid under pages[].config.*)`);
56022
+ }
55985
56023
  if (typeof item.label !== 'string') errors.push(`/menu/${index}/label must be a string`);
56024
+ if (item.route !== undefined && isSentinel(item.route)) {
56025
+ errors.push(`/menu/${index}/route must not be a @resolve: sentinel (sentinels are only valid under pages[].config.*)`);
56026
+ }
55986
56027
  if (item.children !== undefined && !Array.isArray(item.children)) {
55987
56028
  errors.push(`/menu/${index}/children must be an array`);
55988
56029
  }
@@ -55998,18 +56039,24 @@ function validateManifest(manifest, options = {}) {
55998
56039
  } else {
55999
56040
  const seenIds = new Set();
56000
56041
  manifest.pages.forEach((page, index) => {
56001
- if (!isPlainObject$1(page)) {
56042
+ if (!isPlainObject$2(page)) {
56002
56043
  errors.push(`/pages/${index} must be an object`);
56003
56044
  return
56004
56045
  }
56005
56046
  if (typeof page.id !== 'string') {
56006
56047
  errors.push(`/pages/${index}/id must be a string`);
56048
+ } else if (isSentinel(page.id)) {
56049
+ errors.push(`/pages/${index}/id must not be a @resolve: sentinel (sentinels are only valid under pages[].config.*)`);
56007
56050
  } else if (seenIds.has(page.id)) {
56008
56051
  errors.push(`/pages/${index}/id "${page.id}" must be unique within pages[]`);
56009
56052
  } else {
56010
56053
  seenIds.add(page.id);
56011
56054
  }
56012
- if (typeof page.route !== 'string') errors.push(`/pages/${index}/route must be a string`);
56055
+ if (typeof page.route !== 'string') {
56056
+ errors.push(`/pages/${index}/route must be a string`);
56057
+ } else if (isSentinel(page.route)) {
56058
+ errors.push(`/pages/${index}/route must not be a @resolve: sentinel (sentinels are only valid under pages[].config.*)`);
56059
+ }
56013
56060
  if (typeof page.title !== 'string') errors.push(`/pages/${index}/title must be a string`);
56014
56061
  if (typeof page.type !== 'string' || page.type.length === 0) {
56015
56062
  errors.push(`/pages/${index}/type must be a non-empty string`);
@@ -56019,6 +56066,25 @@ function validateManifest(manifest, options = {}) {
56019
56066
  if (page.type === 'custom' && typeof page.component !== 'string') {
56020
56067
  errors.push(`/pages/${index}/component is required when type is "custom"`);
56021
56068
  }
56069
+ // `manifest-resolve-sentinel` REQ-MRS-004: registry-key
56070
+ // fields cannot be dynamic — they resolve at module-load
56071
+ // time against `customComponents`, before the loader runs.
56072
+ if (page.component !== undefined && isSentinel(page.component)) {
56073
+ errors.push(`/pages/${index}/component must not be a @resolve: sentinel (sentinels are only valid under pages[].config.*)`);
56074
+ }
56075
+ if (page.headerComponent !== undefined && isSentinel(page.headerComponent)) {
56076
+ errors.push(`/pages/${index}/headerComponent must not be a @resolve: sentinel (sentinels are only valid under pages[].config.*)`);
56077
+ }
56078
+ if (page.actionsComponent !== undefined && isSentinel(page.actionsComponent)) {
56079
+ errors.push(`/pages/${index}/actionsComponent must not be a @resolve: sentinel (sentinels are only valid under pages[].config.*)`);
56080
+ }
56081
+ if (isPlainObject$2(page.slots)) {
56082
+ for (const [slotName, slotValue] of Object.entries(page.slots)) {
56083
+ if (isSentinel(slotValue)) {
56084
+ errors.push(`/pages/${index}/slots/${slotName} must not be a @resolve: sentinel (sentinels are only valid under pages[].config.*)`);
56085
+ }
56086
+ }
56087
+ }
56022
56088
 
56023
56089
  // Per-type config-shape validation for built-in extended types.
56024
56090
  // (`manifest-page-type-extensions` spec — covers logs/settings/chat/files.)
@@ -56042,6 +56108,8 @@ function validateManifest(manifest, options = {}) {
56042
56108
  manifest.dependencies.forEach((dep, index) => {
56043
56109
  if (typeof dep !== 'string') {
56044
56110
  errors.push(`/dependencies/${index} must be a string`);
56111
+ } else if (isSentinel(dep)) {
56112
+ errors.push(`/dependencies/${index} must not be a @resolve: sentinel (sentinels are only valid under pages[].config.*)`);
56045
56113
  }
56046
56114
  });
56047
56115
  }
@@ -56050,7 +56118,7 @@ function validateManifest(manifest, options = {}) {
56050
56118
  return { valid: errors.length === 0, errors }
56051
56119
  }
56052
56120
 
56053
- function isPlainObject$1(value) {
56121
+ function isPlainObject$2(value) {
56054
56122
  return value !== null && typeof value === 'object' && !Array.isArray(value)
56055
56123
  }
56056
56124
 
@@ -56073,7 +56141,7 @@ function isPlainObject$1(value) {
56073
56141
  */
56074
56142
  function validateTypeConfig(page, index, errors) {
56075
56143
  if (!page || typeof page.type !== 'string') return
56076
- const cfg = isPlainObject$1(page.config) ? page.config : null;
56144
+ const cfg = isPlainObject$2(page.config) ? page.config : null;
56077
56145
  const pathBracket = `pages[${index}].config`;
56078
56146
  const pathSlash = `/pages/${index}/config`;
56079
56147
 
@@ -56127,7 +56195,7 @@ function validateTypeConfig(page, index, errors) {
56127
56195
  }
56128
56196
  const seenTabIds = Object.create(null);
56129
56197
  cfg.tabs.forEach((tab, tIndex) => {
56130
- if (!isPlainObject$1(tab)) {
56198
+ if (!isPlainObject$2(tab)) {
56131
56199
  errors.push(`${pathSlash}/tabs/${tIndex}: must be an object`);
56132
56200
  return
56133
56201
  }
@@ -56270,12 +56338,12 @@ function validateTypeConfig(page, index, errors) {
56270
56338
  */
56271
56339
  function validateSidebarConfig(page, pageIndex, errors) {
56272
56340
  const config = page.config;
56273
- if (!isPlainObject$1(config)) return
56341
+ if (!isPlainObject$2(config)) return
56274
56342
 
56275
56343
  // --- Index sidebar ---
56276
56344
  if (page.type === 'index' && config.sidebar !== undefined) {
56277
56345
  const path = `/pages/${pageIndex}/config/sidebar`;
56278
- if (!isPlainObject$1(config.sidebar)) {
56346
+ if (!isPlainObject$2(config.sidebar)) {
56279
56347
  errors.push(`${path} must be an object`);
56280
56348
  } else {
56281
56349
  if (config.sidebar.enabled !== undefined && typeof config.sidebar.enabled !== 'boolean') {
@@ -56288,13 +56356,13 @@ function validateSidebarConfig(page, pageIndex, errors) {
56288
56356
  if (config.sidebar.columnGroups !== undefined && !Array.isArray(config.sidebar.columnGroups)) {
56289
56357
  errors.push(`${path}/columnGroups must be an array`);
56290
56358
  }
56291
- if (config.sidebar.facets !== undefined && !isPlainObject$1(config.sidebar.facets)) {
56359
+ if (config.sidebar.facets !== undefined && !isPlainObject$2(config.sidebar.facets)) {
56292
56360
  errors.push(`${path}/facets must be an object`);
56293
56361
  }
56294
56362
  if (config.sidebar.showMetadata !== undefined && typeof config.sidebar.showMetadata !== 'boolean') {
56295
56363
  errors.push(`${path}/showMetadata must be a boolean`);
56296
56364
  }
56297
- if (config.sidebar.search !== undefined && !isPlainObject$1(config.sidebar.search)) {
56365
+ if (config.sidebar.search !== undefined && !isPlainObject$2(config.sidebar.search)) {
56298
56366
  errors.push(`${path}/search must be an object`);
56299
56367
  }
56300
56368
  }
@@ -56309,7 +56377,7 @@ function validateSidebarConfig(page, pageIndex, errors) {
56309
56377
  const path = `/pages/${pageIndex}/config/sidebar`;
56310
56378
  const sb = config.sidebar;
56311
56379
  const isBool = typeof sb === 'boolean';
56312
- const isObj = isPlainObject$1(sb);
56380
+ const isObj = isPlainObject$2(sb);
56313
56381
  if (!isBool && !isObj) {
56314
56382
  errors.push(`${path} must be a boolean (legacy) or object`);
56315
56383
  } else if (isObj) {
@@ -56349,7 +56417,7 @@ function validateSidebarConfig(page, pageIndex, errors) {
56349
56417
  }
56350
56418
 
56351
56419
  // --- Detail sidebar tabs (legacy sidebarProps.tabs path) ---
56352
- if (page.type === 'detail' && isPlainObject$1(config.sidebarProps) && config.sidebarProps.tabs !== undefined) {
56420
+ if (page.type === 'detail' && isPlainObject$2(config.sidebarProps) && config.sidebarProps.tabs !== undefined) {
56353
56421
  const tabsPath = `/pages/${pageIndex}/config/sidebarProps/tabs`;
56354
56422
  validateDetailTabsArray(config.sidebarProps.tabs, tabsPath, errors);
56355
56423
  }
@@ -56373,7 +56441,7 @@ function validateDetailTabsArray(tabs, tabsPath, errors) {
56373
56441
  const seenIds = new Set();
56374
56442
  tabs.forEach((tab, tabIndex) => {
56375
56443
  const tabPath = `${tabsPath}/${tabIndex}`;
56376
- if (!isPlainObject$1(tab)) {
56444
+ if (!isPlainObject$2(tab)) {
56377
56445
  errors.push(`${tabPath} must be an object`);
56378
56446
  return
56379
56447
  }
@@ -56424,7 +56492,7 @@ function validateDetailTabsArray(tabs, tabsPath, errors) {
56424
56492
  function validatePageSidebar(page, pageIndex, errors) {
56425
56493
  if (page.sidebar === undefined) return
56426
56494
  const path = `/pages/${pageIndex}/sidebar`;
56427
- if (!isPlainObject$1(page.sidebar)) {
56495
+ if (!isPlainObject$2(page.sidebar)) {
56428
56496
  errors.push(`${path} must be an object`);
56429
56497
  return
56430
56498
  }
@@ -56462,7 +56530,7 @@ function validateColumnsArray(cfg, pathSlash, pathBracket, errors) {
56462
56530
  // Legacy shorthand — accepted as-is.
56463
56531
  return
56464
56532
  }
56465
- if (!isPlainObject$1(col)) {
56533
+ if (!isPlainObject$2(col)) {
56466
56534
  errors.push(`${colPath}: must be a string (legacy shorthand) or object`);
56467
56535
  return
56468
56536
  }
@@ -56494,7 +56562,7 @@ function validateActionsArray(cfg, pathSlash, pathBracket, errors) {
56494
56562
  }
56495
56563
  cfg.actions.forEach((action, aIndex) => {
56496
56564
  const actionPath = `${pathSlash}/actions/${aIndex}`;
56497
- if (!isPlainObject$1(action)) {
56565
+ if (!isPlainObject$2(action)) {
56498
56566
  errors.push(`${actionPath}: must be an object`);
56499
56567
  return
56500
56568
  }
@@ -56551,7 +56619,7 @@ function validateWidgetsArray(cfg, pathSlash, pathBracket, errors) {
56551
56619
  }
56552
56620
  cfg.widgets.forEach((widget, wIndex) => {
56553
56621
  const widgetPath = `${pathSlash}/widgets/${wIndex}`;
56554
- if (!isPlainObject$1(widget)) {
56622
+ if (!isPlainObject$2(widget)) {
56555
56623
  errors.push(`${widgetPath}: must be an object`);
56556
56624
  return
56557
56625
  }
@@ -56586,7 +56654,7 @@ function validateLayoutArray(cfg, pathSlash, pathBracket, errors) {
56586
56654
  }
56587
56655
  cfg.layout.forEach((item, lIndex) => {
56588
56656
  const layoutPath = `${pathSlash}/layout/${lIndex}`;
56589
- if (!isPlainObject$1(item)) {
56657
+ if (!isPlainObject$2(item)) {
56590
56658
  errors.push(`${layoutPath}: must be an object`);
56591
56659
  return
56592
56660
  }
@@ -56627,7 +56695,7 @@ function validateLayoutArray(cfg, pathSlash, pathBracket, errors) {
56627
56695
  * @param {string[]} errors Accumulator
56628
56696
  */
56629
56697
  function validateSettingsSection(section, pathSlash, pathBracket, errors) {
56630
- if (!isPlainObject$1(section)) {
56698
+ if (!isPlainObject$2(section)) {
56631
56699
  errors.push(`${pathSlash}: must be an object`);
56632
56700
  return
56633
56701
  }
@@ -56659,7 +56727,7 @@ function validateSettingsSection(section, pathSlash, pathBracket, errors) {
56659
56727
  // Per-widget shape rules.
56660
56728
  if (hasWidgets) {
56661
56729
  section.widgets.forEach((widget, wIndex) => {
56662
- if (!isPlainObject$1(widget)) {
56730
+ if (!isPlainObject$2(widget)) {
56663
56731
  errors.push(`${pathSlash}/widgets/${wIndex}: must be an object`);
56664
56732
  return
56665
56733
  }
@@ -56702,7 +56770,7 @@ function validateFieldsArray(fields, fieldsPath, errors) {
56702
56770
  if (!Array.isArray(fields)) return
56703
56771
  fields.forEach((field, fIndex) => {
56704
56772
  const fieldPath = `${fieldsPath}/${fIndex}`;
56705
- if (!isPlainObject$1(field)) {
56773
+ if (!isPlainObject$2(field)) {
56706
56774
  errors.push(`${fieldPath}: must be an object`);
56707
56775
  return
56708
56776
  }
@@ -56721,17 +56789,292 @@ function validateFieldsArray(fields, fieldsPath, errors) {
56721
56789
  }
56722
56790
 
56723
56791
  /**
56724
- * Composable that loads and validates a Conduction app manifest.
56792
+ * Manifest `@resolve:` sentinel resolver.
56793
+ *
56794
+ * Implements the `manifest-resolve-sentinel` capability: walks the
56795
+ * `pages[].config` subtrees of an assembled manifest and replaces every
56796
+ * fully-matched `@resolve:<key>` string with the result of the consuming
56797
+ * app's `IAppConfig` lookup for `<key>`. Other manifest paths
56798
+ * (`route`, `id`, top-level fields, `menu[]`, `pages[].component` etc.)
56799
+ * are intentionally untouched — sentinels there are router / registry
56800
+ * invariants and the schema validator rejects them.
56801
+ *
56802
+ * Resolution source (canonical, per spec):
56725
56803
  *
56726
- * The composable implements the three-phase flow specified in
56727
- * REQ-JMR-002 of the json-manifest-renderer capability:
56804
+ * 1. `@nextcloud/initial-state` provisioned key
56805
+ * `app-{appId}-{key}` zero-network, preferred.
56806
+ * 2. Runtime `GET /index.php/apps/{appId}/api/configs/{key}` — falls
56807
+ * through silently on 4xx / network error.
56808
+ * 3. `null` — unresolved.
56809
+ *
56810
+ * Empty-state semantics: an unset / empty value substitutes `null` (not
56811
+ * empty string) and is accumulated into the returned `unresolved` array
56812
+ * so consumers can render an admin warning. A `console.warn` is emitted
56813
+ * once per unresolved key with the offending sentinel.
56814
+ *
56815
+ * The resolver is intentionally split into a synchronous walk + an
56816
+ * asynchronous batch resolution: every sentinel is collected first, then
56817
+ * each unique `(appId, key)` is resolved exactly once (initial-state
56818
+ * lookup is synchronous; runtime fetch is per-key cached for the page
56819
+ * lifetime), and finally the manifest is rewritten in a second walk
56820
+ * with the resolved values. This makes the substitution deterministic
56821
+ * — five `@resolve:foo` references in five pages share one fetch.
56822
+ *
56823
+ * @module utils/resolveManifestSentinels
56824
+ */
56825
+
56826
+ const SENTINEL_PATTERN = /^@resolve:([a-z][a-z0-9_-]*)$/;
56827
+
56828
+ /**
56829
+ * Process-wide cache of resolved IAppConfig values, keyed by
56830
+ * `${appId}::${key}`. Cleared via `clearResolveCache()` (test-only).
56831
+ *
56832
+ * @type {Map<string, Promise<*>>}
56833
+ */
56834
+ const resolveCache = new Map();
56835
+
56836
+ /**
56837
+ * Test-only helper to reset the per-page resolve cache between runs.
56838
+ * Production callers do not need this — the cache is page-lifetime by
56839
+ * design, consistent with the manifest's load-once model.
56840
+ *
56841
+ * @return {void}
56842
+ */
56843
+ function clearResolveCache() {
56844
+ resolveCache.clear();
56845
+ }
56846
+
56847
+ /**
56848
+ * Walk the manifest's `pages[].config` subtrees and replace every
56849
+ * `@resolve:<key>` sentinel with the resolved IAppConfig value.
56850
+ *
56851
+ * Returns a Promise resolving to `{ manifest, unresolved }`:
56852
+ * - `manifest` — a NEW manifest object with sentinels substituted; the
56853
+ * input is NOT mutated.
56854
+ * - `unresolved` — array of IAppConfig keys whose sentinels resolved
56855
+ * to `null` (unset / empty / fetch failure). Useful for surfacing
56856
+ * "n settings unconfigured" admin warnings.
56857
+ *
56858
+ * Sentinels OUTSIDE `pages[].config` are left intact; the schema
56859
+ * validator rejects them downstream so consumers see a clear error
56860
+ * rather than a silent substitution that breaks routing or registry
56861
+ * lookups.
56862
+ *
56863
+ * @param {object} manifest The merged (bundled + backend) manifest.
56864
+ * Walked but not mutated.
56865
+ * @param {string} appId Nextcloud app ID. Used to scope the
56866
+ * IAppConfig lookup namespace.
56867
+ * @param {object} [options] Resolver overrides.
56868
+ * @param {Function} [options.getAppConfigValue] Async (appId, key) =>
56869
+ * value resolver. Override for tests; defaults to the
56870
+ * initial-state-then-fetch chain documented above.
56871
+ * @param {Function} [options.warn] Override for `console.warn`. Used in
56872
+ * tests to capture warning calls without polluting test output.
56873
+ * @return {Promise<{ manifest: object, unresolved: string[] }>}
56874
+ */
56875
+ async function resolveManifestSentinels(manifest, appId, options = {}) {
56876
+ if (!isPlainObject$1(manifest)) {
56877
+ return { manifest, unresolved: [] }
56878
+ }
56879
+
56880
+ const getAppConfigValue = options.getAppConfigValue ?? defaultGetAppConfigValue;
56881
+ const warn = options.warn ?? ((...args) => {
56882
+ // eslint-disable-next-line no-console
56883
+ console.warn(...args);
56884
+ });
56885
+
56886
+ // Phase 1: scan for sentinels under pages[].config only. We only
56887
+ // need the unique key set — the second walk does the substitution.
56888
+ const keys = new Set();
56889
+ const pages = Array.isArray(manifest.pages) ? manifest.pages : [];
56890
+ for (const page of pages) {
56891
+ if (!isPlainObject$1(page) || !isPlainObject$1(page.config)) continue
56892
+ collectSentinelKeys(page.config, keys);
56893
+ }
56894
+
56895
+ if (keys.size === 0) {
56896
+ return { manifest, unresolved: [] }
56897
+ }
56898
+
56899
+ // Phase 2: resolve each unique (appId, key) exactly once.
56900
+ const resolved = new Map();
56901
+ const unresolved = [];
56902
+ await Promise.all(Array.from(keys).map(async (key) => {
56903
+ const value = await getAppConfigValue(appId, key);
56904
+ if (value === undefined || value === null || value === '') {
56905
+ resolved.set(key, null);
56906
+ unresolved.push(key);
56907
+ warn(`[resolveManifestSentinels] Manifest sentinel '@resolve:${key}' resolved to null (key unset)`);
56908
+ } else {
56909
+ resolved.set(key, value);
56910
+ }
56911
+ }));
56912
+
56913
+ // Phase 3: rebuild the manifest immutably, substituting sentinels in
56914
+ // pages[].config only. Other fields are passed through by reference.
56915
+ const out = { ...manifest };
56916
+ out.pages = pages.map((page) => {
56917
+ if (!isPlainObject$1(page) || !isPlainObject$1(page.config)) return page
56918
+ return { ...page, config: substituteInTree(page.config, resolved) }
56919
+ });
56920
+
56921
+ return { manifest: out, unresolved }
56922
+ }
56923
+
56924
+ /**
56925
+ * Recursively scan a tree, accumulating every sentinel key into the
56926
+ * provided `keys` Set. Plain objects + arrays are descended; primitive
56927
+ * leaves are checked against the sentinel pattern.
56928
+ *
56929
+ * @param {*} node Current tree node (object, array, or primitive).
56930
+ * @param {Set<string>} keys Accumulator for unique sentinel keys.
56931
+ * @return {void}
56932
+ */
56933
+ function collectSentinelKeys(node, keys) {
56934
+ if (typeof node === 'string') {
56935
+ const match = node.match(SENTINEL_PATTERN);
56936
+ if (match) keys.add(match[1]);
56937
+ return
56938
+ }
56939
+ if (Array.isArray(node)) {
56940
+ for (const item of node) collectSentinelKeys(item, keys);
56941
+ return
56942
+ }
56943
+ if (isPlainObject$1(node)) {
56944
+ for (const value of Object.values(node)) collectSentinelKeys(value, keys);
56945
+ }
56946
+ }
56947
+
56948
+ /**
56949
+ * Recursively rebuild a tree, replacing each fully-matched sentinel
56950
+ * with its resolved value. Returns a NEW tree; input is unchanged.
56951
+ *
56952
+ * @param {*} node Current tree node.
56953
+ * @param {Map<string,*>} resolved Map of key → resolved value (or null).
56954
+ * @return {*} New tree with sentinels substituted.
56955
+ */
56956
+ function substituteInTree(node, resolved) {
56957
+ if (typeof node === 'string') {
56958
+ const match = node.match(SENTINEL_PATTERN);
56959
+ if (match && resolved.has(match[1])) {
56960
+ return resolved.get(match[1])
56961
+ }
56962
+ return node
56963
+ }
56964
+ if (Array.isArray(node)) {
56965
+ return node.map((item) => substituteInTree(item, resolved))
56966
+ }
56967
+ if (isPlainObject$1(node)) {
56968
+ const out = {};
56969
+ for (const [key, value] of Object.entries(node)) {
56970
+ out[key] = substituteInTree(value, resolved);
56971
+ }
56972
+ return out
56973
+ }
56974
+ return node
56975
+ }
56976
+
56977
+ /**
56978
+ * Default `getAppConfigValue` implementation: consult
56979
+ * `@nextcloud/initial-state` first (zero-network), fall back to a
56980
+ * runtime fetch with per-(appId, key) caching for the page lifetime.
56981
+ *
56982
+ * Returns `null` when neither source resolves a value. Network / 4xx
56983
+ * errors are swallowed silently — the resolver downgrades to "key
56984
+ * unset" in that case (consistent with the silent-fallback pattern in
56985
+ * `useAppManifest`'s backend-merge step).
56986
+ *
56987
+ * @param {string} appId Nextcloud app ID.
56988
+ * @param {string} key IAppConfig key (already validated as
56989
+ * lowercase + alphanumeric + `_-` by the sentinel regex).
56990
+ * @return {Promise<*>} Resolved value or `null` when unset.
56991
+ */
56992
+ async function defaultGetAppConfigValue(appId, key) {
56993
+ const cacheKey = `${appId}::${key}`;
56994
+ if (resolveCache.has(cacheKey)) {
56995
+ return resolveCache.get(cacheKey)
56996
+ }
56997
+ const promise = (async () => {
56998
+ // Step 1: initial-state — synchronous, zero-network.
56999
+ const initial = readInitialState(appId, key);
57000
+ if (initial !== undefined && initial !== null && initial !== '') {
57001
+ return initial
57002
+ }
57003
+ // Step 2: runtime fetch — silent fallback on any error.
57004
+ try {
57005
+ const { default: axios } = await import('@nextcloud/axios');
57006
+ const { generateUrl } = await import('@nextcloud/router');
57007
+ const url = generateUrl(`/apps/${appId}/api/configs/${key}`);
57008
+ const response = await axios.get(url);
57009
+ if (response && response.status === 200 && response.data !== undefined) {
57010
+ const data = response.data;
57011
+ // API may return either a raw scalar or `{ value: ... }`.
57012
+ if (isPlainObject$1(data) && 'value' in data) return data.value
57013
+ return data
57014
+ }
57015
+ } catch (e) {
57016
+ // Silent — caller treats as "unset".
57017
+ }
57018
+ return null
57019
+ })();
57020
+ resolveCache.set(cacheKey, promise);
57021
+ return promise
57022
+ }
57023
+
57024
+ /**
57025
+ * Read a key from `@nextcloud/initial-state`. Looks up the conventional
57026
+ * `app-{appId}-{key}` slot. Returns `undefined` when the package is not
57027
+ * installed (e.g. older host) or the key is not provisioned.
57028
+ *
57029
+ * @param {string} appId Nextcloud app ID.
57030
+ * @param {string} key IAppConfig key.
57031
+ * @return {*} Provisioned value or `undefined`.
57032
+ */
57033
+ function readInitialState(appId, key) {
57034
+ try {
57035
+ // `@nextcloud/initial-state` is an optional peer; the host page
57036
+ // may not provision the slot at all. We resolve via require so
57037
+ // jest mocks the import; bundle-side, the package is treeshaken
57038
+ // when no caller pulls it in.
57039
+ // eslint-disable-next-line global-require, import/no-unresolved, n/no-extraneous-require
57040
+ const mod = require('@nextcloud/initial-state');
57041
+ if (typeof mod.loadState === 'function') {
57042
+ return mod.loadState(appId, key, undefined)
57043
+ }
57044
+ } catch (e) {
57045
+ // Package not installed or no slot provisioned — fall through.
57046
+ }
57047
+ return undefined
57048
+ }
57049
+
57050
+ /**
57051
+ * Type guard — true when value is a plain (non-array, non-null) object.
57052
+ *
57053
+ * @param {*} value Candidate.
57054
+ * @return {boolean} True when value is a plain object.
57055
+ */
57056
+ function isPlainObject$1(value) {
57057
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
57058
+ }
57059
+
57060
+ /**
57061
+ * Composable that loads, resolves, and validates a Conduction app manifest.
57062
+ *
57063
+ * The composable implements the four-phase flow specified in
57064
+ * REQ-JMR-002 of the json-manifest-renderer capability + the
57065
+ * substitution step from the `manifest-resolve-sentinel` capability:
56728
57066
  *
56729
57067
  * 1. Synchronous bundled load — `bundledManifest` is the immediate value.
56730
57068
  * 2. Async backend merge — fetches `/index.php/apps/{appId}/api/manifest`
56731
57069
  * and deep-merges any 200 response over the bundled manifest. 4xx /
56732
57070
  * 5xx / network errors are silently ignored so apps work without a
56733
57071
  * backend endpoint.
56734
- * 3. Validationthe merged result is validated against
57072
+ * 3. Sentinel resolution `@resolve:<key>` strings under
57073
+ * `pages[].config` are substituted with `IAppConfig` values via the
57074
+ * `resolveManifestSentinels` utility (see its module docs for the
57075
+ * resolution source chain). Unresolved keys surface on the
57076
+ * returned `unresolvedSentinels` ref.
57077
+ * 4. Validation — the resolved result is validated against
56735
57078
  * `app-manifest.schema.json`. On failure, the bundled manifest is
56736
57079
  * kept and a `console.warn` is emitted with the error list.
56737
57080
  *
@@ -56739,7 +57082,8 @@ function validateFieldsArray(fields, fieldsPath, errors) {
56739
57082
  * can hot-swap the manifest without a page reload.
56740
57083
  *
56741
57084
  * @param {string} appId Nextcloud app ID. Used to build the default
56742
- * backend endpoint URL via `@nextcloud/router`.
57085
+ * backend endpoint URL via `@nextcloud/router` and to scope
57086
+ * IAppConfig lookups for `@resolve:<key>` sentinels.
56743
57087
  * @param {object} bundledManifest The manifest shipped with the app (the
56744
57088
  * default value, available synchronously).
56745
57089
  * @param {object} [options] Configuration options.
@@ -56749,7 +57093,10 @@ function validateFieldsArray(fields, fieldsPath, errors) {
56749
57093
  * return a promise resolving to `{ status: number, data: object }`.
56750
57094
  * Defaults to `axios.get` from `@nextcloud/axios` (which inherits the
56751
57095
  * Nextcloud CSRF token automatically).
56752
- * @return {{ manifest: import('vue').Ref<object>, isLoading: import('vue').Ref<boolean>, validationErrors: import('vue').Ref<string[]|null> }}
57096
+ * @param {Function} [options.getAppConfigValue] Override the
57097
+ * IAppConfig resolver consumed by `resolveManifestSentinels`. Useful
57098
+ * for tests that want to mount a fixture-driven config map.
57099
+ * @return {{ manifest: import('vue').Ref<object>, isLoading: import('vue').Ref<boolean>, validationErrors: import('vue').Ref<string[]|null>, unresolvedSentinels: import('vue').Ref<string[]> }}
56753
57100
  *
56754
57101
  * @example Basic usage (Composition API)
56755
57102
  * const { manifest, isLoading } = useAppManifest('decidesk', bundled)
@@ -56766,11 +57113,17 @@ function validateFieldsArray(fields, fieldsPath, errors) {
56766
57113
  * endpoint: '/custom/manifest/url',
56767
57114
  * fetcher: (url) => Promise.resolve({ status: 200, data: { ... } }),
56768
57115
  * })
57116
+ *
57117
+ * @example With sentinel resolution + admin warning surface
57118
+ * const { manifest, unresolvedSentinels } = useAppManifest('softwarecatalog', bundled)
57119
+ * // unresolvedSentinels.value is e.g. ['voorzieningen_register']
57120
+ * // when that IAppConfig key is unset on the tenant.
56769
57121
  */
56770
57122
  function useAppManifest(appId, bundledManifest, options = {}) {
56771
57123
  const manifest = Vue.ref(bundledManifest);
56772
57124
  const isLoading = Vue.ref(true);
56773
57125
  const validationErrors = Vue.ref(null);
57126
+ const unresolvedSentinels = Vue.ref([]);
56774
57127
 
56775
57128
  const endpoint = options.endpoint ?? router.generateUrl(`/apps/${appId}/api/manifest`);
56776
57129
  const fetcher = options.fetcher ?? ((url) => axios.get(url))
@@ -56782,7 +57135,18 @@ function useAppManifest(appId, bundledManifest, options = {}) {
56782
57135
  return
56783
57136
  }
56784
57137
  const merged = deepMerge(bundledManifest, response.data);
56785
- const result = validateManifest(merged);
57138
+
57139
+ // Sentinel resolution runs BEFORE validation per
57140
+ // REQ-MRS-002: the validator MUST NEVER observe an
57141
+ // unresolved sentinel at runtime. Resolution failures
57142
+ // (unset IAppConfig keys) substitute null and accumulate
57143
+ // on `unresolvedSentinels`; they do NOT block validation.
57144
+ const { manifest: resolved, unresolved } = await resolveManifestSentinels(merged, appId, {
57145
+ getAppConfigValue: options.getAppConfigValue,
57146
+ });
57147
+ unresolvedSentinels.value = unresolved;
57148
+
57149
+ const result = validateManifest(resolved);
56786
57150
  if (!result.valid) {
56787
57151
  validationErrors.value = result.errors;
56788
57152
  // eslint-disable-next-line no-console
@@ -56792,7 +57156,7 @@ function useAppManifest(appId, bundledManifest, options = {}) {
56792
57156
  );
56793
57157
  return
56794
57158
  }
56795
- manifest.value = merged;
57159
+ manifest.value = resolved;
56796
57160
  } catch (err) {
56797
57161
  // Silent fallback on 404, network errors, non-200 responses.
56798
57162
  // Apps without a backend endpoint should keep working.
@@ -56801,7 +57165,7 @@ function useAppManifest(appId, bundledManifest, options = {}) {
56801
57165
  }
56802
57166
  })();
56803
57167
 
56804
- return { manifest, isLoading, validationErrors }
57168
+ return { manifest, isLoading, validationErrors, unresolvedSentinels }
56805
57169
  }
56806
57170
 
56807
57171
  /**
@@ -112612,6 +112976,7 @@ exports.SEARCH_TYPE = SEARCH_TYPE;
112612
112976
  exports.auditTrailsPlugin = auditTrailsPlugin;
112613
112977
  exports.buildHeaders = buildHeaders;
112614
112978
  exports.buildQueryString = buildQueryString;
112979
+ exports.clearResolveCache = clearResolveCache;
112615
112980
  exports.columnsFromSchema = columnsFromSchema;
112616
112981
  exports.createCrudStore = createCrudStore;
112617
112982
  exports.createObjectStore = createObjectStore;
@@ -112639,6 +113004,7 @@ exports.registerMappingPlugin = registerMappingPlugin;
112639
113004
  exports.registerTranslations = registerTranslations;
112640
113005
  exports.relationsPlugin = relationsPlugin;
112641
113006
  exports.resetVisibilityCache = resetVisibilityCache;
113007
+ exports.resolveManifestSentinels = resolveManifestSentinels;
112642
113008
  exports.searchPlugin = searchPlugin;
112643
113009
  exports.selectionPlugin = selectionPlugin;
112644
113010
  exports.useAppManifest = useAppManifest;