Skip to content

Releases: objectstack-ai/objectstack

create-objectstack@17.1.0

Choose a tag to compare

@github-actionsgithub-actions released this 20 Aug 11:21
2d3860d

Minor Changes

  • 1eb28a1: Retire the five remote content templates from the scaffolder's catalog.

    todo, compliance, content, contracts and procurement were delisted
    from the official ObjectStack template marketplace and are no longer
    maintained, but the CLI carried its own hardcoded catalog and never learned
    that: --help recommended all five by name with marketing descriptions, and
    the Available: line on a bad -t offered them too.

    • blank (bundled, offline) is now the whole catalog, so the help text
      advertises only what is actually supported.
    • Asking for one of the five by name — -t todo in an old script or tutorial —
      is refused with a message that says the template was retired, instead of the
      generic "Unknown template" error that reads as a typo.
    • The GitHub tarball-fetch path that served the remote templates is removed
      along with its tar dependency; nothing else reached it.

    Note this corrects the catalog at HEAD only. Already-published versions keep
    advertising the retired templates until a new version of create-objectstack
    is released.

Patch Changes

  • 4906c90: Fix scaffolded projects describing themselves as the blank template (#9263)

    rewriteProjectIdentity rewrote id / namespace / name in both
    objectstack.config.ts and objectstack.manifest.json from the project name,
    but left description untouched — every scaffolded project carried the blank
    template's own line verbatim ("Minimal ObjectStack environment — a clean
    slate for building."), confidently wrong rather than empty, and printed by
    the first command the getting-started flow tells people to run (os validate).

    The scaffolder now drops description from both files instead of rewriting
    it. There is nothing but the project name to derive a replacement from, and a
    name-derived sentence (e.g. "Support Desk — an ObjectStack environment.")
    would be a bare restatement of the name/displayName row already shown —
    worse than no sentence at all. os validate already omits the description
    line entirely when the field is unset, so a freshly scaffolded project now
    prints cleanly:

     Support Desk v0.1.0
    

    instead of

     Support Desk v0.1.0
    Minimal ObjectStack environment — a clean slate for building.
    
  • f2f09e4: fix(create-objectstack): the scaffolded Dockerfile pins the runtime image to the CLI that builds the artifact, instead of latest under a comment saying to pin (#9017)

    src/templates/blank/Dockerfile shipped FROM ghcr.io/objectstack-ai/objectstack:latest
    directly beneath a comment instructing the reader to "pin the tag to the
    @objectstack/cli version in your package.json so the runtime matches the CLI that built
    the artifact" — an instruction the scaffold itself did not follow. Every app made with
    npx create-objectstack shipped that contradiction from day one, and docker/README.md's
    tag table already scopes latest to quick starts while documenting X.Y.Z as the
    production pin.

    Measured on scaffolded output rather than the template's bytes, before the fix:

    emitted package.json cli range : ^17.0.0
    emitted Dockerfile FROM : FROM ghcr.io/objectstack-ai/objectstack:latest
    agreement (tag vs cli range) : DISAGREE
    

    The tag is resolved after install, from the installed CLI — not from the generated
    package.json.
    That file carries a caret RANGE, and the two are not interchangeable:
    npm resolves ^17.0.0 to the newest 17.x, so pinning the range's floor would ship a
    runtime image older than the CLI that built the artifact — breaking the same promise in
    a new way. The rolling :17 tag does match the range's float window but is exactly what
    the tag table tells production not to use. The resolved version is the only value that
    makes the sentence true, and it is the rule the repo already applies for this purpose in
    .github/workflows/scaffold-e2e.yml ("Pin the runtime's CLI to the SAME version the
    generated project actually resolved to — NOT a hardcoded latest").

    Both halves move together. Pinning the line while leaving an imperative to pin by hand
    would relocate the contradiction rather than remove it, so the comment above the FROM
    line is replaced in the same rewrite. With --skip-install there is no resolved version:
    the tag stays latest and the comment keeps telling the reader to pin — which is true on
    that path, because there the user really must do it by hand.

    The regression proof asserts on scaffolded output, never on the template: it scaffolds
    with the real copy/sync/pin path, plants an installed CLI whose version is deliberately
    not the range's floor (the normal case, and the one that a package.json-derived tag
    would get wrong), and checks the emitted FROM tag against the emitted package.json
    range with a satisfies-check rather than equality.

    .github/workflows/scaffold-e2e.yml now reads the tag it builds its local runtime image
    under out of the generated Dockerfile instead of hardcoding :latest. Those were two
    hand-matched literals; had they skewed, Docker would have quietly pulled the last
    published image instead of the one built from this checkout, and the job's own stated
    hermeticity would have been false while it stayed green.

  • 0a5adba: fix(create-objectstack): the blank template's specVersion stops shipping eleven majors stale, and the version-time sync covers every declared surface on every template (#9264)

    The one bundled template declared the platform it targets in two places that
    disagreed by eleven majors:

    filekeywas
    objectstack.manifest.jsonspecVersion^6.0.0
    objectstack.config.tsengines.protocol^17

    scripts/sync-template-versions.mjs re-stamped the config key and the template's
    @objectstack/* dependency ranges, and never opened the manifest at all. So
    engines.protocol tracked every major bump while specVersion sat at the value
    it held when the script was written — and a green sync-template-versions run
    was never evidence about it, because the script's failure mode was loud for the
    keys it covered and mute for the key it did not.

    This is not confined to the registry contract.create-objectstack copies
    the manifest into every scaffolded project, rewriting name, displayName and
    namespace and dropping description — it has never touched specVersion. So
    every project scaffolded since v7 was stamped with a ^6.0.0 spec range while
    installing @objectstack/spec@^17.0.0.

    The two keys are two facts, and the fix keeps them apart.engines.protocol
    is the ADR-0087 D1 runtime handshake range and carries the protocol major
    (^17). specVersion is documented by TemplateManifestSchema as the
    "Compatible @objectstack/spec semver range" and carries the package range
    (^17.0.0) — the same value the script already writes into the template's own
    @objectstack/spec dependency, so the manifest and the package.json now state
    one fact once. They agree on the major only because the spec package's major and
    the protocol major are kept in lockstep; they are stamped from two different
    values.

    Deleting the key was not available: specVersion is required by
    TemplateManifestSchema, and every shipped manifest is parsed against it by
    check:template-manifests.

    Two structural changes, because one-key-one-file coverage is what let this
    sit:

    • the sync script's file list is now discovered, not hard-coded — templates
      are found by walking src/templates/, the same way check-template-manifests
      finds the manifests it parses, so a second template is covered on the day it
      lands;
    • every stamp is required. A template whose file is missing, whose stamp is
      absent, or whose package.json declares no @objectstack/* dependency is a
      hard failure naming the path — never a skip. A skipped stamp is
      indistinguishable from a synced one in the log, which is the invisibility this
      fixes.

    The manifest is rewritten as text rather than parsed and re-serialized:
    objectstack.manifest.json keeps scaffold.variables compact on one line, and
    JSON.stringify(…, null, 2) would reformat unrelated structure on every release.

    CI coverage lands as four per-template ratchets in template-consistency.test.ts,
    generalized off blank onto the same directory walk — including the invariant
    that catches this exact class: the manifest's specVersion must equal the
    @objectstack/spec range the template actually installs. Either file alone can
    be self-consistently stale; only comparing them catches a stamp that covered one
    and not the other.

@objectstack/verify@17.1.0

Choose a tag to compare

@github-actionsgithub-actions released this 20 Aug 11:21
2d3860d

Patch Changes

@objectstack/types@17.1.0

Choose a tag to compare

@github-actionsgithub-actions released this 20 Aug 11:21
2d3860d

Minor Changes

  • 2f65b1b: error.code is a closed vocabulary at every door (#9106, maintainer ruling
    2026-08-16): the runtime dispatcher's thrown-error exits
    (HttpDispatcher.errorFromThrown, dispatcher-plugin's errorResponseBase,
    endpoint-executor's endpointErrorAnswer — the actions door among them) now
    serve the narrowed code the shared resolver (resolveThrownHttpError,
    @objectstack/types) has always computed, exactly as the REST door has since
    #8016. A thrown code that is not a member of StandardErrorCode ∪ ERROR_CODE_LEDGER no longer reaches error.code.

    It is not dropped: ApiErrorSchema declares a new optional declaredCode
    field — the open, author-authored channel — and the demoted spelling rides
    there. Presence means demotion: the field is absent whenever the producer's
    code is a vocabulary member (it is already in error.code) or the producer
    declared none. The #7867 sandbox passthrough capability is preserved — a
    metadata app's own thrown .code still crosses the QuickJS boundary and still
    reaches the wire.

    For a metadata app that throws its own code (e.g.
    Object.assign(new Error('pick another'), { code: 'DUPLICATE' }) in an action
    body) and reads it back from an actions-door failure:

    • FROM: error.code === 'DUPLICATE'
    • TO: error.code is the closed member the status derives (e.g.
      VALIDATION_ERROR on a 400) and error.declaredCode === 'DUPLICATE'.
      One-line fix: branch on error.declaredCode for app-specific spellings;
      branch on error.code for platform conditions.

    Platform producers are unaffected: every registered code reaches error.code
    verbatim, as before (post-#8846 the dispatcher-vocabulary gate holds that set
    registered). Measured before landing (the ruling's binding precondition): no
    existing consumer of the actions door branches on author-authored strings in
    error.code.

    @objectstack/types adds demotedDeclaredCode(thrown) — the one definition of
    "which spelling a boundary surfaces beside the closed code".

  • 79c46da: feat(contract): a hook refusal can mark its message user-facing — userMessage, the producer-side opt-in channel (#9934, producer half of objectui#5210)

    The console form deliberately discards the server message on 403 and
    substitutes a generic string — the recorded #3821 fix for platform diagnostics
    leaking to end users. That substitution also suppressed every deliberate,
    localized refusal an application hook author wrote (11 real hook guards in the
    objectui#5210 report), and incentivized misusing 400 for permission refusals.
    The maintainer-accepted ruling (2026-08-19, option 1): give the AUTHOR a
    producer-side way to mark a refusal message user-facing, once, at the contract
    level — status-agnostic, with #3821 preserved by construction for everything
    unmarked.

    The marking: set userMessage (non-empty string) on the thrown error at
    throw time. It is a text-carrying field, not a boolean beside message — the
    mark and the marked text are one value, so no boundary that rewraps or
    substitutes message can promote platform prose into the marked channel, and
    platform/driver code never sets it.

    • @objectstack/spec: ApiErrorSchema.userMessage and
      EnhancedApiErrorSchema.userMessage (optional, additive).
    • @objectstack/types: declaredUserMessage(error) — the ONE "is this
      marked?" read (non-empty string, nothing invented) — and
      ThrownHttpError.userMessage on resolveThrownHttpError.
    • @objectstack/rest: mapDataError / resolveErrorResponse ride a declared
      marking onto whatever envelope classification chose (flat body top-level
      userMessage, truncated at the same #5423 bound as the 4xx message).
    • @objectstack/runtime: the QuickJS side-channel carries userMessage
      across the sandbox boundary (both directions, joining code/fields/
      status), and the dispatcher door emits it as a declared sibling in the
      nested envelope.
    • @objectstack/client: the SDK attaches err.userMessage from both wire
      dialects, so a UI renders it verbatim when present and keeps its generic
      substitution when absent.

    The consumer half — the console form rendering a marked message instead of the
    generic form.noPermissionToSave — is objectui#5210.

Patch Changes

  • 2d0af57: fix(tests): give two default-vitest-timeout cases real margin instead of a bare default (#9311)

    Two cases only passed pnpm test when they were not competing for CPU — the
    same defect class as the already-closed precedents #3662, #4186, #4485,
    #5421, #6329: a test running under vitest's defaulttestTimeout /
    hookTimeout with no margin for anything heavier than an idle box.

    packages/types/src/node.test.ts"falls back to the importing package's own resolution when the host does not declare" is the only case
    in the file that performs a real dynamic import() of @objectstack/spec (a
    multi-megabyte package); every sibling in the same describe block resolves
    a small on-disk fixture or fails fast, all under 10ms. Measured on this box:
    ~0.9-1.1s unloaded, already observed failing at 5061ms against the 5000ms
    default under nothing heavier than turbo run test --concurrency=2 (#9311's
    own isolation runs). Gave that one case an explicit 30s testTimeout — the
    same order of magnitude the repo already uses for subprocess/real-load cases
    (#3662 precedent) — and left every sub-10ms sibling alone.

    packages/qa/dogfood/test/semantic-roles.dogfood.test.ts — its
    beforeAll boots the full showcase stack (ObjectQL + ~45 plugins) through
    @objectstack/verify's bootStack, which does not fit vitest's 10s
    hookTimeout default with any margin at all: observed failing at 10027ms
    against the 10000ms budget, and this file's own isolated run measured 18.3s
    (vitest Duration) / 19.5s wall clock for the whole file even with the box
    otherwise idle. Gave the hook an explicit 180s timeout, matching this
    package's own existing house pattern for the identical
    bootStack(showcaseStack, …) call
    (admin-identity-audit-trail.dogfood.test.ts's beforeAll(…, 180_000))
    rather than inventing a new number for the same operation.

    No behaviour change — both suites already pass; this only gives the two
    timeout-sensitive cases room to finish on a loaded box. The repo's full test
    suite is confirmed green at low concurrency (#9311), so this is margin
    repair, not a product fix. turbo.json's default concurrency is out of scope
    for this change (a maintainer-level default, per #9311's own filing).

  • 27a567d: fix(types): teach the internal-leak predicate MySQL's three error templates (#8739)

    looksLikeInternalErrorLeak decides whether a message is a driver dump that
    must not reach an API client. It is applied at three HTTP boundaries
    (@objectstack/rest's mapDataError, @objectstack/runtime's
    dispatcher-plugin and endpoint-executor, the hono adapter) and by
    @objectstack/objectql's log redactor. Its dialect list covered the SQLite
    family and Postgres; on a MySQL deployment it returned false for every one of
    these conditions — silent, not clearing.

    Under the maintainer's 2026-08-15 ruling on #8739, MySQL is a supported
    deployment target
    , not merely a tested dialect — the answer already implied by
    what is published (OS_DATABASE_DRIVER=mysql as a documented deployment knob,
    MysqlConfig as authorable datasource config, per-field MySQL DDL in
    types.mdx) and by a required CI check that stands up a live mysql:8.0. A
    supported target's driver text reaches those boundaries in production, so its
    templates belong in the list.

    Now recognised — one per condition the other two dialects were already
    covered for, each anchored on MySQL's own errmsg template rather than on a bare
    substring:

    • Table 'app.t' doesn't exist (ER_NO_SUCH_TABLE 1146). MySQL's contracted
      spelling quotes db.table as one identifier, so the Postgres
      relation "t" does not exist limb could never reach it.
    • Unknown column 'c' in 'field list' (ER_BAD_FIELD_ERROR 1054). Both quoted
      parts are required; the second is MySQL's clause name (field list,
      where clause, order clause, on clause), and it is what distinguishes the
      driver's template from a sentence that merely calls a column unknown.
    • Duplicate entry 'x' for key 'i' (ER_DUP_ENTRY 1062). The for key tail plus
      a quoted index is the anchor. This is the one MySQL template whose text embeds
      a caller's value rather than an identifier — SQLite's
      UNIQUE constraint failed: t.c and Postgres' violates unique constraint "…"
      both name only an index — which is why closing this gap was worth a behaviour
      change rather than another comment.

    Deliberately still NOT recognised, so the boundary of the change is on the
    record rather than inferred:

    • MySQL's ACL familyAccess denied for user 'u'@'h' to database 'd'
      (1044), SELECT command denied to user … for table 't' (1142) — the
      counterpart of the Postgres permission denied for table limb. Nothing in
      this repo has raised one off a live server, and the standing rule in this
      neighbourhood (unique-violation.ts) is that a dialect's spelling is added
      once it has been MEASURED off a thrown error, never from a reading of the
      manual. Access denied also collides with this platform's own security prose
      (`[Security] Acce...
Read more

@objectstack/trigger-schedule@17.1.0

Choose a tag to compare

@github-actionsgithub-actions released this 20 Aug 11:22
2d3860d

Patch Changes

@objectstack/trigger-record-change@17.1.0

Choose a tag to compare

@github-actionsgithub-actions released this 20 Aug 11:22
2d3860d

Patch Changes

@objectstack/trigger-api@17.1.0

Choose a tag to compare

@github-actionsgithub-actions released this 20 Aug 11:22
2d3860d

Patch Changes

@objectstack/studio@17.1.0

Choose a tag to compare

@github-actionsgithub-actions released this 20 Aug 11:21
2d3860d

Patch Changes

@objectstack/spec@17.1.0

Choose a tag to compare

@github-actionsgithub-actions released this 20 Aug 11:21
2d3860d

Important

This release note is truncated. The changelog entry for @objectstack/spec@17.1.0 is
194,013 characters; the GitHub Releases API rejects any body over
125,000. The complete entry is in packages/spec/CHANGELOG.md.


Minor Changes

  • 07e630e: feat(spec): ActionSchema.onSuccess — post-success navigation for api/script actions, with ${result.*} joining the navigate template's interpolation scope (#9566, #9474)

    The maintainer's 2026-08-18 ruling (recorded on #9566, mirrored on #9474)
    declares ONE post-success navigation contract for both server-executing action
    types instead of two per-type conventions:

    • onSuccess: { navigate, openIn? } — a strict object, read for
      type: 'api' and type: 'script' only (a refinement refuses it on
      url/modal/flow/form, where no success event exists for it to ride —
      the ADR-0078 posture, same enforcement shape as the body-on-non-script
      refinement).
    • navigate is a route/URL template. Its documented interpolation scope is
      ${param.*} + ${ctx.*} (existing) + ${result.*} — NEW: the action's
      server response payload
      (an api action's response body, a script
      handler's return value), which is what makes "server clones a record → jump
      to the new record" declarable: navigate: '/apps/crm/tasks/${result.id}'.
      The interpolation ENGINE stays the renderer's (objectui interpolateTarget);
      the spec records the contract.
    • openIn is the closed enum 'self' | 'newTab', defaulting 'self'
      (materialized, the file's default convention) — no general navigation DSL.
    • The shipped handler-return convention ({ redirectUrl, openIn? },
      objectui#2967/#2904) keeps its 17.0.0 semantics: absent openIn still means
      new-tab (no silent behavior flip for existing handlers); a handler may return
      openIn: 'self' explicitly.

    The console consumer is the downstream objectui half (SPA navigation branch,
    executeAPI navigation handling, ${result.*} interpolation), filed
    Blocked-by these cards; the liveness ledger records the key at planned
    strength with the amend-on-landing instruction.

  • 2f65b1b: error.code is a closed vocabulary at every door (#9106, maintainer ruling
    2026-08-16): the runtime dispatcher's thrown-error exits
    (HttpDispatcher.errorFromThrown, dispatcher-plugin's errorResponseBase,
    endpoint-executor's endpointErrorAnswer — the actions door among them) now
    serve the narrowed code the shared resolver (resolveThrownHttpError,
    @objectstack/types) has always computed, exactly as the REST door has since
    #8016. A thrown code that is not a member of StandardErrorCode ∪ ERROR_CODE_LEDGER no longer reaches error.code.

    It is not dropped: ApiErrorSchema declares a new optional declaredCode
    field — the open, author-authored channel — and the demoted spelling rides
    there. Presence means demotion: the field is absent whenever the producer's
    code is a vocabulary member (it is already in error.code) or the producer
    declared none. The #7867 sandbox passthrough capability is preserved — a
    metadata app's own thrown .code still crosses the QuickJS boundary and still
    reaches the wire.

    For a metadata app that throws its own code (e.g.
    Object.assign(new Error('pick another'), { code: 'DUPLICATE' }) in an action
    body) and reads it back from an actions-door failure:

    • FROM: error.code === 'DUPLICATE'
    • TO: error.code is the closed member the status derives (e.g.
      VALIDATION_ERROR on a 400) and error.declaredCode === 'DUPLICATE'.
      One-line fix: branch on error.declaredCode for app-specific spellings;
      branch on error.code for platform conditions.

    Platform producers are unaffected: every registered code reaches error.code
    verbatim, as before (post-#8846 the dispatcher-vocabulary gate holds that set
    registered). Measured before landing (the ruling's binding precondition): no
    existing consumer of the actions door branches on author-authored strings in
    error.code.

    @objectstack/types adds demotedDeclaredCode(thrown) — the one definition of
    "which spelling a boundary surfaces beside the closed code".

  • 720ee95: fix(security): the shipped admin permission sets no longer grant export on the * wildcard (#8681)

    BREAKING for any deployment whose administrators export today. Landing after
    the v17.0.0 cut, so it ships as minor under the lockstep launch-window
    convention; the migration prescription is registered under protocol major 18,
    where objectstack migrate meta users will look.

    admin_full_access, organization_admin and the derived
    organization_admin_no_bypass shipped objects['*'].allowExport = true. That
    single line made the 17.0 export axis undeniable for anyone holding an admin
    set: an application could declare an object exportable by nobody, ship it, and
    the platform would export it anyway.

    Measured on 17.0.0 GA — 40 export probes, 5 principals, 8 objects, real Bearer
    tokens — an org owner exported crm_quote (9 rows), crm_campaign (13) and
    crm_task (15) with 200 and full data. No app permission set granted export on
    any of the three, and the app had no way to say no:

    1. the wildcard lives in code-package metadata, so editing it answers
      403 [not_overridable] Metadata item 'permission/admin_full_access' is provided by a code package;
    2. the org admin holds no app-authored permission set, so there is nowhere to
      author the per-object allowExport: false that would otherwise have won.

    This was never a gate defect. The same run proves the export gate exact for
    every other principal: a token refused on one object exports another on the same
    route, granting allowExport at runtime flips 403 to 200, and revoking it flips
    it back. A plain member carrying '*': { allowExport: true } exported too — the
    wildcard was simply doing what it said. What changes is that the platform stops
    shipping that grant.

    This is #5491 applied to the export axis. That change removed member_default's
    CRUD wildcard because a wildcard in a set every principal resolves is not a
    default but a floor no app can get under; the export wildcard survived by
    omission rather than by decision, one tier up.

    Migration — grant allowExport explicitly in an app permission set where
    admin export is intended.
    There is no automatic replacement, deliberately:
    which principals may take a bulk machine-readable copy of a table is the
    segregation-of-duties judgement the axis exists to make explicit.

    // In YOUR app's permission set — not a platform set (those are not overridable).{name: 'system_admin',objects: {crm_account: {allowRead: true,allowExport: true},// export intendedcrm_quote: {allowRead: true},// export withheld},}

    ⚠️Nothing fails at parse time, and the shipped sets are re-seeded on
    upgrade.
    A deployment that upgrades without editing anything is valid metadata
    whose administrators have quietly lost export on every object no app set names —
    the first sign is a support report, not an error. Verify behaviourally: sign in
    as an org owner and call GET /api/v1/data/<object>/export, expecting 200 where
    export is intended and 403 EXPORT_NOT_PERMITTED where it is not.

    What is deliberately unchanged. READ is untouched — an admin still sees
    every record they saw before; this narrows bulk egress only. allowExport on a
    '*' entry remains a supported, honoured authoring shape in an app's own sets.
    Specific-over-wildcard precedence is unchanged (an explicit per-object entry
    still overrides the wildcard). The viewAllRecords / modifyAllRecords
    super-user bits still do not imply export, exactly as before. And an app's own
    admin set already gets precisely its declared posture — declared false answers
    403, declared true answers 200 — which is what makes withdrawing the platform
    grant safe rather than merely restrictive.

    Both admin sets are fixed together, and the org-admin pair from one declaration
    (organization_admin_no_bypass is derived from organization_admin). Fixing
    one and not the other was rejected outright: a half-closed export boundary reads
    as closed and is not.

  • f287435: feat(spec): refuse undeclared keys on the analytics authoring surface (#4001 data batch D)

    BREAKING accept-set narrowing, landing after the v17.0.0 cut (the lockstep
    launch-window convention ships it as minor; the migration prescription is
    registered under protocol major 18, where os migrate meta users will look).

    All 8 data/analytics.zod.ts sites are strict: the cube family (CubeSchema +
    its refreshKey block, MetricSchema + its filters[] items,
    DimensionSchema, CubeJoinSchema) and the query family
    (AnalyticsQuerySchema + its timeDimensions[] items). Before this change an
    undeclared key on any of them was silently dropped: a join authored with a
    typo'd relationship registered with the many_to_one default — a different
    join shape than the author declared — and a cube's misspelled key vanished
    under a successful parse.

    The subtle half is the query: /analytics/query's TOP level has been strict
    since #3878 (AnalyticsQueryRequestSchema), but top-level strictness does not
    rec...

Read more

@objectstack/setup@17.1.0

Choose a tag to compare

@github-actionsgithub-actions released this 20 Aug 11:21
2d3860d

Patch Changes

@objectstack/service-storage@17.1.0

Choose a tag to compare

@github-actionsgithub-actions released this 20 Aug 11:22
2d3860d

Patch Changes

  • bbd86ed: Attachment access hooks: read the caller's org under the blessed organizationId name

    callerContext() in the sys_attachment access kit built its fallback
    execution envelope from session.tenantId — an alias removed from the
    hook/action session surface in v11 (#3290). HookContextSchema strips a
    tenantId key and the engine's buildSession only ever emits
    organizationId, so on every call that reached the session fallback (no
    execution context riding along) the envelope handed to
    ISharingService.canEdit carried no organization at all. Parent-record
    access for attachments was therefore evaluated without the caller's active
    org on that path. It now reads session.organizationId, matching the
    sys_comment kit, which already did.

    The sys_comment kit's own callerContext() had the same read as a dead
    first arm (s.tenantId ?? s.organizationId); the arm is removed. That half
    is behaviour-neutral — the fallback already carried the value.

    Both kits gain coverage of the session-fallback path in both directions: the
    blessed name is read, and a stray removed-alias key does not become the org.

  • 593c4bf: feat(spec): storage becomes the canonical CoreServiceName slot; file-storage stays a deprecated v17 alias (#9683)

    Maintainer ruling, 2026-08-18, verbatim: 「9683 file-storage 可以叫 storage」.
    The file-storage slot was the only CoreServiceName member whose spelling
    diverged from its documented accessor (services.storage), with no recorded
    reason anywhere in the tree.

    • CoreServiceName gains storage as the canonical member; file-storage
      stays an accepted, deprecated alias within v17 (it is a published enum
      member — existing getService('file-storage') callers keep working).
      CORE_SERVICE_PROVIDER and ServiceRequirementDef carry both.
    • @objectstack/service-storage registers the same instance under both
      names (the http.server / http-server pattern), pinned by an
      alias-equivalence test.
    • Every internal consumer resolves storage: the HTTP dispatcher, the email
      plugin's attachment store, and os migrate files-to-references. Discovery
      reports the service under the canonical storage key and mirrors the row
      verbatim under the file-storage key for the alias's v17 lifetime, so
      existing discovery readers (e.g. the console endpoint catalog) keep
      working.
    • Docs (kernel/runtime-services, kernel/contracts) now document the
      canonical slot; a custom v17 provider for this slot should register both
      names.
  • 1258dca: Restore the #4757 unscoped multi-delete refusal on sys_attachment through the wired engine (#9719).

    ObjectQL.registerHook gains an opt-in dispatchUnscopedMultiDelete declaration (valid on beforeDelete registrations only — anything else is refused at registration): when a multi: true delete arrives with no where at all (absent or null), the engine's predicate path dispatches the whole-operation context ONCE to declaring registrations — before any matched row is resolved, zero-match included — so a guard about the operation's shape can refuse it. Binding input.id on that context is refused (HookTargetRebindError, path 'unscoped-multi'). Undeclared registrations, scoped deletes (including the match-all where: {}), and by-id deletes see no new dispatch.

    The sys_attachment access guard declares the flag, so its documented refusal of a predicate-less multi-delete fires again with its declared envelope (ATTACHMENT_DELETE_DENIED, HTTP 403): since the per-row dispatch contract (#5038/#5574) that branch was unreachable, and a predicate-less multi: true delete quietly removed every row the caller happened to be entitled to. System-context and context-less programmatic deletes bypass the guard exactly as before.

  • 4639cec: Behaviour change: an unscoped multi: true UPDATE of sys_comment is now refused, where it previously succeeded for a caller entitled to every row (#9974).

    This is not the restoration of a guard that used to work — it is a deliberate narrowing of what the engine accepts, ruled by the maintainer on 2026-08-19. If you issue ql.update('sys_comment', data, { multi: true }) with no where at all, that call works today and will start failing with RECORD_NOT_ACCESSIBLE / 403. The fix at the call site is to say which rows you mean — pass a where. The explicit match-all where: {} is still accepted and still authorizes every matched row individually; only an absent or null predicate is refused.

    Why the accept set narrowed rather than the declaration: resolveTargetRows has declared this refusal for both write verbs since #4630, but on update it could only ever fire by accident — when the sweep happened to touch a row the caller lacked rights to, and then with a per-row message (Cannot update comment c2: …) naming a row rather than the shape. A caller who owned every row had the whole table rewritten, and a zero-match probe resolved silently. A guard that fires by accident reads as enforcement while enforcing nothing. The ruling weighed recoverability: a delete leaves a trace of who removed what, an overwrite leaves none — the old value is gone on the spot with nothing to restore from — and a forgotten where is the mistake generated code makes most often.

    Engine (@objectstack/objectql).#9719's opt-in whole-operation dispatch now covers beforeUpdate's predicate path as well as beforeDelete's, and the registration flag is renameddispatchUnscopedMultiDeletedispatchUnscopedMultiWrite (one flag generalized to both events rather than a second flag; it is per-registration and per-event, so a delete-only guard still says "delete only" by declaring it on beforeDelete alone). Declaring it on any other event is still refused at registration time. Binding input.id on the whole-operation context is refused on both verbs (HookTargetRebindError, path unscoped-multi), and the error now names the caller's event.

    Blast radius. The dispatch is delivered ONLY to registrations that declare the flag, so sys_comment is the only object whose update accept set changes; every other object's unscoped multi: true update behaves exactly as before. sys_attachment keeps its delete-only declaration and is unaffected on update. A repo-wide structural sweep of 4 663 source files found no in-tree caller — none in examples/, none in the dogfood apps, none in packages/ source — that issues an unscoped multi: true update against a declaring object.

    @objectstack/service-storage is a rename-only follow: its sys_attachment guard declares the renamed flag on the same event, with the same behaviour.

  • Updated dependencies [56656aa]

  • Updated dependencies [c9f5950]

  • Updated dependencies [d6e80b2]

  • Updated dependencies [07e630e]

  • Updated dependencies [66beee0]

  • Updated dependencies [2f65b1b]

  • Updated dependencies [720ee95]

  • Updated dependencies [f287435]

  • Updated dependencies [2782805]

  • Updated dependencies [e43d63a]

  • Updated dependencies [9aa8890]

  • Updated dependencies [7c9c1dd]

  • Updated dependencies [03520eb]

  • Updated dependencies [899052a]

  • Updated dependencies [75b7c24]

  • Updated dependencies [d5552ca]

  • Updated dependencies [d9813a9]

  • Updated dependencies [8640fb2]

  • Updated dependencies [2420641]

  • Updated dependencies [2ad91c3]

  • Updated dependencies [f57fb38]

  • Updated dependencies [00777a0]

  • Updated dependencies [d491625]

  • Updated dependencies [2d0af57]

  • Updated dependencies [420804d]

  • Updated dependencies [716ac9b]

  • Updated dependencies [a38408a]

  • Updated dependencies [62b1427]

  • Updated dependencies [7ea1372]

  • Updated dependencies [23abe27]

  • Updated dependencies [985a9cd]

  • Updated dependencies [5f5e234]

  • Updated dependencies [a8189ae]

  • Updated dependencies [26e70fb]

  • Updated dependencies [27a567d]

  • Updated dependencies [42b05af]

  • Updated dependencies [2b292ce]

  • Updated dependencies [abcf853]

  • Updated dependencies [8b9eba5]

  • Updated dependencies [d575779]

  • Updated dependencies [94f7ef8]

  • Updated dependencies [c5ac5e4]

  • Updated dependencies [a777944]

  • Updated dependencies [dd88e1c]

  • Updated dependencies [856527c]

  • Updated dependencies [870f710]

  • Updated dependencies [79c46da]

  • Updated dependencies [1e050a5]

  • Updated dependencies [7ff3975]

  • Updated dependencies [29d055b]

  • Updated dependencies [65589d6]

  • Updated dependencies [2c86fe3]

  • Updated dependencies [e196c6a]

  • Updated dependencies [24173e9]

  • Updated dependencies [4ab7523]

  • Updated dependencies [19539b4]

  • Updated dependencies [f8eb736]

  • Updated dependencies [11b779e]

  • Updated dependencies [739fe5b]

  • Updated dependencies [4bfe1a5]

  • Updated dependencies [2065e31]

  • Updated dependencies [b69d0f5]

  • Updated dependencies [4d47afe]

  • Updated dependencies [e4e5c6e]

  • Updated dependencies [9a56784]

  • Updated dependencies [d00d2f6]

  • Updated dependencies [df0c12d]

  • Updated dependencies [d31785f]

  • Updated dependencies [c308a4f]

  • Updated dependencies [e2899f6]

  • Updated dependencies [3851f87]

  • Updated dependencies [2a29caa]

  • Updated dependencies [09a6eee]

  • Updated dependencies [1a7f907]

  • Updated dependencies [cd455c8]

  • Updated dependencies [e1bb0ca]

  • Updated dependencies [30d3752]

  • Updated dependencies [c80e7ae]

  • Updated dependencies [09a9a8a]

  • Updated dependencies [07026cf]

  • Updated dependencies [5d4f3d5]

  • Updated dependencies [4d80e8b]

  • Updated dependencies [30b1c63]

  • U...

Read more