Repository navigation
fix(service-sms): a configured SMS provider delivers through its env or Settings credentials, without OS_SMS_PROVIDER - #22815
Conversation
… its settings credentials resolveSmsCapabilityArg merges the selected provider's credentials into providerOptions, env over config.sms.providerOptions per key, from the OS_SMS_* variables the sms settings namespace is env-locked with. applySmsSettings honours the constructor's provider when the namespace names none; a provider the namespace names still wins. One table, SMS_PROVIDER_SETTING_KEYS, maps setting keys to transport options for both faces. Claude-Session: https://claude.ai/code/session_01CBAfsWMSfM3EToQGVStEcp Co-authored-by: Claude <noreply@anthropic.com>
… and add the changeset capability-arg.config-parity.contract.test.ts holds SMS_PROVIDER_SETTING_KEYS equal to the sms manifest's per-provider keys, the resolver's env reads equal to SettingsService's envKeyOf names, and one environment equal to one transport through either face. sms-plugin.test.ts pins the constructor provider at kernel:ready, with the Settings-named-provider and missing-credentials controls. Claude-Session: https://claude.ai/code/session_01CBAfsWMSfM3EToQGVStEcp Co-authored-by: Claude <noreply@anthropic.com>
…s of the change It asserts the log transport, isConfigured() false and a warn-level line naming the provider, wherever in the boot it was logged, so it reads the same before and after the kernel:ready leg. Claude-Session: https://claude.ai/code/session_01CBAfsWMSfM3EToQGVStEcp Co-authored-by: Claude <noreply@anthropic.com>
…s-configured-provider
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 3 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 f647c851a947bb42fc4ee68abe98e982c4024e23 && git checkout f647c851a947bb42fc4ee68abe98e982c4024e23
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 23419bafb742fc13fa3c22b446263bbd6041c78e a59a982f8bd47b3bddb7822d6db2be9641dfb972 && git checkout -B drift-repro 23419bafb742fc13fa3c22b446263bbd6041c78e && git merge --no-ff a59a982f8bd47b3bddb7822d6db2be9641dfb972
node scripts/docs-audit/affected-docs.mjs --json 23419bafb742fc13fa3c22b446263bbd6041c78e
|
Fixes #22789
Clause-②: no
A configuration that names an SMS provider (
provider: 'twilio'or'aliyun') and keeps that provider's keys in the environment, but sets noOS_SMS_PROVIDER, now delivers. Before this change it booted onLogSmsTransportwithisConfigured() === false, throughos serveandbootStackalike. The change follows triage6106456354: mirror the mail resolver, not a second rule. Dispatch: seatdomain:services#1, claim6107123356, sessionsession_01CBAfsWMSfM3EToQGVStEcp.What changed (
packages/services/service-smsonly)resolveSmsCapabilityArg(src/capability-arg.ts) merges the selected provider's credentials intoproviderOptions, env overconfig.sms.providerOptions, key by key. That is the mail resolver's rule (OS_EMAIL_SMTP_*overconfig.email.options). It reads the variablesSettingsServiceenv-locks thesmsnamespace with (envKeyOf('sms', key)):OS_SMS_TWILIO_ACCOUNT_SIDtoaccountSid,OS_SMS_TWILIO_AUTH_TOKENtoauthToken,OS_SMS_TWILIO_FROM_NUMBERtofrom,OS_SMS_TWILIO_MESSAGING_SERVICE_SIDtomessagingServiceSid;OS_SMS_ALIYUN_ACCESS_KEY_IDtoaccessKeyId,OS_SMS_ALIYUN_ACCESS_KEY_SECRETtoaccessKeySecret,OS_SMS_ALIYUN_SIGN_NAMEtosignName,OS_SMS_ALIYUN_TEMPLATE_CODEtodefaultTemplateCode.applySmsSettings(src/sms-plugin.ts, a private method ofSmsServicePlugin) honours the constructor'sproviderwhen the namespace names none. "Names none" isprovideratsource: 'default', the criterion thelogbranch already used.providerOptionskey by key, so a constructor-only option such asbaseUrlsurvives the rebuild.OS_SMS_PROVIDERenv lock) still wins.SMS_PROVIDER_SETTING_KEYS(src/transports/index.ts), maps each setting key to its transport option.providerFromSettingsand the resolver both read it, so the mapping is written once. It is not exported from the package root, so the published surface is unchanged.@objectstack/service-smspatch changeset.Not in this PR: admitting
sms.provideron the stack definition (packages/spec). That is the follow-on of #22748, which the seat tells.plugin-emailis untouched too.Measured through
bootStack, before and afterA scratch test (not committed) in
packages/verifybootedbootStack({ ...defineStack(...), sms: ... })with the environment below and read thesmsservice.origin/main5fc57b382, with the verify dependency closure built from that tree.@objectstack/service-smsrebuilt at429b114ef. Its source has not changed since.sms.provider: 'twilio', Twilio keys in env, noOS_SMS_PROVIDERisConfiguredfalse,LogSmsTransportTwilioSmsTransportOS_SMS_PROVIDER=twilio(control)TwilioSmsTransportTwilioSmsTransportsms.provider: 'aliyun', Aliyun keys in env, noOS_SMS_PROVIDERLogSmsTransportAliyunSmsTransportsms.provider: 'twilio', no credentials anywhere (control)LogSmsTransport, WARN at initLogSmsTransport, WARN at init and atkernel:readysms.provider: 'twilio'and Twilio keys, plusOS_SMS_PROVIDER=aliyunand Aliyun keys (control)AliyunSmsTransportAliyunSmsTransportCase D is not a refusal with a
code: today a configured provider with no credentials boots onto the log transport, with a warn-level line andisConfigured() === false. The controls assert exactly that kind: the log transport,isConfigured()false, and a warn line naming the provider.Tests
capability-arg.config-parity.contract.test.ts(new; the shape of the mail file of the same name). It holds three things, usingSettingsService,envKeyOfandsmsSettingsManifestfrom@objectstack/service-settings(an existing devDependency):SMS_PROVIDER_SETTING_KEYSmaps exactly the keys thesmsmanifest declares per provider (grouped by each specifier's ownvisibleexpression), with the measured key set pinned.envKeyOfname, env over config per credential, for the selected provider only. A configured provider with no credentials is still not refused (control).OS_SMS_PROVIDER, a settings service mounted) isisConfigured() === truefrom init on, and the constructor face and the settings face build equal credentials.sms-plugin.test.ts, a new block:baseUrlsurvives the overlay;isConfigured()false and a warn line naming the provider.a59a982f8:pnpm --filter @objectstack/service-sms testgave 6 files and 94 tests passed.pnpm --filter @objectstack/service-sms typecheckexited 0. The test program (tsconfig.test.json,--listFiles) holds 6 of the 6 test files.Ablation (one-shot, at
1718ed1aa)Each leg went through
scripts/ablation-replace.mjs(WRAP mode). The anchor hit once and was proven on disk by its count and blob change. The restore was proven by blob hash equal to HEAD with an emptygit diff HEAD. The subject is imported by relative path from source, so nodist/is involved.: smsProviderOptionsFrom(provider,became: {} || smsProviderOptionsFrom(provider,. Result: 8 failed, 28 passed. Red: the four resolver reads, the two "delivers, from init on" pins, and the two "both faces build the same credentials" pins. Green, as designed: the Settings-named-provider control, both missing-credentials controls, and the resolver's not-refused control.kernel:ready.const configuredProvider = !named &&becameconst configuredProvider = false && !named &&. Result: 3 failed, 33 passed. Red: the twilio and aliyun namespace-credential pins and thebaseUrloverlay pin. Green: every control.The bootStack door is repaired by either leg alone at
kernel:readywhen a settings service is mounted. Only the init-time reading measures the resolver leg, which is why the "from init on" pins readisConfigured()at init.Gates (at
a59a982f8, after mergingorigin/main098481744)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 63 commands. 62 exited 0. One is NOT MEASURED:pnpm check:dual-build-cjs-loadsexited 3, PREREQUISITE NOT MET, because it reads every package'sdist/and this worktree built only the verify closure. As a supplementary reading, not the gate:requireof@objectstack/service-sms's CJSdist/index.jsloads and answers the resolver.--ranreconciliation: "63 derived famil(ies) accounted for — 62 run, 1 NOT-MEASURED", 0 UNRUN.--format jsonreports 5;--print-configonsrc/sms-plugin.tsshows noparserOptions.projectand noprojectService, so type-aware linting is off;Acceptance notes
packages/spec/src/system/sms-config.zod.tslanded with feat(spec): the stack definition admitsappNameand the non-secretemail/smsmembers; their credentials are refused by name #22778 (85edd61cd) after this branch was cut. Its header says the SMS reader merges no environment credential intoproviderOptions, and thatprovidercomes back "in the change that makesSmsServicePluginhonour it". The first sentence is false after this PR, and this PR is that change. Carrier: the spec: the stack definition has noemail,smsorappNamekey, so theconfig.email/config.smscontractservereads is unreachable from adefineStackconfig #22748 follow-on that lifts thesms.providerrefusal. It is not edited here, because this dispatch has nopackages/spec.mainmoved by two commits after the merge above (85edd61cd,7b0a9c93c). Neither touchesservice-sms. The CLI contract test feat(spec): the stack definition admitsappNameand the non-secretemail/smsmembers; their credentials are refused by name #22778 added,serve-stack-email-sms-door.contract.test.ts, asserts resolver outputs this change leaves equal. Read through the builtdist:{ provider: 'log', retries: 3 },{ provider: 'twilio', retries: 3 }and{ provider: 'aliyun' }.kernel:readywarning is the existing line. It says "selected" even when the provider came from the constructor. Its prose is not pinned.service-sms, and no cross-package module was created.Generated by Claude Code