No declarative way to constrain an object FIELD to a value domain (iana_time_zone), and neither extension point an app can reach can express it #14168

Description

@os-warren

Found while adding "this must be a real IANA zone" to an application field (duly_duty.timezone, objectstack-ai/duly#24). Filing per that app's rule "if the platform genuinely cannot express it, file upstream rather than quietly working around it".

The gap

packages/spec already publishes the vocabulary and, valuably, the definition of membershipSpecifierValueDomainSchema in system/settings-manifest.zod.ts:

'iana_time_zone' | 'iso_4217_currency' | 'iso_3166_alpha2'

with the note that for iana_time_zone"membership is the Intl.DateTimeFormat probe … NOT Intl.supportedValuesOf('timeZone')". That note is correct and it saved me a bug — see the measurement below.

But valueDomain is reachable only from a settings Specifier. An object field has no equivalent. FieldType has no timezone member, and FieldSchema carries maxLength / minLength / min / max but no value-domain slot. So an authored zone field is a bare Field.text and nothing checks it; the value is discovered to be wrong wherever it is finally consumed.

Why an application cannot close it at its own layer either

Both extension points an app can reach were measured on @objectstack/spec / @objectstack/runtime / @objectstack/objectql17.2.0, Node v22.22.2.

1. A script / cross_field validation rule (CEL) cannot. The whole stdlib registered in @objectstack/formula's registerStdLib is:

now today daysFromNow daysAgo isBlank coalesce trim joinNonEmpty daysBetween
addDays addMonths date datetime abs round floor ceil min max upper lower
contains startsWith endsWith matches len isEmpty

There is no zone/currency/country oracle, and no app-level way to register one — buildEnv is internal to the package. The only reachable spelling is matches(record.timezone, '<regex>'), which either checks shape only (Europe/Munich passes) or freezes a tzdata snapshot into metadata — precisely what the iana_time_zone note warns is a measurably different set from what the host accepts.

2. An L2 hook body cannot: the sandbox has no Intl. Measured directly against the runtime's own sandbox (quickjs-emscripten 0.32.0, newQuickJSWASMModule, the variant AppPlugin wires through QuickJSScriptRunner):

typeof Intl -> undefined
typeof Date -> function
typeof JSON -> object

HookBodyCapability is api.read | api.write | api.transaction | crypto.uuid | log — nothing grants Intl, so this is not a capability the author forgot to declare.

The part that makes this actively hazardous, not merely missing

objectstack buildsilently lowers a self-contained inline handler into an L2 body — confirmed in a real artifact, where a hook authored as handler: <inline fn> ships as:

{ "handler": "duly_task_lifecycle_stamps",
"body": { "language": "js", "source": "", "capabilities": [] } }

and resolveHandler in bindHooksToEngine prefers body and ignores handler whenever both are present. So the natural way to write this check — an inline beforeInsert handler doing the Intl.DateTimeFormat probe — behaves like this:

  • pnpm validate, typecheck, test and build are all green (in-process tests run the raw JS function in Node, where Intl exists);
  • in production the lowered body throws ReferenceError: Intl is not defined inside the sandbox;
  • with the onError: 'abort' that a validation-shaped hook must declare, every write to that object is refused.

Nothing anywhere in that sequence says the handler moved to a runtime that lacks the global it uses.

The only route that keeps the probe in Node is the string handler ref (handler: 'my_fn' + defineStack({ functions })), resolved against the bundle functions map + runtimeModule. That works — but hook.zod.ts marks handler"DEPRECATED, prefer body" and warnLegacyHandler prints "Move the handler source into Hook.body". Following that advice on any host-API-dependent check converts a working guard into a refuse-everything hook. The deprecation direction and the only viable route for this class of check point opposite ways.

What would close it

In rough order of preference:

  1. valueDomain on a fieldField.text({ valueDomain: 'iana_time_zone' }), enforced on the write path with the membership definition already written down in settings-manifest.zod.ts. Declarative, translatable refusal message, visible to Studio / OpenAPI / the form layer, and one oracle for settings and fields alike.
  2. A format validation member alongside email | url | phone | json drawn from the same closed vocabulary.
  3. Failing both: state in hook.zod.ts that a body cannot reach host intrinsics and that the string handler ref is the supported route for checks that need them, so the deprecation does not read as advice to break them.

Independently of (1)-(3), the sandbox-lowering hazard seems worth a build-time diagnostic on its own: an inline handler that references a global the sandbox does not provide is lowerable, buildable, testable and broken only in production.

Meanwhile

objectstack-ai/duly is using the string handler ref with the Intl.DateTimeFormat probe in Node, sharing one oracle with the period engine that consumes the value, and pointing at this issue in the code.


Measurement backing the iana_time_zone note, since it is the crux and it is worth having twice — Node v22.22.2:

Intl.supportedValuesOf('timeZone').length -> 418
includes 'UTC' -> false
includes 'GMT' -> false
includes 'Asia/Kolkata' -> false
includes 'Europe/Kyiv' -> false
includes 'US/Eastern' -> false
new Intl.DateTimeFormat('en-US', { timeZone: v }) -> resolves for every one of them

A field constrained against the enumerated list would reject UTC — the platform's own declared default.


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
       blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      Skip to content

      No declarative way to constrain an object FIELD to a value domain (iana_time_zone), and neither extension point an app can reach can express it #14168

      Description

      @os-warren

      Found while adding "this must be a real IANA zone" to an application field (duly_duty.timezone, objectstack-ai/duly#24). Filing per that app's rule "if the platform genuinely cannot express it, file upstream rather than quietly working around it".

      The gap

      packages/spec already publishes the vocabulary and, valuably, the definition of membershipSpecifierValueDomainSchema in system/settings-manifest.zod.ts:

      'iana_time_zone' | 'iso_4217_currency' | 'iso_3166_alpha2'
      

      with the note that for iana_time_zone"membership is the Intl.DateTimeFormat probe … NOT Intl.supportedValuesOf('timeZone')". That note is correct and it saved me a bug — see the measurement below.

      But valueDomain is reachable only from a settings Specifier. An object field has no equivalent. FieldType has no timezone member, and FieldSchema carries maxLength / minLength / min / max but no value-domain slot. So an authored zone field is a bare Field.text and nothing checks it; the value is discovered to be wrong wherever it is finally consumed.

      Why an application cannot close it at its own layer either

      Both extension points an app can reach were measured on @objectstack/spec / @objectstack/runtime / @objectstack/objectql17.2.0, Node v22.22.2.

      1. A script / cross_field validation rule (CEL) cannot. The whole stdlib registered in @objectstack/formula's registerStdLib is:

      now today daysFromNow daysAgo isBlank coalesce trim joinNonEmpty daysBetween
      addDays addMonths date datetime abs round floor ceil min max upper lower
      contains startsWith endsWith matches len isEmpty
      

      There is no zone/currency/country oracle, and no app-level way to register one — buildEnv is internal to the package. The only reachable spelling is matches(record.timezone, '<regex>'), which either checks shape only (Europe/Munich passes) or freezes a tzdata snapshot into metadata — precisely what the iana_time_zone note warns is a measurably different set from what the host accepts.

      2. An L2 hook body cannot: the sandbox has no Intl. Measured directly against the runtime's own sandbox (quickjs-emscripten 0.32.0, newQuickJSWASMModule, the variant AppPlugin wires through QuickJSScriptRunner):

      typeof Intl -> undefined
      typeof Date -> function
      typeof JSON -> object
      

      HookBodyCapability is api.read | api.write | api.transaction | crypto.uuid | log — nothing grants Intl, so this is not a capability the author forgot to declare.

      The part that makes this actively hazardous, not merely missing

      objectstack buildsilently lowers a self-contained inline handler into an L2 body — confirmed in a real artifact, where a hook authored as handler: <inline fn> ships as:

      { "handler": "duly_task_lifecycle_stamps",
      "body": { "language": "js", "source": "", "capabilities": [] } }

      and resolveHandler in bindHooksToEngine prefers body and ignores handler whenever both are present. So the natural way to write this check — an inline beforeInsert handler doing the Intl.DateTimeFormat probe — behaves like this:

      • pnpm validate, typecheck, test and build are all green (in-process tests run the raw JS function in Node, where Intl exists);
      • in production the lowered body throws ReferenceError: Intl is not defined inside the sandbox;
      • with the onError: 'abort' that a validation-shaped hook must declare, every write to that object is refused.

      Nothing anywhere in that sequence says the handler moved to a runtime that lacks the global it uses.

      The only route that keeps the probe in Node is the string handler ref (handler: 'my_fn' + defineStack({ functions })), resolved against the bundle functions map + runtimeModule. That works — but hook.zod.ts marks handler"DEPRECATED, prefer body" and warnLegacyHandler prints "Move the handler source into Hook.body". Following that advice on any host-API-dependent check converts a working guard into a refuse-everything hook. The deprecation direction and the only viable route for this class of check point opposite ways.

      What would close it

      In rough order of preference:

      1. valueDomain on a fieldField.text({ valueDomain: 'iana_time_zone' }), enforced on the write path with the membership definition already written down in settings-manifest.zod.ts. Declarative, translatable refusal message, visible to Studio / OpenAPI / the form layer, and one oracle for settings and fields alike.
      2. A format validation member alongside email | url | phone | json drawn from the same closed vocabulary.
      3. Failing both: state in hook.zod.ts that a body cannot reach host intrinsics and that the string handler ref is the supported route for checks that need them, so the deprecation does not read as advice to break them.

      Independently of (1)-(3), the sandbox-lowering hazard seems worth a build-time diagnostic on its own: an inline handler that references a global the sandbox does not provide is lowerable, buildable, testable and broken only in production.

      Meanwhile

      objectstack-ai/duly is using the string handler ref with the Intl.DateTimeFormat probe in Node, sharing one oracle with the period engine that consumes the value, and pointing at this issue in the code.


      Measurement backing the iana_time_zone note, since it is the crux and it is worth having twice — Node v22.22.2:

      Intl.supportedValuesOf('timeZone').length -> 418
      includes 'UTC' -> false
      includes 'GMT' -> false
      includes 'Asia/Kolkata' -> false
      includes 'Europe/Kyiv' -> false
      includes 'US/Eastern' -> false
      new Intl.DateTimeFormat('en-US', { timeZone: v }) -> resolves for every one of them
      

      A field constrained against the enumerated list would reject UTC — the platform's own declared default.


      Generated by Claude Code

      Metadata

      Metadata

      Assignees

      No one assigned

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          No declarative way to constrain an object FIELD to a value domain (iana_time_zone), and neither extension point an app can reach can express it #14168

          Description

          @os-warren

          Found while adding "this must be a real IANA zone" to an application field (duly_duty.timezone, objectstack-ai/duly#24). Filing per that app's rule "if the platform genuinely cannot express it, file upstream rather than quietly working around it".

          The gap

          packages/spec already publishes the vocabulary and, valuably, the definition of membershipSpecifierValueDomainSchema in system/settings-manifest.zod.ts:

          'iana_time_zone' | 'iso_4217_currency' | 'iso_3166_alpha2'
          

          with the note that for iana_time_zone"membership is the Intl.DateTimeFormat probe … NOT Intl.supportedValuesOf('timeZone')". That note is correct and it saved me a bug — see the measurement below.

          But valueDomain is reachable only from a settings Specifier. An object field has no equivalent. FieldType has no timezone member, and FieldSchema carries maxLength / minLength / min / max but no value-domain slot. So an authored zone field is a bare Field.text and nothing checks it; the value is discovered to be wrong wherever it is finally consumed.

          Why an application cannot close it at its own layer either

          Both extension points an app can reach were measured on @objectstack/spec / @objectstack/runtime / @objectstack/objectql17.2.0, Node v22.22.2.

          1. A script / cross_field validation rule (CEL) cannot. The whole stdlib registered in @objectstack/formula's registerStdLib is:

          now today daysFromNow daysAgo isBlank coalesce trim joinNonEmpty daysBetween
          addDays addMonths date datetime abs round floor ceil min max upper lower
          contains startsWith endsWith matches len isEmpty
          

          There is no zone/currency/country oracle, and no app-level way to register one — buildEnv is internal to the package. The only reachable spelling is matches(record.timezone, '<regex>'), which either checks shape only (Europe/Munich passes) or freezes a tzdata snapshot into metadata — precisely what the iana_time_zone note warns is a measurably different set from what the host accepts.

          2. An L2 hook body cannot: the sandbox has no Intl. Measured directly against the runtime's own sandbox (quickjs-emscripten 0.32.0, newQuickJSWASMModule, the variant AppPlugin wires through QuickJSScriptRunner):

          typeof Intl -> undefined
          typeof Date -> function
          typeof JSON -> object
          

          HookBodyCapability is api.read | api.write | api.transaction | crypto.uuid | log — nothing grants Intl, so this is not a capability the author forgot to declare.

          The part that makes this actively hazardous, not merely missing

          objectstack buildsilently lowers a self-contained inline handler into an L2 body — confirmed in a real artifact, where a hook authored as handler: <inline fn> ships as:

          { "handler": "duly_task_lifecycle_stamps",
          "body": { "language": "js", "source": "", "capabilities": [] } }

          and resolveHandler in bindHooksToEngine prefers body and ignores handler whenever both are present. So the natural way to write this check — an inline beforeInsert handler doing the Intl.DateTimeFormat probe — behaves like this:

          • pnpm validate, typecheck, test and build are all green (in-process tests run the raw JS function in Node, where Intl exists);
          • in production the lowered body throws ReferenceError: Intl is not defined inside the sandbox;
          • with the onError: 'abort' that a validation-shaped hook must declare, every write to that object is refused.

          Nothing anywhere in that sequence says the handler moved to a runtime that lacks the global it uses.

          The only route that keeps the probe in Node is the string handler ref (handler: 'my_fn' + defineStack({ functions })), resolved against the bundle functions map + runtimeModule. That works — but hook.zod.ts marks handler"DEPRECATED, prefer body" and warnLegacyHandler prints "Move the handler source into Hook.body". Following that advice on any host-API-dependent check converts a working guard into a refuse-everything hook. The deprecation direction and the only viable route for this class of check point opposite ways.

          What would close it

          In rough order of preference:

          1. valueDomain on a fieldField.text({ valueDomain: 'iana_time_zone' }), enforced on the write path with the membership definition already written down in settings-manifest.zod.ts. Declarative, translatable refusal message, visible to Studio / OpenAPI / the form layer, and one oracle for settings and fields alike.
          2. A format validation member alongside email | url | phone | json drawn from the same closed vocabulary.
          3. Failing both: state in hook.zod.ts that a body cannot reach host intrinsics and that the string handler ref is the supported route for checks that need them, so the deprecation does not read as advice to break them.

          Independently of (1)-(3), the sandbox-lowering hazard seems worth a build-time diagnostic on its own: an inline handler that references a global the sandbox does not provide is lowerable, buildable, testable and broken only in production.

          Meanwhile

          objectstack-ai/duly is using the string handler ref with the Intl.DateTimeFormat probe in Node, sharing one oracle with the period engine that consumes the value, and pointing at this issue in the code.


          Measurement backing the iana_time_zone note, since it is the crux and it is worth having twice — Node v22.22.2:

          Intl.supportedValuesOf('timeZone').length -> 418
          includes 'UTC' -> false
          includes 'GMT' -> false
          includes 'Asia/Kolkata' -> false
          includes 'Europe/Kyiv' -> false
          includes 'US/Eastern' -> false
          new Intl.DateTimeFormat('en-US', { timeZone: v }) -> resolves for every one of them
          

          A field constrained against the enumerated list would reject UTC — the platform's own declared default.


          Generated by Claude Code

          Metadata

          Metadata

          Assignees

          No one assigned

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              No declarative way to constrain an object FIELD to a value domain (iana_time_zone), and neither extension point an app can reach can express it #14168

              Description

              @os-warren

              Found while adding "this must be a real IANA zone" to an application field (duly_duty.timezone, objectstack-ai/duly#24). Filing per that app's rule "if the platform genuinely cannot express it, file upstream rather than quietly working around it".

              The gap

              packages/spec already publishes the vocabulary and, valuably, the definition of membershipSpecifierValueDomainSchema in system/settings-manifest.zod.ts:

              'iana_time_zone' | 'iso_4217_currency' | 'iso_3166_alpha2'
              

              with the note that for iana_time_zone"membership is the Intl.DateTimeFormat probe … NOT Intl.supportedValuesOf('timeZone')". That note is correct and it saved me a bug — see the measurement below.

              But valueDomain is reachable only from a settings Specifier. An object field has no equivalent. FieldType has no timezone member, and FieldSchema carries maxLength / minLength / min / max but no value-domain slot. So an authored zone field is a bare Field.text and nothing checks it; the value is discovered to be wrong wherever it is finally consumed.

              Why an application cannot close it at its own layer either

              Both extension points an app can reach were measured on @objectstack/spec / @objectstack/runtime / @objectstack/objectql17.2.0, Node v22.22.2.

              1. A script / cross_field validation rule (CEL) cannot. The whole stdlib registered in @objectstack/formula's registerStdLib is:

              now today daysFromNow daysAgo isBlank coalesce trim joinNonEmpty daysBetween
              addDays addMonths date datetime abs round floor ceil min max upper lower
              contains startsWith endsWith matches len isEmpty
              

              There is no zone/currency/country oracle, and no app-level way to register one — buildEnv is internal to the package. The only reachable spelling is matches(record.timezone, '<regex>'), which either checks shape only (Europe/Munich passes) or freezes a tzdata snapshot into metadata — precisely what the iana_time_zone note warns is a measurably different set from what the host accepts.

              2. An L2 hook body cannot: the sandbox has no Intl. Measured directly against the runtime's own sandbox (quickjs-emscripten 0.32.0, newQuickJSWASMModule, the variant AppPlugin wires through QuickJSScriptRunner):

              typeof Intl -> undefined
              typeof Date -> function
              typeof JSON -> object
              

              HookBodyCapability is api.read | api.write | api.transaction | crypto.uuid | log — nothing grants Intl, so this is not a capability the author forgot to declare.

              The part that makes this actively hazardous, not merely missing

              objectstack buildsilently lowers a self-contained inline handler into an L2 body — confirmed in a real artifact, where a hook authored as handler: <inline fn> ships as:

              { "handler": "duly_task_lifecycle_stamps",
              "body": { "language": "js", "source": "", "capabilities": [] } }

              and resolveHandler in bindHooksToEngine prefers body and ignores handler whenever both are present. So the natural way to write this check — an inline beforeInsert handler doing the Intl.DateTimeFormat probe — behaves like this:

              • pnpm validate, typecheck, test and build are all green (in-process tests run the raw JS function in Node, where Intl exists);
              • in production the lowered body throws ReferenceError: Intl is not defined inside the sandbox;
              • with the onError: 'abort' that a validation-shaped hook must declare, every write to that object is refused.

              Nothing anywhere in that sequence says the handler moved to a runtime that lacks the global it uses.

              The only route that keeps the probe in Node is the string handler ref (handler: 'my_fn' + defineStack({ functions })), resolved against the bundle functions map + runtimeModule. That works — but hook.zod.ts marks handler"DEPRECATED, prefer body" and warnLegacyHandler prints "Move the handler source into Hook.body". Following that advice on any host-API-dependent check converts a working guard into a refuse-everything hook. The deprecation direction and the only viable route for this class of check point opposite ways.

              What would close it

              In rough order of preference:

              1. valueDomain on a fieldField.text({ valueDomain: 'iana_time_zone' }), enforced on the write path with the membership definition already written down in settings-manifest.zod.ts. Declarative, translatable refusal message, visible to Studio / OpenAPI / the form layer, and one oracle for settings and fields alike.
              2. A format validation member alongside email | url | phone | json drawn from the same closed vocabulary.
              3. Failing both: state in hook.zod.ts that a body cannot reach host intrinsics and that the string handler ref is the supported route for checks that need them, so the deprecation does not read as advice to break them.

              Independently of (1)-(3), the sandbox-lowering hazard seems worth a build-time diagnostic on its own: an inline handler that references a global the sandbox does not provide is lowerable, buildable, testable and broken only in production.

              Meanwhile

              objectstack-ai/duly is using the string handler ref with the Intl.DateTimeFormat probe in Node, sharing one oracle with the period engine that consumes the value, and pointing at this issue in the code.


              Measurement backing the iana_time_zone note, since it is the crux and it is worth having twice — Node v22.22.2:

              Intl.supportedValuesOf('timeZone').length -> 418
              includes 'UTC' -> false
              includes 'GMT' -> false
              includes 'Asia/Kolkata' -> false
              includes 'Europe/Kyiv' -> false
              includes 'US/Eastern' -> false
              new Intl.DateTimeFormat('en-US', { timeZone: v }) -> resolves for every one of them
              

              A field constrained against the enumerated list would reject UTC — the platform's own declared default.


              Generated by Claude Code

              Metadata

              Metadata

              Assignees

              No one assigned

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  No declarative way to constrain an object FIELD to a value domain (iana_time_zone), and neither extension point an app can reach can express it #14168

                  Description

                  @os-warren

                  Found while adding "this must be a real IANA zone" to an application field (duly_duty.timezone, objectstack-ai/duly#24). Filing per that app's rule "if the platform genuinely cannot express it, file upstream rather than quietly working around it".

                  The gap

                  packages/spec already publishes the vocabulary and, valuably, the definition of membershipSpecifierValueDomainSchema in system/settings-manifest.zod.ts:

                  'iana_time_zone' | 'iso_4217_currency' | 'iso_3166_alpha2'
                  

                  with the note that for iana_time_zone"membership is the Intl.DateTimeFormat probe … NOT Intl.supportedValuesOf('timeZone')". That note is correct and it saved me a bug — see the measurement below.

                  But valueDomain is reachable only from a settings Specifier. An object field has no equivalent. FieldType has no timezone member, and FieldSchema carries maxLength / minLength / min / max but no value-domain slot. So an authored zone field is a bare Field.text and nothing checks it; the value is discovered to be wrong wherever it is finally consumed.

                  Why an application cannot close it at its own layer either

                  Both extension points an app can reach were measured on @objectstack/spec / @objectstack/runtime / @objectstack/objectql17.2.0, Node v22.22.2.

                  1. A script / cross_field validation rule (CEL) cannot. The whole stdlib registered in @objectstack/formula's registerStdLib is:

                  now today daysFromNow daysAgo isBlank coalesce trim joinNonEmpty daysBetween
                  addDays addMonths date datetime abs round floor ceil min max upper lower
                  contains startsWith endsWith matches len isEmpty
                  

                  There is no zone/currency/country oracle, and no app-level way to register one — buildEnv is internal to the package. The only reachable spelling is matches(record.timezone, '<regex>'), which either checks shape only (Europe/Munich passes) or freezes a tzdata snapshot into metadata — precisely what the iana_time_zone note warns is a measurably different set from what the host accepts.

                  2. An L2 hook body cannot: the sandbox has no Intl. Measured directly against the runtime's own sandbox (quickjs-emscripten 0.32.0, newQuickJSWASMModule, the variant AppPlugin wires through QuickJSScriptRunner):

                  typeof Intl -> undefined
                  typeof Date -> function
                  typeof JSON -> object
                  

                  HookBodyCapability is api.read | api.write | api.transaction | crypto.uuid | log — nothing grants Intl, so this is not a capability the author forgot to declare.

                  The part that makes this actively hazardous, not merely missing

                  objectstack buildsilently lowers a self-contained inline handler into an L2 body — confirmed in a real artifact, where a hook authored as handler: <inline fn> ships as:

                  { "handler": "duly_task_lifecycle_stamps",
                  "body": { "language": "js", "source": "", "capabilities": [] } }

                  and resolveHandler in bindHooksToEngine prefers body and ignores handler whenever both are present. So the natural way to write this check — an inline beforeInsert handler doing the Intl.DateTimeFormat probe — behaves like this:

                  • pnpm validate, typecheck, test and build are all green (in-process tests run the raw JS function in Node, where Intl exists);
                  • in production the lowered body throws ReferenceError: Intl is not defined inside the sandbox;
                  • with the onError: 'abort' that a validation-shaped hook must declare, every write to that object is refused.

                  Nothing anywhere in that sequence says the handler moved to a runtime that lacks the global it uses.

                  The only route that keeps the probe in Node is the string handler ref (handler: 'my_fn' + defineStack({ functions })), resolved against the bundle functions map + runtimeModule. That works — but hook.zod.ts marks handler"DEPRECATED, prefer body" and warnLegacyHandler prints "Move the handler source into Hook.body". Following that advice on any host-API-dependent check converts a working guard into a refuse-everything hook. The deprecation direction and the only viable route for this class of check point opposite ways.

                  What would close it

                  In rough order of preference:

                  1. valueDomain on a fieldField.text({ valueDomain: 'iana_time_zone' }), enforced on the write path with the membership definition already written down in settings-manifest.zod.ts. Declarative, translatable refusal message, visible to Studio / OpenAPI / the form layer, and one oracle for settings and fields alike.
                  2. A format validation member alongside email | url | phone | json drawn from the same closed vocabulary.
                  3. Failing both: state in hook.zod.ts that a body cannot reach host intrinsics and that the string handler ref is the supported route for checks that need them, so the deprecation does not read as advice to break them.

                  Independently of (1)-(3), the sandbox-lowering hazard seems worth a build-time diagnostic on its own: an inline handler that references a global the sandbox does not provide is lowerable, buildable, testable and broken only in production.

                  Meanwhile

                  objectstack-ai/duly is using the string handler ref with the Intl.DateTimeFormat probe in Node, sharing one oracle with the period engine that consumes the value, and pointing at this issue in the code.


                  Measurement backing the iana_time_zone note, since it is the crux and it is worth having twice — Node v22.22.2:

                  Intl.supportedValuesOf('timeZone').length -> 418
                  includes 'UTC' -> false
                  includes 'GMT' -> false
                  includes 'Asia/Kolkata' -> false
                  includes 'Europe/Kyiv' -> false
                  includes 'US/Eastern' -> false
                  new Intl.DateTimeFormat('en-US', { timeZone: v }) -> resolves for every one of them
                  

                  A field constrained against the enumerated list would reject UTC — the platform's own declared default.


                  Generated by Claude Code

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      No declarative way to constrain an object FIELD to a value domain (iana_time_zone), and neither extension point an app can reach can express it #14168

                      Description

                      @os-warren

                      Found while adding "this must be a real IANA zone" to an application field (duly_duty.timezone, objectstack-ai/duly#24). Filing per that app's rule "if the platform genuinely cannot express it, file upstream rather than quietly working around it".

                      The gap

                      packages/spec already publishes the vocabulary and, valuably, the definition of membershipSpecifierValueDomainSchema in system/settings-manifest.zod.ts:

                      'iana_time_zone' | 'iso_4217_currency' | 'iso_3166_alpha2'
                      

                      with the note that for iana_time_zone"membership is the Intl.DateTimeFormat probe … NOT Intl.supportedValuesOf('timeZone')". That note is correct and it saved me a bug — see the measurement below.

                      But valueDomain is reachable only from a settings Specifier. An object field has no equivalent. FieldType has no timezone member, and FieldSchema carries maxLength / minLength / min / max but no value-domain slot. So an authored zone field is a bare Field.text and nothing checks it; the value is discovered to be wrong wherever it is finally consumed.

                      Why an application cannot close it at its own layer either

                      Both extension points an app can reach were measured on @objectstack/spec / @objectstack/runtime / @objectstack/objectql17.2.0, Node v22.22.2.

                      1. A script / cross_field validation rule (CEL) cannot. The whole stdlib registered in @objectstack/formula's registerStdLib is:

                      now today daysFromNow daysAgo isBlank coalesce trim joinNonEmpty daysBetween
                      addDays addMonths date datetime abs round floor ceil min max upper lower
                      contains startsWith endsWith matches len isEmpty
                      

                      There is no zone/currency/country oracle, and no app-level way to register one — buildEnv is internal to the package. The only reachable spelling is matches(record.timezone, '<regex>'), which either checks shape only (Europe/Munich passes) or freezes a tzdata snapshot into metadata — precisely what the iana_time_zone note warns is a measurably different set from what the host accepts.

                      2. An L2 hook body cannot: the sandbox has no Intl. Measured directly against the runtime's own sandbox (quickjs-emscripten 0.32.0, newQuickJSWASMModule, the variant AppPlugin wires through QuickJSScriptRunner):

                      typeof Intl -> undefined
                      typeof Date -> function
                      typeof JSON -> object
                      

                      HookBodyCapability is api.read | api.write | api.transaction | crypto.uuid | log — nothing grants Intl, so this is not a capability the author forgot to declare.

                      The part that makes this actively hazardous, not merely missing

                      objectstack buildsilently lowers a self-contained inline handler into an L2 body — confirmed in a real artifact, where a hook authored as handler: <inline fn> ships as:

                      { "handler": "duly_task_lifecycle_stamps",
                      "body": { "language": "js", "source": "", "capabilities": [] } }

                      and resolveHandler in bindHooksToEngine prefers body and ignores handler whenever both are present. So the natural way to write this check — an inline beforeInsert handler doing the Intl.DateTimeFormat probe — behaves like this:

                      • pnpm validate, typecheck, test and build are all green (in-process tests run the raw JS function in Node, where Intl exists);
                      • in production the lowered body throws ReferenceError: Intl is not defined inside the sandbox;
                      • with the onError: 'abort' that a validation-shaped hook must declare, every write to that object is refused.

                      Nothing anywhere in that sequence says the handler moved to a runtime that lacks the global it uses.

                      The only route that keeps the probe in Node is the string handler ref (handler: 'my_fn' + defineStack({ functions })), resolved against the bundle functions map + runtimeModule. That works — but hook.zod.ts marks handler"DEPRECATED, prefer body" and warnLegacyHandler prints "Move the handler source into Hook.body". Following that advice on any host-API-dependent check converts a working guard into a refuse-everything hook. The deprecation direction and the only viable route for this class of check point opposite ways.

                      What would close it

                      In rough order of preference:

                      1. valueDomain on a fieldField.text({ valueDomain: 'iana_time_zone' }), enforced on the write path with the membership definition already written down in settings-manifest.zod.ts. Declarative, translatable refusal message, visible to Studio / OpenAPI / the form layer, and one oracle for settings and fields alike.
                      2. A format validation member alongside email | url | phone | json drawn from the same closed vocabulary.
                      3. Failing both: state in hook.zod.ts that a body cannot reach host intrinsics and that the string handler ref is the supported route for checks that need them, so the deprecation does not read as advice to break them.

                      Independently of (1)-(3), the sandbox-lowering hazard seems worth a build-time diagnostic on its own: an inline handler that references a global the sandbox does not provide is lowerable, buildable, testable and broken only in production.

                      Meanwhile

                      objectstack-ai/duly is using the string handler ref with the Intl.DateTimeFormat probe in Node, sharing one oracle with the period engine that consumes the value, and pointing at this issue in the code.


                      Measurement backing the iana_time_zone note, since it is the crux and it is worth having twice — Node v22.22.2:

                      Intl.supportedValuesOf('timeZone').length -> 418
                      includes 'UTC' -> false
                      includes 'GMT' -> false
                      includes 'Asia/Kolkata' -> false
                      includes 'Europe/Kyiv' -> false
                      includes 'US/Eastern' -> false
                      new Intl.DateTimeFormat('en-US', { timeZone: v }) -> resolves for every one of them
                      

                      A field constrained against the enumerated list would reject UTC — the platform's own declared default.


                      Generated by Claude Code

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          No declarative way to constrain an object FIELD to a value domain (iana_time_zone), and neither extension point an app can reach can express it #14168

                          Description

                          @os-warren

                          Found while adding "this must be a real IANA zone" to an application field (duly_duty.timezone, objectstack-ai/duly#24). Filing per that app's rule "if the platform genuinely cannot express it, file upstream rather than quietly working around it".

                          The gap

                          packages/spec already publishes the vocabulary and, valuably, the definition of membershipSpecifierValueDomainSchema in system/settings-manifest.zod.ts:

                          'iana_time_zone' | 'iso_4217_currency' | 'iso_3166_alpha2'
                          

                          with the note that for iana_time_zone"membership is the Intl.DateTimeFormat probe … NOT Intl.supportedValuesOf('timeZone')". That note is correct and it saved me a bug — see the measurement below.

                          But valueDomain is reachable only from a settings Specifier. An object field has no equivalent. FieldType has no timezone member, and FieldSchema carries maxLength / minLength / min / max but no value-domain slot. So an authored zone field is a bare Field.text and nothing checks it; the value is discovered to be wrong wherever it is finally consumed.

                          Why an application cannot close it at its own layer either

                          Both extension points an app can reach were measured on @objectstack/spec / @objectstack/runtime / @objectstack/objectql17.2.0, Node v22.22.2.

                          1. A script / cross_field validation rule (CEL) cannot. The whole stdlib registered in @objectstack/formula's registerStdLib is:

                          now today daysFromNow daysAgo isBlank coalesce trim joinNonEmpty daysBetween
                          addDays addMonths date datetime abs round floor ceil min max upper lower
                          contains startsWith endsWith matches len isEmpty
                          

                          There is no zone/currency/country oracle, and no app-level way to register one — buildEnv is internal to the package. The only reachable spelling is matches(record.timezone, '<regex>'), which either checks shape only (Europe/Munich passes) or freezes a tzdata snapshot into metadata — precisely what the iana_time_zone note warns is a measurably different set from what the host accepts.

                          2. An L2 hook body cannot: the sandbox has no Intl. Measured directly against the runtime's own sandbox (quickjs-emscripten 0.32.0, newQuickJSWASMModule, the variant AppPlugin wires through QuickJSScriptRunner):

                          typeof Intl -> undefined
                          typeof Date -> function
                          typeof JSON -> object
                          

                          HookBodyCapability is api.read | api.write | api.transaction | crypto.uuid | log — nothing grants Intl, so this is not a capability the author forgot to declare.

                          The part that makes this actively hazardous, not merely missing

                          objectstack buildsilently lowers a self-contained inline handler into an L2 body — confirmed in a real artifact, where a hook authored as handler: <inline fn> ships as:

                          { "handler": "duly_task_lifecycle_stamps",
                          "body": { "language": "js", "source": "", "capabilities": [] } }

                          and resolveHandler in bindHooksToEngine prefers body and ignores handler whenever both are present. So the natural way to write this check — an inline beforeInsert handler doing the Intl.DateTimeFormat probe — behaves like this:

                          • pnpm validate, typecheck, test and build are all green (in-process tests run the raw JS function in Node, where Intl exists);
                          • in production the lowered body throws ReferenceError: Intl is not defined inside the sandbox;
                          • with the onError: 'abort' that a validation-shaped hook must declare, every write to that object is refused.

                          Nothing anywhere in that sequence says the handler moved to a runtime that lacks the global it uses.

                          The only route that keeps the probe in Node is the string handler ref (handler: 'my_fn' + defineStack({ functions })), resolved against the bundle functions map + runtimeModule. That works — but hook.zod.ts marks handler"DEPRECATED, prefer body" and warnLegacyHandler prints "Move the handler source into Hook.body". Following that advice on any host-API-dependent check converts a working guard into a refuse-everything hook. The deprecation direction and the only viable route for this class of check point opposite ways.

                          What would close it

                          In rough order of preference:

                          1. valueDomain on a fieldField.text({ valueDomain: 'iana_time_zone' }), enforced on the write path with the membership definition already written down in settings-manifest.zod.ts. Declarative, translatable refusal message, visible to Studio / OpenAPI / the form layer, and one oracle for settings and fields alike.
                          2. A format validation member alongside email | url | phone | json drawn from the same closed vocabulary.
                          3. Failing both: state in hook.zod.ts that a body cannot reach host intrinsics and that the string handler ref is the supported route for checks that need them, so the deprecation does not read as advice to break them.

                          Independently of (1)-(3), the sandbox-lowering hazard seems worth a build-time diagnostic on its own: an inline handler that references a global the sandbox does not provide is lowerable, buildable, testable and broken only in production.

                          Meanwhile

                          objectstack-ai/duly is using the string handler ref with the Intl.DateTimeFormat probe in Node, sharing one oracle with the period engine that consumes the value, and pointing at this issue in the code.


                          Measurement backing the iana_time_zone note, since it is the crux and it is worth having twice — Node v22.22.2:

                          Intl.supportedValuesOf('timeZone').length -> 418
                          includes 'UTC' -> false
                          includes 'GMT' -> false
                          includes 'Asia/Kolkata' -> false
                          includes 'Europe/Kyiv' -> false
                          includes 'US/Eastern' -> false
                          new Intl.DateTimeFormat('en-US', { timeZone: v }) -> resolves for every one of them
                          

                          A field constrained against the enumerated list would reject UTC — the platform's own declared default.


                          Generated by Claude Code

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              No declarative way to constrain an object FIELD to a value domain (iana_time_zone), and neither extension point an app can reach can express it #14168

                              Description

                              @os-warren

                              Found while adding "this must be a real IANA zone" to an application field (duly_duty.timezone, objectstack-ai/duly#24). Filing per that app's rule "if the platform genuinely cannot express it, file upstream rather than quietly working around it".

                              The gap

                              packages/spec already publishes the vocabulary and, valuably, the definition of membershipSpecifierValueDomainSchema in system/settings-manifest.zod.ts:

                              'iana_time_zone' | 'iso_4217_currency' | 'iso_3166_alpha2'
                              

                              with the note that for iana_time_zone"membership is the Intl.DateTimeFormat probe … NOT Intl.supportedValuesOf('timeZone')". That note is correct and it saved me a bug — see the measurement below.

                              But valueDomain is reachable only from a settings Specifier. An object field has no equivalent. FieldType has no timezone member, and FieldSchema carries maxLength / minLength / min / max but no value-domain slot. So an authored zone field is a bare Field.text and nothing checks it; the value is discovered to be wrong wherever it is finally consumed.

                              Why an application cannot close it at its own layer either

                              Both extension points an app can reach were measured on @objectstack/spec / @objectstack/runtime / @objectstack/objectql17.2.0, Node v22.22.2.

                              1. A script / cross_field validation rule (CEL) cannot. The whole stdlib registered in @objectstack/formula's registerStdLib is:

                              now today daysFromNow daysAgo isBlank coalesce trim joinNonEmpty daysBetween
                              addDays addMonths date datetime abs round floor ceil min max upper lower
                              contains startsWith endsWith matches len isEmpty
                              

                              There is no zone/currency/country oracle, and no app-level way to register one — buildEnv is internal to the package. The only reachable spelling is matches(record.timezone, '<regex>'), which either checks shape only (Europe/Munich passes) or freezes a tzdata snapshot into metadata — precisely what the iana_time_zone note warns is a measurably different set from what the host accepts.

                              2. An L2 hook body cannot: the sandbox has no Intl. Measured directly against the runtime's own sandbox (quickjs-emscripten 0.32.0, newQuickJSWASMModule, the variant AppPlugin wires through QuickJSScriptRunner):

                              typeof Intl -> undefined
                              typeof Date -> function
                              typeof JSON -> object
                              

                              HookBodyCapability is api.read | api.write | api.transaction | crypto.uuid | log — nothing grants Intl, so this is not a capability the author forgot to declare.

                              The part that makes this actively hazardous, not merely missing

                              objectstack buildsilently lowers a self-contained inline handler into an L2 body — confirmed in a real artifact, where a hook authored as handler: <inline fn> ships as:

                              { "handler": "duly_task_lifecycle_stamps",
                              "body": { "language": "js", "source": "", "capabilities": [] } }

                              and resolveHandler in bindHooksToEngine prefers body and ignores handler whenever both are present. So the natural way to write this check — an inline beforeInsert handler doing the Intl.DateTimeFormat probe — behaves like this:

                              • pnpm validate, typecheck, test and build are all green (in-process tests run the raw JS function in Node, where Intl exists);
                              • in production the lowered body throws ReferenceError: Intl is not defined inside the sandbox;
                              • with the onError: 'abort' that a validation-shaped hook must declare, every write to that object is refused.

                              Nothing anywhere in that sequence says the handler moved to a runtime that lacks the global it uses.

                              The only route that keeps the probe in Node is the string handler ref (handler: 'my_fn' + defineStack({ functions })), resolved against the bundle functions map + runtimeModule. That works — but hook.zod.ts marks handler"DEPRECATED, prefer body" and warnLegacyHandler prints "Move the handler source into Hook.body". Following that advice on any host-API-dependent check converts a working guard into a refuse-everything hook. The deprecation direction and the only viable route for this class of check point opposite ways.

                              What would close it

                              In rough order of preference:

                              1. valueDomain on a fieldField.text({ valueDomain: 'iana_time_zone' }), enforced on the write path with the membership definition already written down in settings-manifest.zod.ts. Declarative, translatable refusal message, visible to Studio / OpenAPI / the form layer, and one oracle for settings and fields alike.
                              2. A format validation member alongside email | url | phone | json drawn from the same closed vocabulary.
                              3. Failing both: state in hook.zod.ts that a body cannot reach host intrinsics and that the string handler ref is the supported route for checks that need them, so the deprecation does not read as advice to break them.

                              Independently of (1)-(3), the sandbox-lowering hazard seems worth a build-time diagnostic on its own: an inline handler that references a global the sandbox does not provide is lowerable, buildable, testable and broken only in production.

                              Meanwhile

                              objectstack-ai/duly is using the string handler ref with the Intl.DateTimeFormat probe in Node, sharing one oracle with the period engine that consumes the value, and pointing at this issue in the code.


                              Measurement backing the iana_time_zone note, since it is the crux and it is worth having twice — Node v22.22.2:

                              Intl.supportedValuesOf('timeZone').length -> 418
                              includes 'UTC' -> false
                              includes 'GMT' -> false
                              includes 'Asia/Kolkata' -> false
                              includes 'Europe/Kyiv' -> false
                              includes 'US/Eastern' -> false
                              new Intl.DateTimeFormat('en-US', { timeZone: v }) -> resolves for every one of them
                              

                              A field constrained against the enumerated list would reject UTC — the platform's own declared default.


                              Generated by Claude Code

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions