Skip to content

feat(calendar): add durable subscription lifecycle domain - #514

Closed
seonghobae wants to merge 11 commits into
feat/access-grant-domain-413from
feat/calendar-subscription-domain-413
Closed

feat(calendar): add durable subscription lifecycle domain#514
seonghobae wants to merge 11 commits into
feat/access-grant-domain-413from
feat/calendar-subscription-domain-413

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Buyer/security outcome

Refs #413. ScopeWeave's protected calendar feed still accepts a broad session JWT in the URL query string. Calendar clients need a reusable URL lifecycle that is materially different from the one-time stream / attachment_view grant lifecycle in #506. This stacked slice adds the framework-neutral credential domain needed for a separately revocable, rotatable, project-bound calendar subscription without prematurely changing the protected route, schema, or UI.

This PR does not close #413. It creates no database table or migration, changes no Hono route, issues no production subscription credential, and implements no UI. The issue #413 product-design prerequisite is captured in an editable Figma interaction contract; future UI implementation remains separate work and must conform to that contract plus the eventual API behavior.

Exact current stack

Protected develop: 28420da358f57be5e85be3660251e39b85e1cc94.

Current parent/base: feat/access-grant-domain-413@72460895916ce413146f111923fc05ebfae1dc43 (#506).

Current contributor head: a2ce02710447ad4e46b57909f87d65fcfe5c1a10.

Fresh parent→child comparison is ahead-only, zero-behind, has the exact current parent as merge base, and changes exactly seven files:

  • server/calendar_subscription_domain.mjs
  • tests/unit/calendar-subscription-domain.test.mjs
  • tests/unit/calendar-subscription-domain-edge.test.mjs
  • tests/unit/coverage-script-contract.test.mjs
  • package.json
  • docs/doctoring/calendar-subscription-domain.md
  • CHANGELOG.md

No existing auth/session implementation, Hono route, SQLite schema, migration ledger, Clearfolio behavior, attachment behavior, browser client, workflow, dependency, or lockfile is changed.

Parent reconciliation

The parent advanced from historical head 1c9d5cd6... to 72460895... by absorbing the current protected Microsoft Project XML hardening. This branch was reconciled from the exact current parent tree, then only the six non-shared calendar-domain blobs were overlaid. CHANGELOG.md was merged semantically so the parent’s protected XML-whitespace and short-lived access-grant entries remain intact while the calendar-subscription entry is added.

Current merge commit a2ce0271... records the old child and exact current parent as ancestry, but the tree itself is parent-first rather than a stale-child reuse. Fresh comparison proves no protected XML/security/source regression and exactly the intended seven-file semantic delta. Any later parent movement requires the same parent-tree-first reconstruction and shared-file merge before this branch advances.

Figma interaction contract

Editable design: https://www.figma.com/design/EwcePYFQ85DjBLhFfMXLvI

ScopeWeave Calendar Subscription Management — Issue 413 specifies interaction/security, management list, create, one-time secret reveal, rotate confirmation, revoke confirmation, and empty states. The design is active-PR evidence, not shipped behavior; executable browser acceptance remains mandatory before a future UI slice can claim it.

Implemented contract

  • fixed calendar audience scopeweave:calendar;
  • 32 random bytes per reusable secret, encoded as 43-character unpadded base64url;
  • independent 128-bit csub_... correlation identifier;
  • plaintext secret returned only on create() / rotate(); only SHA-256 hashes cross the repository port and neither plaintext nor hash enters audit events;
  • project-management authorization for create/list/rotate/revoke;
  • explicit expiry with exact-expiry rejection;
  • safe lifecycle listing without secret/hash/membership-version material;
  • live membership-version rechecks inside atomic usage and rotation transitions;
  • old-secret invalidation on rotation and operator-idempotent revocation;
  • post-commit audit delivery semantics with a production transactional-outbox obligation; and
  • normalized future persistence responsibilities separated from feat(data): add fail-closed schema migration ledger #500's schema-transition authority.

TDD and verification boundary

The contract preceded production implementation: c86c878f... imported an absent production module and failed RED with ERR_MODULE_NOT_FOUND; subsequent commits supplied the domain and edge/failure coverage. Both focused behavior suites are registered in the canonical c8 path and coverage-script-contract.test.mjs locks the registrations.

Every workflow/check/review result predating a2ce02710447ad4e46b57909f87d65fcfe5c1a10 is historical and non-authorizing. Fresh exact-current-head repository and organization evidence is required. Pending, queued, skipped-required, neutral, stale, predecessor, synthetic, status-only, model-only, or author-only evidence is non-passing.

Standards and integration boundary

docs/doctoring/calendar-subscription-domain.md distinguishes protected truth from active stacked work, records the Figma contract, threat boundary, atomic membership contract, persistence/audit obligations, rollback, and APA 7 references to RFC 6750, RFC 9700/BCP 240, and RFC 8725.

Do not integrate independently of #506. After #506 reaches protected develop, reconcile this semantic slice onto the resulting live protected head and require every then-applicable exact-head repository/organization CI, browser E2E where applicable, owned production statement/branch/function/line coverage, docstring evidence, SAST/security/dependency/supply-chain gates, zero valid unresolved review findings, and qualifying current-head/last-push independent approval under the live rulesets. No predecessor-head evidence transfers.

@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 70caa774-c31f-4cfd-ad09-ea9155c8336f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cursor

cursor Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not merge this head. A reusable calendar URL that only re-checks live membership can survive remove-then-rejoin: authorize() captures a fresh version, the adapter compares that fresh value to live membership, and the stored issuance epoch is never consulted. That is acceptable for a ≤5-minute one-time grant; it is not acceptable for a feed secret that lives in calendar clients, browser history, and proxy logs.

Land the successor instead: #539 (03b0d91 on feat/access-grant-domain-413). It keeps this framework-neutral boundary and adds:

  • issuance-epoch binding on use, with rotate as the rejoin re-bind path
  • frozen purpose: calendar_read on the principal so a thin route cannot treat the secret as a session-equivalent project credential
  • 366-day create/rotate lifetime cap and exact-expiry rejection on the use path
  • first-transition-only revoke audit via revocation_applied
  • exact-expiry, remove-then-rejoin, mid-check bump, rotate-vs-use, and leap-day tests

#524 (SQLite adapter) must rebase onto #539, not this head. Local npm run test:unit passed on #539. This review does not approve #514 or #539. Predecessor-head checks on a2ce027 are not authorizing evidence for #539.

Open in Web View Automation 

Sent by Cursor Automation: Fix Issues

Comment on lines +261 to +271
const membershipVersion = await readMembershipVersion(
membershipRevocation,
existing.subject_id,
existing.project_id,
);
const used = await repository.recordUsageAtomically(secretHash, {
now_ms: readNow(clock),
project_id: projectId,
audience: CALENDAR_SUBSCRIPTION_AUDIENCE,
membership_version: membershipVersion,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This passes a fresh membership version into recordUsageAtomically(). After remove-then-rejoin the live version equals that freshly captured value, the unrevoked row still has the old epoch, and a leaked calendar URL keeps working.

Pass existing.membership_version (issuance epoch). Require live === issued && live === row.membership_version in the same transaction. Make revoke-on-membership-change mandatory, not an alternative. #539 implements this and adds a remove-then-rejoin test that expects 401 until rotate.

subscriptionId: used.subscription_id,
subjectId: used.subject_id,
projectId: used.project_id,
audience: used.audience,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The authorized principal returns only audience. A thin route can treat this as a session-equivalent project principal and open JSON APIs, SSE, or attachments. Issue #413 requires project-only calendar read authority.

Freeze purpose: 'calendar_read' on create, persist, authorize, and the returned principal. #539 does this.

Comment on lines +197 to +205
h.setNow(1_800_000_005_000);
const principal = await h.service.authorize({ secret: created.secret, projectId: 'project-1' });
assert.deepEqual(principal, {
subscriptionId: created.subscriptionId,
subjectId: 'user-1',
projectId: 'project-1',
audience: CALENDAR_SUBSCRIPTION_AUDIENCE,
});
assert.deepEqual(h.usages, [{ subscription_id: created.subscriptionId, used_at_ms: 1_800_000_005_000 }]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This authorizes at nowMs < expiresAtMs only. List-status at exact expiry in the edge file is not use-path coverage. Parent access-grant sets the clock to expiresAtMs and expects 401.

Add setNow(created.expiresAtMs) then authorize()calendar_subscription_unauthorized. Also add a remove-then-rejoin case and a rotate-vs-use race where the old secret loses. #539 covers these.

Copy link
Copy Markdown
Contributor Author

Superseded by #539 after fresh exact-head comparison. #539 head 03b0d91973a3ab7faff2d9e56bd33ce244b071cb is an exact one-commit descendant of this head a2ce02710447ad4e46b57909f87d65fcfe5c1a10 (ahead_by=1, behind_by=0, merge base = this head), so all #514 domain work is preserved. The successor closes the remove→rejoin issuance-epoch authority gap, fixes the principal purpose, lifetime/expiry rules, and first-transition revoke auditing. #524 must be reconciled onto #539 before its persistence evidence is current.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant