Repository navigation
fix(spec): resolveBundleLocale and pickData answer only a bundle's own locale keys - #30678
Conversation
…n locale keys A requested code naming an Object.prototype member (constructor, toString, __proto__) was found on every plain-object bundle: resolveBundleLocale's exact fast path answered it as a registered locale and pickData's exact read handed back the inherited function as bundle data. Both reads now use an own-property check, so the code takes the same fallback an unknown code takes; an own key of that name still answers. Claude-Session: https://claude.ai/code/session_01L6bhgTqqeHSwZYJmApbDCa Co-authored-by: Claude <[email protected]>
…constructor write Claude-Session: https://claude.ai/code/session_01L6bhgTqqeHSwZYJmApbDCa Co-authored-by: Claude <[email protected]>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 170 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 44dd999eddf9bf57c4142f93641b040160c3be33 && git checkout 44dd999eddf9bf57c4142f93641b040160c3be33
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 788715ef5cb205d3e70052d25a595e13a8aa9d7c fdba0dbd7563d1da25a725806ed887270211b89f && git checkout -B drift-repro 788715ef5cb205d3e70052d25a595e13a8aa9d7c && git merge --no-ff fdba0dbd7563d1da25a725806ed887270211b89f
node scripts/docs-audit/affected-docs.mjs --json 788715ef5cb205d3e70052d25a595e13a8aa9d7c
|
|
domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa Contract-review-claim: session_01L6bhgTqqeHSwZYJmApbDCa · head a160083 · 2026-11-05T22:10Z Full review at Generated by Claude Code |
|
domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa Contract reviewServed-tier: Inputs read: card #30619 (body, triage 6666526394, claim 6669698230, dev report 6670828385) and its origin, the contract review of PR #30601 (comment 6665489168, S6); PR #30678 body, file list (3 files, +60/−3) and the net diff of merge base Gate verdicts, read at 2026-11-05T22:37Z after every run had completed (the last at 2026-11-05T22:36Z): 64 check-runs on ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentJudged on the tree at
Must-fix: none. Should-fix:
Implemented-by: VERDICT: PASS Generated by Claude Code |
…through translateObject builtinSystemFieldLabel returned entry[resolved] straight, so before the own-key fast path a constructor or __proto__ locale served an inherited member as an injected system field's label. The pin reads created_at through translateObject under both names and expects what 'xx' answers. The changeset now names that user-visible answer. Claude-Session: https://claude.ai/code/session_01L6bhgTqqeHSwZYJmApbDCa Co-authored-by: Claude <[email protected]>
|
domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa Contract reviewServed-tier: Delta review: the head moved from Gate verdicts, read at 2026-11-05T23:28Z in one reading taken after every run had completed (the last at 2026-11-05T23:26Z): 68 check-runs on ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Must-fix: none. Should-fix: none. S1 of 6671371228 is closed by this head: the census is corrected, the impact sentence is corrected, the changeset names the label, and the pin is in the tree. Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa 维护者速读
席位意见:契约复审两轮都 PASS。第一轮唯一的 should-fix 指出,调用点清单漏了内部的 席位核对 — ACCEPT · domain:spec#3 · head
|
domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa
Requested by Sales · project thread
Fixes #30619
Clause-②: no
Metadata-protocol: none
维护者速读
resolveBundleLocale的精确快路径与pickData的精确读取改为只认翻译包自己的键(own property),constructor/toString/__proto__这类原型成员名不再被当成已注册语言。?locale=)原样进入解析器,原型成员名会命中任意普通对象翻译包,解析器把它当语言返回;用户可见的一处是/meta对象读取时,平台注入的系统字段(如「Created At」)标签丢失。i18n-resolver.ts这两处。packages/spec/src/system/i18n-resolver.ts的resolveBundleLocale快路径与pickData首行;其次i18n-resolver.test.ts末尾新增的 describe 块。pnpm --filter @objectstack/spec exec vitest run src/system/i18n-resolver.test.ts,预期 501 passed,其中包括末尾 own-key 新块的 9 个用例。What changes
packages/spec/src/system/i18n-resolver.ts, two reads, both nowObject.prototype.hasOwnProperty.call(the spec package'stsconfig.jsontargetslib: ES2020, which has noObject.hasOwn; the file already uses this form at its other two own-key reads):resolveBundleLocale, step 1 (the exact fast path):bundle[requested] !== undefinedbecomes an own-key check plus the same!== undefinedtest. An own key whose value isundefinedstill does not answer this step, as before.pickData, the exact read:bundle[locale]is read only whenlocaleis an own key; otherwise the read goes toresolveBundleLocaleas an unknown code does.The rest of
resolveBundleLocalewas read and needs no change: steps 2 to 5 (case-insensitive, platform table language, base language, variant expansion) run inresolveLocaleAmongoverObject.keys(bundle), which lists own keys only, andpickData's last read,bundle[resolved], is keyed by a code that one of those own-key steps returned. No other read inpickDataindexes the bundle by a requested code. Reads keyed by metadata names (objects?.[objectName]and the like) are out of scope and untouched.No signature, export or schema change, and no edit outside
packages/spec.Callers
resolveBundleLocalehas seven non-test callers outsidepackages/speconorigin/main(the assumption holds, line numbers too), plus four insidepackages/spec:pickData(the translators' bundle read),builtinSystemFieldLabel,validation-message.tsandoperation-message.ts. Traced read-only:requestedcomes frompackages/objectql/src/engine-validation-messages.ts:204(negotiatedMessageLocale)context.locale, which isExecutionContext.locale: the request'srequestLocale ?? localization.locale(packages/core/src/security/assemble-execution-context.ts:394). The dispatcher's request leg ispreferredLocaleFromHeader(accept-language)(packages/runtime/src/security/resolve-execution-context.ts:271); the REST leg also takes?locale=.?locale=string arrives as is.packages/plugins/plugin-approvals/src/reader-locale-labels.ts:128readerLabelsFor(service, context?.locale)(approval-service.ts:9622,reader-locale-labels-middleware.ts:127):ExecutionContext.locale.packages/plugins/plugin-audit/src/activity-summary-message.ts:278readerSummaryLocale(getI18n(), ctx.context?.locale, …)(:387):ExecutionContext.locale.packages/plugins/plugin-email/src/email-service.ts:1512input.localeofsendTemplate/renderTemplate, then the stack's declared fallback. The only callers in the tree areplugin-auth's mails, throughemailLocaleArg: the storedsys_user.locale(shape-checked bynormalizeRecipientLocale,^[A-Za-z]{2,3}(-[A-Za-z0-9]{1,8})*$), else the request'sAccept-Languagetag only when it is a shipped auth-mail locale (authEmailLocaleFromRequest), else the deployment's configured locale.Object.prototypemember name passes the tag shape or the shipped-locale list.packages/runtime/src/sandbox/hook-refusal-catalogue.ts:113engineCtx.session.locale(:181): the request's execution-context locale.packages/services/service-automation/src/engine.ts:1462(negotiatedServiceLocale)AutomationContext.locale, the door'sExecutionContext.locale(its own docblock: "whose header leg is the request's tag verbatim"), viatranslatedFlowTemplateinnode-text-rendering.ts:197.packages/services/service-messaging/src/notification-text.ts:353recipientLocale():resolveRecipientLocale(sys_user.locale, deployment default); the publicrender(keys, literal, locale)takes a caller's locale, and no non-test caller in the tree calls it.normalizeRecipientLocale's tag shape.packages/spec/src/system/i18n-resolver.tspickData(insidepackages/spec)extractLocale/metaRequestLocale: theAccept-Languagetop tag or a raw?locale=), then the declared fallback chain.packages/spec/src/system/i18n-resolver.tsbuiltinSystemFieldLabel(insidepackages/spec, called fromtranslateObject's field pass)packages/spec/src/system/validation-message.ts:399,operation-message.ts:332(insidepackages/spec)localeoption their callers pass, the sameExecutionContext.localein the engine paths.So five of the seven outside callers, and all four inside ones, pass an unvalidated request value. What it measurably changed before this PR:
resolveBundleLocale(offered, requested) ?? requested, so a prototype-name request came back as itself both before and after the fix, and the answer the caller handst()is the same.BUILTIN_*_MESSAGES[matched][messageKey]. No built-in message key is aFunctionorObject.prototypemember name, so they fell back to the default locale's template before the fix too.pickData's inherited answer was never read through: every reader's first hop is a fixed group (objects,apps,settings, …) that none of those members carries.builtinSystemFieldLabelreturnsentry[resolved]directly, andtranslateObject's field pass assigns any truthy answer tonext.label. Before this PR an object read (/meta) under?locale=constructororAccept-Language: constructorserved theObjectconstructor as the label of every injected system field still at its English default; JSON serialisation dropped it, so the label went missing. Under__proto__it servedObject.prototype(an object where a string belongs). Now the ladder answersundefinedfor those codes, the chain moves on, and the English default stands.Tests
New block at the end of
packages/spec/src/system/i18n-resolver.test.ts(bundle locale keys are the bundle's OWN keys …), at the end of the file so #30618's cases cannot conflict with it:constructor,toStringand__proto__:resolveBundleLocale({ en: {} }, code)equalsresolveBundleLocale({ en: {} }, 'xx')(bothundefined), and the same over{ en, zh-CN };resolveViewLabel(which reads throughpickData;pickDataitself is not exported) answers exactly what it answers for'xx': the literal label, or the next locale of the fallback chain;zh-CN(Object.create(bundle)) answerszh-CNthe way it answers an unknown code, inresolveBundleLocaleand throughresolveViewLabel;created_at, labelCreated At) read through the exportedtranslateObjectwith{ locale: 'constructor' }and{ locale: '__proto__' }keeps a string label equal to what'xx'gives (Created At). This pins the served-document path above;constructorand__proto__key (built byJSON.parse) and an ownconstructoron anObject.create(null)bundle still answer, andresolveViewLabelreads the ownconstructordata. This shows the fix is an own-property check, not a deny-list.Readings (head fdba0db, round 1)
pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/system/i18n-resolver.test.ts:Tests 501 passed (501), lockVERDICT command-exit 0.pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec typecheck && pnpm --filter @objectstack/spec test:Test Files 817 passed (817),Tests 25783 passed | 1 todo (25784),VERDICT command-exit 0.pnpm --filter @objectstack/spec check:generated: exit 0,✓ All 14 generated artifacts are up to date, includingcheck:test-typecheckgreen with the new pin. In round 0 an earlier run caught one new TS2740 in the first version of the test (aconstructorassignment typed asFunction); the fixture was rewritten, and the ledger was not touched.eslint --no-inline-configon both changed.tsfiles: exit 0, no output.node scripts/check-changeset-no-major.mjs --base b3d2f4bdb --event EVENT(apull_requestpayload carrying this body's two declaration lines): exit 0,✓ LEVEL AXIS: this PR declares clause-② no and metadata-protocol none … a non-protocol change at patch.Ablation (via
scripts/ablation-replace.mjs, which takes a literal anchor, checks that it hit, and restores; the spec tests import./i18n-resolverfrom source, so nodistleg applies):if (bundle[requested] !== undefined) {. Round 1, on fdba0db: anchor x1 → x0, blob405d8b76→d21038ff.Tests 5 failed | 496 passed: the three prototype-nameresolveBundleLocalepins, the inherited-key pin, and the newtranslateObjectsystem-field pin. Restored: blob after restore405d8b76equals the HEAD blob, andgit diff HEADis empty. In round 0, on aa1ad0b, the same leg gave 4 failed, before the system-field pin existed.pickData's read reverted toconst exact = bundle[locale];(round 0, on aa1ad0b): anchor x1 → x0, blob405d8b76→eb268592.Tests 1 failed | 499 passed: the inherited-key pin. Restored the same way, blob equals HEAD.i18n-resolver.tshas not changed since, so its blob is still405d8b76.Observed direction for leg B: the
constructor/toString/__proto__reader pins stay green withpickData's read reverted. No exported reader reads a property thatObject,Function.prototype.toStringorObject.prototypecarries (every reader's first hop is a fixed group such asobjects,appsorsettings), so the functionpickDatareturned was never visible through them. The inherited-key pin is the one that shows thepickDataread on its own.Acceptance notes
b3d2f4bdb.origin/mainhas moved 6 commits since, none ini18n-resolver.ts, its test orplatform-locales.ts, so no merge was taken. If platform locale table: script-subtag codes (zh-Hant-TW, zh-Hans-CN, zh-Hant-HK) are not read aliases, so a two-script bundle answers them with the first registered zh-* key #30618 lands first,mainis merged in (no rebase).pickDatareturnsundefined" is pinned throughresolveViewLabel, becausepickDatais not exported and the ruling forbids an export change. See the leg-B note above for what that pin can and cannot see.resolveBundleLocale({ en: {}, fr: undefined }, 'fr')still answers'fr'. The fast path skips theundefinedvalue as before, but step 1 ofresolveLocaleAmongfindsfrinObject.keys. This is unchanged by this PR and is not pinned here.ExecutionContext.localeis a separate question this PR does not take up.builtinSystemFieldLabel, and with it the one user-visible wrong answer: a missing system-field label under a prototype-name locale. That answer is fixed by the same fast-path change. Round 1 adds thetranslateObjectpin for it, corrects the census and the impact sentence here, and names the label in the changeset. No source change:i18n-resolver.tsis byte-identical to round 0.origin/mainmoved no commit oni18n-resolver.ts, its test orplatform-locales.ts, so no merge was taken.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01L6bhgTqqeHSwZYJmApbDCa