Skip to content

wip - #2064

Closed
leave330 wants to merge 0 commit into
mainfrom
feat/oapi-event-subscribe
Closed

wip#2064
leave330 wants to merge 0 commit into
mainfrom
feat/oapi-event-subscribe

Conversation

@leave330

@leave330 leave330 commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Withdrawn; work moved out of this branch.

@github-actions github-actions Bot added the size/XL Architecture-level or global-impact change label Jul 27, 2026
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.16% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: refined event subscription support across management, consume, and encryption.
Description check ✅ Passed The description covers the required summary, change list, test plan, and related issues, though it uses a nonstandard "Feature changes" heading.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/oapi-event-subscribe

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (17)
cmd/event/bus.go-32-41 (1)

32-41: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Make the bus UAT sentinel errors typed.

cmd/event/bus.go is under the active cmd/ scope of errs-no-bare-wrap, so errUATUserMismatch/errUATUnverifiable created with bare errors.New and line 75 fmt.Errorf("%w", errUATUnverifiable) will fail the pinned lint. Use typed error constructors from github.com/larksuite/cli/errs and preserve the cause with .WithCause(err).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/event/bus.go` around lines 32 - 41, Replace the bare errors.New
definitions of errUATUserMismatch and errUATUnverifiable with typed constructors
from github.com/larksuite/cli/errs. Update the error path around
verifyUATBelongsToUser that currently uses fmt.Errorf("%w", errUATUnverifiable)
to preserve the underlying cause via the typed error’s WithCause(err), while
retaining the existing sentinel identities and messages.

Source: Coding guidelines

internal/event/bus/bus.go-445-455 (1)

445-455: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Guard the encrypted path on a non-empty RemoteSubscriptionID.

The completeness gate above only fires when RemoteSubscriptionID != "". A Hello with IncludeResourceData=true and an empty RemoteSubscriptionID therefore reaches fetchAndSet with an empty subscription id — protocol.Hello's own contract says IncludeResourceData is "Only meaningful alongside a non-empty RemoteSubscriptionID" (internal/event/protocol/messages.go lines 144-151). Fail closed on that combination rather than issuing a GetEncryptKey("").

🛡️ Proposed guard
-	if hello.IncludeResourceData && b.encryptKeyProvider != nil {
+	if hello.IncludeResourceData && bc.RemoteSubscriptionID() == "" {
+		b.logger.Printf("WARN: rejecting encrypted consumer pid=%d key=%q: include_resource_data without a remote_subscription_id",
+			hello.PID, hello.EventKey)
+		if werr := bc.writeFrame(protocol.NewHelloAckRejected("v1", protocol.RejectReasonIncompleteRefinedHello)); werr != nil {
+			b.logger.Printf("WARN: reject hello_ack (incomplete_refined_hello) write to pid=%d key=%q failed: %v",
+				hello.PID, hello.EventKey, werr)
+		}
+		bc.Close()
+		return
+	}
+	if hello.IncludeResourceData && b.encryptKeyProvider != nil {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/bus/bus.go` around lines 445 - 455, Update the
encrypted-resource handling around b.encryptKeyProvider.fetchAndSet to require a
non-empty bc.RemoteSubscriptionID() in addition to IncludeResourceData. When
IncludeResourceData is true but the subscription ID is empty, reject the Hello
and close the connection using the existing decrypt-key-unavailable response
path, without calling fetchAndSet or issuing a key lookup with an empty ID.
internal/event/bus/status_query_test.go-61-66 (1)

61-66: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assert the exact v2 markers, not just non-emptiness.

A regression that sets ProtocolVersion = "v1" or drops CapabilityHelloV2 from the list still passes these checks. protocol.ProtocolVersionV2, protocol.CapabilityRefinedRouting, and protocol.CapabilityHelloV2 are exported constants — compare against them directly.

As per coding guidelines, "contract tests must assert the changed field or behavior directly so reverting the implementation causes failure."

💚 Proposed assertions
-	if resp.ProtocolVersion == "" {
-		t.Error("ProtocolVersion is empty, want a non-empty v2 marker")
-	}
-	if len(resp.Capabilities) == 0 {
-		t.Error("Capabilities is empty, want a non-empty capability list")
-	}
+	if resp.ProtocolVersion != protocol.ProtocolVersionV2 {
+		t.Errorf("ProtocolVersion = %q, want %q", resp.ProtocolVersion, protocol.ProtocolVersionV2)
+	}
+	for _, want := range []string{protocol.CapabilityRefinedRouting, protocol.CapabilityHelloV2} {
+		if !slices.Contains(resp.Capabilities, want) {
+			t.Errorf("Capabilities = %v, want it to contain %q", resp.Capabilities, want)
+		}
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/bus/status_query_test.go` around lines 61 - 66, Update the
response assertions in the status query test to compare ProtocolVersion directly
with protocol.ProtocolVersionV2, and verify Capabilities contains both
protocol.CapabilityRefinedRouting and protocol.CapabilityHelloV2. Replace the
non-empty checks while preserving the existing failure reporting style.

Source: Coding guidelines

cmd/event/consume.go-70-74 (1)

70-74: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Help text cites refined EventKeys that aren't in the shipped catalog. The only registered refined base key is im.message.created_v1 (events/refined/refined_keys_mock.json), yet the event command tree's help documents two other names; copy-pasted examples fail with "unknown EventKey".

  • cmd/event/consume.go#L70-L74: replace im.message.example_v1 with im.message.created_v1 in the REFINED EVENTKEYS paragraph, and in the Example block at lines 102-105.
  • cmd/event/event.go#L21-L24: replace im.message.receive_v2/chat-id/oc_xxx with im.message.created_v1/chat-id/oc_xxx, matching the same file's own example at line 45.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/event/consume.go` around lines 70 - 74, Update the refined EventKey
examples to use the registered base key im.message.created_v1: change the
REFINED EVENTKEYS paragraph and Example block in cmd/event/consume.go, and the
example in cmd/event/event.go, replacing im.message.example_v1 and
im.message.receive_v2 respectively while preserving the existing
resource-selector format.
cmd/event/subscription/subscription_test.go-230-237 (1)

230-237: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Isolate config state for the cmdutil.TestFactory case.

This test builds a real factory but doesn't pin LARKSUITE_CLI_CONFIG_DIR, so it can read the developer's/CI host config dir.

As per coding guidelines: "Use cmdutil.TestFactory(t, config) for test factories and set LARKSUITE_CLI_CONFIG_DIR to t.TempDir() with t.Setenv to isolate configuration state."

🛡️ Proposed fix
 func TestResolveEffectiveIdentity_StrictModeRejectsCrossIdentity(t *testing.T) {
+	t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
 	// Bot-only account -> strict mode bot (SupportedIdentities bit 2). A real
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/event/subscription/subscription_test.go` around lines 230 - 237, Isolate
the TestFactory configuration in
TestResolveEffectiveIdentity_StrictModeRejectsCrossIdentity by setting
LARKSUITE_CLI_CONFIG_DIR to a fresh t.TempDir() via t.Setenv before calling
cmdutil.TestFactory. Keep the existing factory configuration and test behavior
unchanged.

Source: Coding guidelines

cmd/event/subscription/update.go-57-58 (1)

57-58: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Example won't parse: pflag boolean flags require =.

--include-resource-data false keeps false as a second positional arg, so cobra.ExactArgs(1) rejects the command with an arg-count error instead of the intended typed rejection.

📝 Proposed fix
-		Example: `  lark-cli event subscription update sub_xxx --include-resource-data false`,
+		Example: `  lark-cli event subscription update sub_xxx --include-resource-data=false`,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/event/subscription/update.go` around lines 57 - 58, Update the Example
command for the subscription update Cobra command to use equals syntax for the
boolean flag, ensuring false is parsed as the flag value and the command still
receives exactly one positional argument under Args: cobra.ExactArgs(1).
cmd/event/subscription/list.go-146-159 (1)

146-159: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

A negative --page-size is silently dropped.

o.pageSize > 0 quietly ignores negative input rather than reporting it. Reject it with a typed validation error in runList so the user isn't silently given server-default paging.

🛡️ Proposed fix (in `runList`, before building the request)
if o.pageSize < 0 {
    return errs.NewValidationError(errs.SubtypeInvalidArgument,
        "--page-size must not be negative").
        WithParam("page-size").
        WithHint("omit --page-size (or pass 0) to use the server default")
}

As per coding guidelines: "never silently coerce unsupported inputs, ignore unhonored options... Return a typed validation error when a requested behavior cannot be honored."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/event/subscription/list.go` around lines 146 - 159, The runList
validation flow must reject negative o.pageSize values before constructing the
subscription request. Return the established typed invalid-argument validation
error, identifying the page-size parameter and explaining that zero or omission
uses the server default; preserve the existing request-building behavior for
nonnegative values.

Source: Coding guidelines

cmd/event/status.go-39-58 (1)

39-58: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the help-text grammar and trailing whitespace.

Line 47 ends with a trailing space, and "a missing scope silently fall back" should be "a missing scope silently falls back".

✏️ Proposed fix
 SCOPE: the local view (bus in-memory state) needs no scope and is always
 shown. For a refined consumer, remote_state/expire_time/
 include_resource_data are additionally supplemented from a live 'subscription
-get' call, but ONLY as a weak, optional dependency: it requires event:subscription:read; 
-a missing scope silently fall back to the local-only view — this never fails the command.
+get' call, but ONLY as a weak, optional dependency: it requires
+event:subscription:read; a missing scope silently falls back to the
+local-only view — this never fails the command.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/event/status.go` around lines 39 - 58, Update the `Long` help text in the
event status command to remove the trailing whitespace after
`event:subscription:read;` and correct “a missing scope silently fall back” to
“a missing scope silently falls back.”
cmd/event/subscription/delete_test.go-179-182 (1)

179-182: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Unchecked type assertion can panic instead of failing the test.

If local_impact is present but note is missing or non-string, localImpact["note"].(string) panics. Use the comma-ok form so the assertion fails with a readable message.

🛡️ Proposed fix
-	localImpact, ok := generic["local_impact"].(map[string]interface{})
-	if !ok || !strings.Contains(localImpact["note"].(string), "not a substitute for stopping local consumers") {
+	localImpact, ok := generic["local_impact"].(map[string]interface{})
+	note, noteOK := localImpact["note"].(string)
+	if !ok || !noteOK || !strings.Contains(note, "not a substitute for stopping local consumers") {
 		t.Errorf(`"local_impact.note" = %v, want it to mention "not a substitute for stopping local consumers"`, generic["local_impact"])
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/event/subscription/delete_test.go` around lines 179 - 182, Update the
local_impact validation in the test to extract localImpact["note"] with a
comma-ok string assertion before calling strings.Contains. Treat a missing or
non-string note as a test failure and retain the existing readable error message
instead of allowing a panic.
cmd/event/subscription/get_test.go-124-134 (1)

124-134: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assert the typed metadata, not just "some typed error".

This error-path test only checks that errs.ProblemOf succeeds. Pin Category and Subtype (invalid_response) so a subtype regression actually fails the test.

💚 Proposed fix
-	if _, ok := errs.ProblemOf(err); !ok {
-		t.Fatalf("expected a typed errs.* error, got %T: %v", err, err)
-	}
+	p, ok := errs.ProblemOf(err)
+	if !ok {
+		t.Fatalf("expected a typed errs.* error, got %T: %v", err, err)
+	}
+	if p.Subtype != errs.SubtypeInvalidResponse {
+		t.Errorf("Subtype = %s, want %s", p.Subtype, errs.SubtypeInvalidResponse)
+	}

As per coding guidelines: "Error-path tests must assert typed metadata through errs.ProblemOf (category, subtype, and param)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/event/subscription/get_test.go` around lines 124 - 134, Update
TestGetSubscription_SuccessWithNoData_ReturnsTypedInternalError to inspect the
metadata returned by errs.ProblemOf(err) and assert Category and Subtype are
both "invalid_response"; retain the existing non-nil and typed-error checks.

Source: Coding guidelines

internal/event/consume/refined_test.go-58-59 (1)

58-59: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

gofmt will reformat this line.

boolPtr has extra padding before {; gofmt does not align consecutive one-line func declarations, so gofmt -l . will flag this file.

As per coding guidelines: "Run gofmt; Go code must be formatted with no gofmt -l . output."

🎨 Formatting fix
 func strPtr(s string) *string { return &s }
-func boolPtr(b bool) *bool     { return &b }
+func boolPtr(b bool) *bool    { return &b }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/consume/refined_test.go` around lines 58 - 59, Run gofmt on
the helper declarations near strPtr and boolPtr, removing the extra spacing
before boolPtr’s opening brace so the file produces no gofmt output.

Source: Coding guidelines

internal/event/bus/conn.go-437-469 (1)

437-469: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

No recovery path out of decrypt-degraded state.

Once decryptFailCount reaches the threshold, degradedReason stays decrypt_failed forever: a later successful decrypt calls SetDecryptState(decryptStateDecrypted), which touches neither the counter nor degradedReason. Three transient failures (e.g. a key rotation window) permanently mark the consumer degraded in event status. Consider clearing both on a successful decrypt.

🛠️ Proposed fix sketch
 func (c *Conn) SetDecryptState(state string) {
 	c.identityMu.Lock()
 	defer c.identityMu.Unlock()
 	c.decryptState = state
+	// A successful decrypt supersedes an earlier persistent-failure
+	// degradation: reset the streak and clear the decrypt-owned reason.
+	if state == decryptStateDecrypted {
+		c.decryptFailCount = 0
+		if c.degradedReason == decryptStateFailed {
+			c.degradedReason = ""
+		}
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/bus/conn.go` around lines 437 - 469, Update SetDecryptState to
restore healthy state when called with decryptStateDecrypted: reset
decryptFailCount and clear degradedReason, while preserving the existing state
update and locking. Leave failure recording in RecordDecryptFailure unchanged.
internal/event/bus/identity.go-43-56 (1)

43-56: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use typed precondition error for missing app config

loadCurrentAppProfile should return a typed precondition error for the missing app config case instead of a bare fmt.Errorf, unless internal config sentinels are intentionally exempt.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/bus/identity.go` around lines 43 - 56, Update
resolveCurrentIdentity’s missing-current-app branch to return the project’s
typed precondition error, reusing the established constructor or sentinel rather
than fmt.Errorf. Preserve the existing message/context and leave the no-user
validation unchanged; only exempt this case if internal config sentinels are
explicitly handled that way elsewhere.

Source: Coding guidelines

internal/event/bus/lifecycle_test.go-217-223 (1)

217-223: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Possible flake: this waitForCalls(..., 1) can consume the first run's token.

started is buffered, and the first Handle's token was already drained at line 207 — but the merged run's token is the only one left only if timing cooperates; on a slow worker the call at line 218 returns immediately after the queue re-send and the 50ms settle window is all that guards the callCount() != 2 assertion. Waiting explicitly for the second call (waitForCalls(t, action, 1) after release is fine, but assert with a bounded poll to 2) removes the timing dependency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/bus/lifecycle_test.go` around lines 217 - 223, Make the
post-release assertion in the lifecycle test wait explicitly for the merged
second Handle invocation using a bounded poll until action.callCount() reaches
2, rather than relying on the fixed 50ms settle window. Keep the existing
release and first-call synchronization, and retain the failure diagnostics from
action.allCalls().
cmd/event/subscription/reactivate_test.go-83-90 (1)

83-90: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Error-path tests only check that the error is typed, not which typed error it is. Both no-data tests copy the same errs.ProblemOf(err); !ok shape, so a subtype/category regression in doReactivateSubscription/doRenewSubscription would go unnoticed.

  • cmd/event/subscription/reactivate_test.go#L83-L90: capture the *errs.Problem and assert Subtype == errs.SubtypeInvalidResponse (plus category).
  • cmd/event/subscription/renew_test.go#L82-L89: apply the same assertion for the renew no-data path.

As per coding guidelines: "Error-path tests must assert typed metadata through errs.ProblemOf (category, subtype, and param)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/event/subscription/reactivate_test.go` around lines 83 - 90, The no-data
error tests only verify that an error is typed, not its metadata. In
cmd/event/subscription/reactivate_test.go lines 83-90, capture the *errs.Problem
returned by errs.ProblemOf(err) and assert the expected category,
errs.SubtypeInvalidResponse subtype, and parameter; apply the same metadata
assertions to the no-data test in cmd/event/subscription/renew_test.go lines
82-89.

Source: Coding guidelines

internal/event/source/feishu.go-310-333 (1)

310-333: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

UnionId is never consulted despite the doc promising a preference order.

The comment says "OpenId is preferred over UnionId when a 'user' authority carries both", which implies a fallback; the code only reads OpenId, so a user authority carrying only union_id normalizes to the bare "user". That value feeds the hub's authority cross-check, where a bare "user" will not match an owner keyed by open_id, and the event is silently dropped. Either add the fallback or reword the doc to state that a union_id-only authority is deliberately not resolvable here.

🐛 Proposed fix (if the fallback is intended)
 	principalID := ""
 	if a.OpenId != nil {
 		principalID = *a.OpenId
+	} else if a.UnionId != nil {
+		principalID = *a.UnionId
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/source/feishu.go` around lines 310 - 333, Update
FormatLifecycleAuthority to use UnionId as the principalID fallback when OpenId
is absent, preserving OpenId’s precedence when both identifiers are present.
Keep the existing nil handling and formatSubscriptionAuthority normalization
unchanged.
internal/event/subscription_client_test.go-181-192 (1)

181-192: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assert the typed metadata, not just "some typed error".

errs.ProblemOf(err) is checked only for ok here (and at Lines 205-207, 268-270, 344-346), so any typed error passes — an unresolved-identity failure classified as, say, a network error would keep this green. Assert Category and Subtype on the returned Problem (and Param via errors.As on *errs.ValidationError where applicable), as the neighbouring TestNewSubscriptionClient_UserIdentity_MissingUAT_FailsClosed already does.

As per coding guidelines: "Error-path tests must assert typed metadata through errs.ProblemOf (category, subtype, and param) and verify cause preservation rather than relying only on message substrings."

💚 Proposed assertion shape
-	if _, ok := errs.ProblemOf(err); !ok {
-		t.Fatalf("expected a typed errs.* error, got %T: %v", err, err)
-	}
+	p, ok := errs.ProblemOf(err)
+	if !ok {
+		t.Fatalf("expected a typed errs.* error, got %T: %v", err, err)
+	}
+	if p.Subtype != errs.SubtypeInvalidArgument {
+		t.Errorf("Subtype = %s, want %s", p.Subtype, errs.SubtypeInvalidArgument)
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/subscription_client_test.go` around lines 181 - 192,
Strengthen the error assertions in
TestNewSubscriptionClient_UnresolvedIdentity_FailsClosedRatherThanGuessing and
the corresponding cases around the other referenced tests. Extract the Problem
from errs.ProblemOf(err) and assert the expected Category and Subtype, then use
errors.As to validate the expected Param on *errs.ValidationError where
applicable, following
TestNewSubscriptionClient_UserIdentity_MissingUAT_FailsClosed. Also verify the
underlying cause is preserved instead of accepting any typed error.

Source: Coding guidelines

🧹 Nitpick comments (22)
internal/event/bus/hub_test.go (1)

128-134: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

mustNotReceive's 50ms window can hide a real delivery.

Negative assertions here back the fail-closed drop paths (cross-check, dedup, identity gate). Since Hub.Publish enqueues synchronously before returning, the wait can be replaced with a non-blocking drain — deterministic and faster than a 50ms sleep per call.

♻️ Deterministic negative assertion
 func mustNotReceive(t *testing.T, ch chan interface{}, label string) {
 	t.Helper()
-	if _, _, msg := recvEvent(ch, 50*time.Millisecond); msg != nil {
-		t.Fatalf("%s: should not have received a message, got %#v", label, msg)
-	}
+	// Publish enqueues synchronously, so anything destined for ch is already
+	// there by the time Publish returns.
+	select {
+	case msg := <-ch:
+		t.Fatalf("%s: should not have received a message, got %#v", label, msg)
+	default:
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/bus/hub_test.go` around lines 128 - 134, Update mustNotReceive
to perform a non-blocking receive check after Hub.Publish has synchronously
enqueued any delivery, instead of waiting 50ms through recvEvent. Preserve the
existing failure message and assert that no message is immediately available on
ch.
cmd/event/bus_test.go (1)

74-82: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a cause-preservation assertion (and preserve the cause in the source).

verifyUATBelongsToUser wraps the prove failure as fmt.Errorf("%w: %v", errUATUnverifiable, err) (cmd/event/bus.go line 77), so the original proveErr is flattened into text and errors.Is(err, proveErr) is false. Diagnosing a real user_info failure then loses the typed/wrapped cause.

As per coding guidelines, error-path tests must "verify cause preservation rather than relying only on message substrings."

♻️ Preserve both errors, then assert it

In cmd/event/bus.go:

-		return fmt.Errorf("%w: %v", errUATUnverifiable, err)
+		return fmt.Errorf("%w: %w", errUATUnverifiable, err)

In the test case:

 		{
 			name:         "no stored token + verification failure is rejected (fail-closed)",
 			userOpenID:   "ou_alice",
 			token:        "tok-unknown",
 			stored:       nil,
 			proveErr:     errors.New("user_info API returned HTTP 401"),
 			wantErr:      true,
 			wantSentinel: errUATUnverifiable,
+			wantCause:    true, // assert errors.Is(err, tc.proveErr)
 		},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/event/bus_test.go` around lines 74 - 82, Update verifyUATBelongsToUser so
failures from the user-info verification preserve both errUATUnverifiable and
the original proveErr as wrapped causes rather than formatting proveErr into
plain text. Extend the “no stored token + verification failure is rejected
(fail-closed)” test to assert errors.Is(err, proveErr) in addition to the
existing sentinel assertion.

Source: Coding guidelines

cmd/event/subscription/subscription_test.go (1)

365-375: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Dead continue after t.Fatalf, and a missing subcommand aborts the whole loop.

t.Fatalf never returns, so the continue is unreachable; using t.Errorf + continue also lets the second subcommand still be checked.

♻️ Proposed refactor
 		sub, ok := found[name]
 		if !ok {
-			t.Fatalf("subscription command group missing %q subcommand", name)
+			t.Errorf("subscription command group missing %q subcommand", name)
 			continue
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/event/subscription/subscription_test.go` around lines 365 - 375, In the
subcommand validation loop, remove the unreachable continue after t.Fatalf and
change the missing-subcommand handling so the test records an error without
aborting the loop, allowing both “list” and “get” to be checked. Preserve the
existing risk validation for found subcommands.
cmd/event/subscription/create.go (1)

274-282: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Returning the shared package slice on the false path leaves an aliasing hazard.

createRequiredScopes(false) hands back subscriptionMutationScopes itself, and that value is embedded into createDryRunResult.RequiredScopes. A future append on the result field would write into the shared backing array — the exact bug the true branch already defends against. Returning a copy on both paths makes the guarantee unconditional.

♻️ Make both paths allocate
 func createRequiredScopes(includeResourceData bool) []string {
-	if !includeResourceData {
-		return subscriptionMutationScopes
-	}
 	scopes := make([]string, 0, len(subscriptionMutationScopes)+len(subscriptionEncryptKeyReadScopes))
 	scopes = append(scopes, subscriptionMutationScopes...)
+	if !includeResourceData {
+		return scopes
+	}
 	scopes = append(scopes, subscriptionEncryptKeyReadScopes...)
 	return scopes
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/event/subscription/create.go` around lines 274 - 282, Update
createRequiredScopes so the !includeResourceData path returns a newly allocated
copy of subscriptionMutationScopes rather than the shared package slice.
Preserve the existing true-path combination and ensure both branches return
independently owned slices, preventing later append operations on RequiredScopes
from mutating shared backing storage.
internal/event/consume/refined_test.go (1)

1359-1361: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Two config fixtures write with os.WriteFile inside an internal/ test. Both sites bypass the repository's filesystem abstraction; the shared fix is to route them through internal/vfs.

  • internal/event/consume/refined_test.go#L1359-L1361: replace os.WriteFile(filepath.Join(dir, "config.json"), ...) with vfs.WriteFile(...) and drop the now-unused os import if nothing else needs it.
  • internal/event/consume/refined_test.go#L1435-L1437: apply the same vfs.WriteFile replacement inside runOnce.

As per coding guidelines: "Use internal/vfs filesystem APIs instead of os filesystem APIs." Based on learnings: for any Go test file under internal/, use internal/vfs including fixture/setup helpers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/consume/refined_test.go` around lines 1359 - 1361, Replace
both os.WriteFile calls in internal/event/consume/refined_test.go at lines
1359-1361 and 1435-1437, including the runOnce helper, with vfs.WriteFile using
the existing path and contents; remove the os import if no other references
remain.

Sources: Coding guidelines, Learnings

internal/event/reconcile_test.go (1)

554-566: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the typed metadata on this fail-closed error.

The test only checks err != nil, so it would still pass if the fail-closed branch degraded to an untyped error. Asserting errs.ProblemOf(err)'s category/subtype (SubtypeUnknown, internal) locks the contract.

As per coding guidelines: "Error-path tests must assert typed metadata through errs.ProblemOf (category, subtype, and param) and verify cause preservation rather than relying only on message substrings."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/reconcile_test.go` around lines 554 - 566, Update
TestReconcileExisting_EncryptedBothTrue_NoProberSupplied_FailsClosed to inspect
errs.ProblemOf(err) and assert the fail-closed error’s internal category,
SubtypeUnknown subtype, and expected param metadata; also verify the underlying
cause is preserved. Keep the existing non-nil and no-reuse assertions.

Source: Coding guidelines

internal/event/consume/refined.go (1)

623-637: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use typed error constructors instead of bare fmt.Errorf here.

resolveCurrentProfileIdentity returns a raw fmt.Errorf for the "no current app config" case, and the caller at Line 182 wraps it with another fmt.Errorf. Both eventually land inside a typed errApplyOkHelloFailed, but the inner values carry no category/subtype of their own. A errs.NewValidationError(errs.SubtypeFailedPrecondition, ...) (missing profile) and .WithCause(err) on the caller side keeps the classification intact.

As per coding guidelines: "Use the prescribed typed error constructors for validation, failed preconditions, API failures, network failures, file I/O failures, and unknown lower-layer errors".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/consume/refined.go` around lines 623 - 637, Update
resolveCurrentProfileIdentity to return
errs.NewValidationError(errs.SubtypeFailedPrecondition, ...) when
CurrentAppConfig returns nil instead of using fmt.Errorf. At the caller around
the existing error wrapping, preserve the typed classification by attaching the
returned error with .WithCause(err) rather than embedding it in another
fmt.Errorf.

Source: Coding guidelines

internal/event/reconcile.go (1)

98-109: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

"Mutually exclusive" options aren't actually enforced.

WithDeferredEncryptKeyConfirmation's doc says it is mutually exclusive with WithEncryptKeyProber, but supplying both silently takes the prober branch. Given the whole block is fail-closed by design, rejecting the ambiguous combination the same way the default case does would keep behavior and documentation aligned.

Also applies to: 237-257

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/reconcile.go` around lines 98 - 109, The reconcile
configuration must reject supplying both WithDeferredEncryptKeyConfirmation and
WithEncryptKeyProber instead of silently selecting the prober path. Update the
option validation or requestedIncludeResourceData=true handling in
ReconcileExisting to detect the conflicting flags and route it through the
existing fail-closed internal-error/default behavior.
cmd/event/subscription/list_test.go (1)

176-190: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer errors.Is over message-string equality for the pass-through assertion.

A sentinel makes the "unchanged, unwrapped" contract explicit and survives message rewording.

♻️ Proposed refactor
 func TestListSubscriptions_TransportError_PropagatesTyped(t *testing.T) {
-	fake := &fakeListAPI{err: errors.New("boom: connection reset")}
+	wantErr := errors.New("boom: connection reset")
+	fake := &fakeListAPI{err: wantErr}
 
 	_, err := listSubscriptions(context.Background(), fake, listOpts{})
 	if err == nil {
 		t.Fatal("expected an error, got nil")
 	}
-	if err.Error() != "boom: connection reset" {
+	if !errors.Is(err, wantErr) {
 		// listSubscriptions must pass the error through unchanged — it is
 		// *eventlib.SubscriptionClient's job (already tested) to
 		// wrap it into a typed error; this command layer must not swallow
 		// or double-wrap it.
 		t.Errorf("err = %v, want it passed through unchanged", err)
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/event/subscription/list_test.go` around lines 176 - 190, Update
TestListSubscriptions_TransportError_PropagatesTyped to use a sentinel error for
fakeListAPI and assert the returned error with errors.Is instead of comparing
err.Error() to a message string, preserving the pass-through and unwrapped
contract.
internal/event/bus/identity_test.go (1)

376-381: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Fake panics with index-out-of-range if resolveCurrent is ever called more than twice.

A future change to the gate (e.g. an extra resolve) would fail as a panic rather than a readable assertion. Clamping to the last entry keeps the failure mode legible.

♻️ Proposed refactor
 	resolveCurrent := func() (currentIdentity, error) {
-		id := identities[resolveCalls]
+		idx := resolveCalls
+		if idx >= len(identities) {
+			idx = len(identities) - 1
+		}
+		id := identities[idx]
 		resolveCalls++
 		return id, nil
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/bus/identity_test.go` around lines 376 - 381, Update the
resolveCurrent test helper to clamp resolveCalls to the final identities entry
when resolving beyond the available entries, while preserving sequential
selection for the initial calls. Keep the existing call-count tracking and
returned identity behavior intact, but prevent index-out-of-range panics so
later assertions report the actual failure.
internal/event/protocol/messages_test.go (1)

195-236: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing the omitempty counterpart for Event's v2 fields.

Hello and ConsumerInfo both get a "zero-valued v2 field must not reach the wire" test; Event only gets round-trip + old-frame decode. A legacy consumer parsing strictly would benefit from the same pin.

💚 Proposed test
func TestEvent_V2FieldsOmittedWhenZero(t *testing.T) {
	e := NewEvent("im.message.receive_v1", "e1", "111", 1, json.RawMessage(`{}`))
	data, err := json.Marshal(e)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	for _, key := range []string{
		`"remote_subscription_id"`, `"target_resource"`,
		`"authority"`, `"subscription_event_id"`,
	} {
		if bytes.Contains(data, []byte(key)) {
			t.Errorf("zero-valued v2 Event field leaked onto wire: %s in %s", key, data)
		}
	}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/protocol/messages_test.go` around lines 195 - 236, Add a
TestEvent_V2FieldsOmittedWhenZero test alongside TestEvent_V2FieldsRoundTrip
that marshals an Event created with NewEvent and verifies the zero-valued v2
fields remote_subscription_id, target_resource, authority, and
subscription_event_id are absent from the JSON wire data. Use the existing event
constructors and encoding conventions, and fail with the field name and
serialized data when any key is present.
internal/event/bus/conn.go (1)

258-268: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

degradedReason is a single slot written by three independent producers.

The identity gate (SetBoundConnID/SetDegraded), the lifecycle action (ClearActionDegraded), and the decrypt path all write the same field, so a successful rebind silently erases a decrypt/lifecycle degradation (and vice versa) even though the underlying condition still holds. If status accuracy matters here, consider per-domain reason slots with a derived rollup.

Also applies to: 297-305, 459-469

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/bus/conn.go` around lines 258 - 268, Replace the shared
degradedReason slot in Conn with separate reason fields for identity, lifecycle
action, and decrypt domains. Update SetBoundConnID, SetDegraded,
ClearActionDegraded, and the decrypt-path writers to modify only their
respective slots, then derive the exposed degradation reason from the active
domain reasons so clearing or rebinding one condition does not erase others.
internal/event/bus/conn_test.go (1)

333-343: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Orphaned doc comment: it documents TestConn_IdentityGateState_ConcurrentAccessRace (defined at Line 570) but sits above the Task-17 section header.

Move it down to the function it describes so the section banner reads cleanly.

♻️ Proposed fix
-// TestConn_IdentityGateState_ConcurrentAccessRace exercises every new
-// identity-gate mutator/getter concurrently: in production this state is
-// written from TWO independent goroutines (identity.go's onConnReady, driven
-// by the WS ready/reconnect callback, and Hub.Publish's per-event delivery
-// gate, driven by the source's emit goroutine) and read by a future status
-// command from yet another — hence its own dedicated mutex rather than the
-// zero-lock convention used for the write-once owner fields above. Run with
-// -race (mirrors hub_publish_race_test.go's style for the Hub side).
 // --- Task 17: lifecycle summary state (spec §5.1/§5.5) --------------------

and re-attach it directly above func TestConn_IdentityGateState_ConcurrentAccessRace (Line 570).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/bus/conn_test.go` around lines 333 - 343, Move the multi-line
documentation comment describing TestConn_IdentityGateState_ConcurrentAccessRace
from above the Task-17 lifecycle section to directly above that test function.
Leave the Task-17 section header and TestConn_LifecycleSummary_DefaultEmpty
declaration together without the orphaned comment.
internal/event/bus/hub_consumers_refined_test.go (1)

20-21: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Discarded net.Pipe() peers across the new Hub tests. Both files use conn, _ := net.Pipe() and close only one end, unlike the rest of the package's Hub/Conn tests which keep and close both.

  • internal/event/bus/hub_consumers_refined_test.go#L20-L21: bind the second return value and defer its Close() in all three tests.
  • internal/event/bus/hub_lifecycle_test.go#L19-L20: same for every net.Pipe() call site in this file (Lines 19, 25, 31, 64, 94, 119, 142, 176).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/bus/hub_consumers_refined_test.go` around lines 20 - 21, The
Hub tests discard one peer returned by net.Pipe(), leaving resources unclosed.
In internal/event/bus/hub_consumers_refined_test.go lines 20-21, update all
three tests to retain and defer-close both pipe connections; apply the same
change at every net.Pipe() call site in internal/event/bus/hub_lifecycle_test.go
lines 19, 25, 31, 64, 94, 119, 142, and 176.
internal/event/bus/lifecycle_dispatch_test.go (1)

1104-1111: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Pin the expected outcome instead of accepting both.

The err == nil → t.Log branch makes this assertion vacuous: any behavior passes. Decide the contract for "no subscription client configured" (typed error vs. nil) and assert it directly, keeping the summary-recording check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/bus/lifecycle_dispatch_test.go` around lines 1104 - 1111, The
test around action.Handle must assert the defined contract for a missing
subscription client instead of accepting either outcome. Update the err == nil
branch to require the expected typed error or nil result, while preserving the
LastLifecycleEvent assertion that verifies the summary is recorded.
internal/event/bus/lifecycle_ports.go (1)

56-58: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Prefer a checked type assertion here.

The invariant holds today, but an unchecked assertion in a lifecycle worker goroutine turns any future non-*Conn implementation into a daemon-killing panic; returning an error degrades just that consumer instead.

♻️ Proposed change
 func (g *identityGate) BindConsumer(ctx context.Context, c lifecycle.Conn) error {
-	return g.bindConsumer(ctx, c.(*Conn))
+	conn, ok := c.(*Conn)
+	if !ok {
+		return fmt.Errorf("bind consumer: unexpected lifecycle.Conn type %T", c)
+	}
+	return g.bindConsumer(ctx, conn)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/bus/lifecycle_ports.go` around lines 56 - 58, Update
identityGate.BindConsumer to use a checked type assertion when converting the
lifecycle.Conn argument to *Conn. If the assertion fails, return an appropriate
error instead of panicking; otherwise preserve the existing bindConsumer flow.
internal/event/bus/lifecycle/action.go (1)

187-208: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Unsynchronized post-construction wiring read from the executor goroutine.

gate/newSubClient/encryptKeyRemover are plain fields written by these setters and read by Handle on the executor's worker goroutine. Today all wiring happens in NewBus/SetIdentityProviders/SetSubscriptionClient before Run starts, so it's safe — but nothing enforces it, and Hub.SetCurrentResolver guards the equivalent state with mu (internal/event/bus/hub.go Lines 131-139). Consider atomic.Pointer/mutex, or at least a doc line stating these must be wired before the executor starts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/bus/lifecycle/action.go` around lines 187 - 208, Synchronize
access to the wiring fields used by the executor: gate, newSubClient, and
encryptKeyRemover in SubscriptionAction, including reads from Handle and writes
through SetIdentityGate, SetNewSubscriptionClient, and SetEncryptKeyRemover.
Prefer the existing synchronization approach used by Hub.SetCurrentResolver, or
document and enforce that all wiring completes before the executor starts if
synchronization is intentionally not added.
internal/event/bus/encrypt_key.go (2)

29-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Doc contradicts the declaration.

The comment says "A field on the provider (not a const)", but this is a const. The tunable field is encryptKeyProvider.fetchTimeout; this const is only its default.

♻️ Proposed comment fix
 // defaultEncryptKeyFetchTimeout bounds the ONE Hello-time GetEncryptKey call.
-// A field on the provider (not a const) so tests can shrink it; deliberately
-// not env-configurable — this is control-plane housekeeping, run once per
-// encrypted consumer registration, never a hot path.
+// It only seeds encryptKeyProvider.fetchTimeout, which tests can shrink;
+// deliberately not env-configurable — this is control-plane housekeeping, run
+// once per encrypted consumer registration, never a hot path.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/bus/encrypt_key.go` around lines 29 - 33, Correct the comment
above defaultEncryptKeyFetchTimeout to describe it as the default value for the
provider’s encryptKeyProvider.fetchTimeout field, not as a provider field
itself. Preserve the existing timeout, test-tuning, environment-configuration,
and control-plane context.

100-106: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

gate is written unguarded while newClient is mutex-guarded.

setIdentityGate mutates p.gate with no lock, yet resolveOwnerIdentity reads it on the fetchAndSet path — the exact concurrency window newClientMu exists to cover for newClient. Production wires both before Run(), so this is latent rather than live, but the asymmetry invites a race under -race if a test (or future code) sets the gate concurrently. Either guard both or document that only newClient can be set late.

🔒 Proposed fix
-	newClientMu sync.RWMutex
-	newClient   func(as core.Identity, uat string) (encryptKeyClient, error)
+	// mu guards the post-construction wiring below (gate + newClient), which
+	// may be set slightly before Run() while a test drives fetchAndSet.
+	mu        sync.RWMutex
+	gate      *identityGate
+	newClient func(as core.Identity, uat string) (encryptKeyClient, error)
func (p *encryptKeyProvider) setIdentityGate(g *identityGate) {
	p.mu.Lock()
	p.gate = g
	p.mu.Unlock()
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/bus/encrypt_key.go` around lines 100 - 106, Protect the gate
assignment in setIdentityGate with the same synchronization used when
resolveOwnerIdentity reads p.gate on the fetchAndSet path. Add or reuse the
provider mutex around both the write and corresponding read, preserving the
existing newClientMu handling separately.
internal/event/bus/encrypt_key_test.go (1)

350-360: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Classification table misses errEncryptKeyBotUnsupported.

encryptKeyFailureClass has a resource_data_requires_user branch that no case covers, so a rename/removal of that token would go unnoticed.

💚 Proposed test addition
 		{errEncryptKeyOwnerMismatch, "owner_mismatch"},
+		{errEncryptKeyBotUnsupported, "resource_data_requires_user"},
 		{errEncryptKeyNoGate, "no_identity_gate"},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/bus/encrypt_key_test.go` around lines 350 - 360, Add
errEncryptKeyBotUnsupported with the expected "resource_data_requires_user"
classification to the cases table covering encryptKeyFailureClass. Keep the
existing classifications unchanged so the test explicitly exercises every
branch, including the resource-data-requires-user outcome.
internal/event/registry_test.go (1)

314-332: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Only one of the four new panic branches is covered.

validateRefinedSubscription also panics on empty PathSegment, empty SelectorKey, and out-of-set AuthTypes; none of those are asserted, so removing any of them would keep this test green.

💚 Proposed additional cases
for _, tc := range []struct {
	name string
	tmpl KeyTemplate
}{
	{"empty path segment", KeyTemplate{SelectorKey: "z_id"}},
	{"empty selector key", KeyTemplate{PathSegment: "z-id"}},
	{"bad auth type", KeyTemplate{SelectorKey: "z_id", PathSegment: "z-id", AuthTypes: []string{"nope"}}},
} {
	t.Run(tc.name, func(t *testing.T) {
		def := KeyDefinition{Key: "x.y.bad_v1", EventType: "x.y.bad_v1", ResourceType: "x.y",
			Schema: nativeSchema(), RefinedSubscription: true, KeyTemplates: []KeyTemplate{tc.tmpl}}
		assertPanics(t, func() { RegisterKey(def) })
	})
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/registry_test.go` around lines 314 - 332, Expand
TestRegisterKey_RefinedValidation to assert panics for the remaining
validateRefinedSubscription branches: a template with an empty PathSegment, one
with an empty SelectorKey, and one containing an out-of-set AuthTypes value. Add
separate test cases using invalid refined KeyDefinitions and RegisterKey, while
preserving the existing missing-template and valid-registration checks.
internal/event/registry.go (1)

110-133: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

ResourceType is required for a refined key but not validated.

internal/event/resolve.go Line 143 builds TargetResource: baseDef.ResourceType + "?" + tmpl.SelectorKey + "=" + .... A refined key registered without ResourceType therefore yields a malformed "?chat_id=oc_x" that flows straight into subscription create/update, instead of failing loudly at registration like the other refined-key contract violations here. Template/Example have the same "declared required, silently optional" shape — Example is what the empty-value hint at resolve.go Line 133 renders.

🛡️ Proposed validation
 	if len(def.KeyTemplates) == 0 {
 		panic(fmt.Sprintf("EventKey %s: RefinedSubscription requires non-empty KeyTemplates", def.Key))
 	}
+	if def.ResourceType == "" {
+		panic(fmt.Sprintf("EventKey %s: RefinedSubscription requires a non-empty ResourceType", def.Key))
+	}
 	for i, tmpl := range def.KeyTemplates {
+		if tmpl.Example == "" {
+			panic(fmt.Sprintf("EventKey %s: KeyTemplates[%d] Example must not be empty", def.Key, i))
+		}
 		if tmpl.PathSegment == "" {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/registry.go` around lines 110 - 133, The
validateRefinedSubscription function must also reject refined keys with an empty
ResourceType, Template, or Example. Add registration-time validation for these
required fields, using the existing def.Key context and consistent panic
behavior so malformed refined definitions fail before resolution.

Comment thread events/refined/catalog_test.go Outdated
@@ -0,0 +1,71 @@
package refined

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔴 Critical | ⚡ Quick win

Missing license header — this is the CI license-header failure.

Every other new file in this PR carries the standard two-line header.

🐛 Proposed fix
+// Copyright (c) 2026 Lark Technologies Pte. Ltd.
+// SPDX-License-Identifier: MIT
+
 package refined
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
package refined
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package refined
🧰 Tools
🪛 GitHub Actions: CI / 2064 / 12_license-header.txt

[error] 1-1: license-eye header check failed: file does not have a valid license header.

🪛 GitHub Actions: CI / 2064 / license-header

[error] 1-1: license-eye header check failed: file does not have a valid license header.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@events/refined/catalog_test.go` at line 1, Add the repository’s standard
two-line license header at the top of catalog_test.go, before the package
refined declaration, matching the format used by other new files in the pull
request.

Source: Pipeline failures

Comment thread internal/credential/user_info.go Outdated
Comment on lines +64 to +77
func (p *CredentialProvider) VerifyUATOpenID(ctx context.Context, brand core.LarkBrand, uat string) (string, error) {
if p == nil || p.httpClient == nil {
return "", fmt.Errorf("credential: no HTTP client configured to verify a user access token")
}
hc, err := p.httpClient()
if err != nil {
return "", err
}
info, err := fetchUserInfo(ctx, hc, brand, uat)
if err != nil {
return "", err
}
return info.OpenID, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Does the credential provider's httpClient factory set a Timeout?
rg -nP -C6 'httpClient\s*(:?=|func\(\))' internal/credential | rg -n -C6 'Timeout|http\.Client'
ast-grep run --pattern '&http.Client{$$$}' --lang go internal/credential

Repository: larksuite/cli

Length of output: 4963


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate usages of NewCredentialProvider / NewDefaultTokenProvider =="
rg -n "NewCredentialProvider|NewDefaultTokenProvider" --glob '*.go' .

echo
echo "== credential_provider relevant sections =="
sed -n '1,80p' internal/credential/http_client.go 2>/dev/null || true
sed -n '120,170p' internal/credential/credential_provider.go
sed -n '100,135p' internal/credential/default_provider.go

echo
echo "== ResolveUAT callers and timeout/ctx usages around bus =="
rg -n -C8 "ResolveUAT|verifyUATBelongsToUser|VerifyUATOpenID|handleHello|bindConsumer|context.Background" internal/event internal/credential --glob '*.go'

echo
echo "== client creation sites outside tests =="
rg -n -C5 'http.Client\{|Timeout:\s*time\.' --glob '*.go' --glob '!**/*_test.go' . | sed -n '1,220p'

Repository: larksuite/cli

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== tracked files under internal/credential =="
git ls-files internal/credential | sed -n '1,120p'

echo
echo "== NewCredentialProvider/NewDefaultTokenProvider exact matches =="
rg -n -C4 'NewCredentialProvider\(|NewDefaultTokenProvider\(' --glob '*.go' .

echo
echo "== credential http client / request factory files and timeout mentions =="
git ls-files internal/credential | xargs rg -n -C4 'http\.Client|Timeout|user_info|openid|OpenID\(|VerifyUATOpenID|ResolveToken' || true

echo
echo "== identity gate resolveUAT implementation around live UAT resolution =="
rg -n -C12 'func .*ResolveUAT|verifyUATBelongsToUser|VerifyUATOpenID|fetchUserInfo\(' internal/event internal/credential --glob '*.go'

Repository: larksuite/cli

Length of output: 50370


Don’t rely on the caller’s context to bound UAT verification.

VerifyUATOpenID makes a live HTTP round-trip over ctx; internal/cmdutil/factory_default.go wires the credential provider through deps.HttpClient, and deps.HttpClient is not set by the existing Client construction paths with a guaranteed Timeout. When a context.Background() Hello path reaches this call, a hung user_info request can block that goroutine/pipe until the socket closes. Add an explicit context.WithTimeout around the HTTP call, or establish a guaranteed default http.Client.Timeout at the injection point.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/credential/user_info.go` around lines 64 - 77, The VerifyUATOpenID
flow must impose its own timeout instead of relying solely on the caller’s
context. Add an explicit bounded context around the fetchUserInfo call, or
guarantee a nonzero http.Client.Timeout where deps.HttpClient is injected, while
preserving the existing error propagation and OpenID return behavior.

Comment on lines 556 to 590
// subscriptionDecryptFailureRe matches the SDK's stable whole-envelope
// decrypt-failure error (event/dispatcher/dispatcher.go's
// decryptSubscriptionEnvelope: "subscription event decryption failed
// (subscription_id=%s): ...") and captures the subscription_id. This couples to
// the pinned SDK's error string — the only surface the WS path exposes for a
// per-message decrypt failure (Do returns the error; the ws client logs it via
// this logger). The capture stops at ')' so it never swallows the ": <detail>"
// tail (which carries no key, but also no useful routing info).
var subscriptionDecryptFailureRe = regexp.MustCompile(`subscription event decryption failed \(subscription_id=([^)]*)\)`)

// decryptFailureLogClass is the fixed classification used when a decrypt-failure
// SDK error line is rewritten before it reaches bus.log. The log must not carry
// the crypto/padding detail in the SDK's raw error tail, e.g. "illegal base64
// data", "cipher too short", or "ciphertext is not a multiple of the block
// size", because those details could become a decryption oracle. The token
// mirrors Conn.decryptStateFailed's "decrypt_failed" state.
const decryptFailureLogClass = "decrypt_failed"

// redactDecryptFailureLine detects a decrypt-failure SDK Error line
// (subscriptionDecryptFailureRe) and truncates it right after the
// "(subscription_id=...)" it already carries, discarding everything from
// there to the end of the line — event/dispatcher/dispatcher.go's
// decryptSubscriptionEnvelope always appends the raw crypto/padding detail as
// the LAST component of the wrapped ws-client log line ("...: <detail>"), so
// this is the ONLY place that detail appears on this path — and replacing it
// with a fixed classification instead. Any harmless prefix context the ws
// client adds (message_type/message_id/trace_id, etc.) is kept verbatim. A
// line that is not a decrypt failure is returned byte-for-byte unchanged.
func redactDecryptFailureLine(msg string) string {
loc := subscriptionDecryptFailureRe.FindStringIndex(msg)
if loc == nil {
return msg
}
return msg[:loc[1]] + ": classification=" + decryptFailureLogClass
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

oapi-sdk-go v3 subscription event decryption failed error message dispatcher

💡 Result:

The error message indicating a decryption failure in the Lark (Feishu) OpenAPI SDK for Go (v3) when handling event subscriptions is typically caused by a mismatch or incorrect configuration of the security credentials used by the EventDispatcher [1][2]. When you initialize the EventDispatcher in the Go SDK, you must provide the correct Verification Token and Event Encrypt Key if encryption is enabled in the Lark Developer Console [1][3][4]. If these values are missing, incorrect, or if encryption is enabled on the platform but the SDK is not configured to handle it, decryption will fail [1][2][5]. To resolve this issue, follow these steps: 1. Verify Encryption Settings: Go to the Lark Developer Console, navigate to your app, and check the Events and Callbacks > Encryption Strategy section [1][6][2]. Confirm whether Encryption is enabled [4]. 2. Update Dispatcher Initialization: When creating your EventDispatcher, ensure you are passing the exact Verification Token and Event Encrypt Key from the Developer Console [1][3][4]. Example configuration: handler:= dispatcher.NewEventDispatcher("your_verification_token", "your_event_encrypt_key") If encryption is enabled on the platform, passing empty strings or incorrect values will cause decryption errors [1][3][4]. 3. Check SDK Middleware: Ensure the http.HandleFunc used for receiving events is correctly wrapped with the handler using httpserverext.NewEventHandlerFunc [1][7][3]. This middleware handles the decryption and verification logic automatically [7][2]. 4. Debugging: If the error persists, you can enable debug logs by passing an option to the event handler to see more granular error details, which can help confirm if the issue is related to signature verification or the decryption process itself [3][8]: http.HandleFunc("/webhook/event", httpserverext.NewEventHandlerFunc(handler, larkevent.WithLogLevel(larkcore.LogLevelDebug))) If you have confirmed your keys are correct and are still encountering issues, verify that your server is correctly receiving the full, uncorrupted request body from the Lark platform, as any modification to the payload will cause signature and decryption verification to fail [2][5][9].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
fd -i 'go.mod|go.sum|feishu.go' . | sed 's#^\./##' | sort

echo
echo "Relevant occurrences:"
rg -n "subscriptionDecryptFailureRe|decryptSubscriptionEnvelope|redactDecryptFailureLine|DecryptFailure|decrypt_failed|oapi-sdk-go|larksuite/oapi" -S .

echo
echo "Module versions:"
if [ -f go.mod ]; then awk '/oapi-sdk-go|larksuite\/oapi|github.com\/larksuite\/oapi-sdk-go|go 1\./{print}' go.mod; fi
if [ -f go.sum ]; then rg 'oapi-sdk-go|larksuite/oapi' go.sum | head -20; fi

Repository: larksuite/cli

Length of output: 25442


🌐 Web query:

site:github.com/larksuite/oapi-sdk-go "subscription event decryption failed" v3.7.2 event dispatcher

💡 Result:

The error "subscription event decryption failed" (or related decryption/unmarshal errors) when using the Lark (Feishu) Open Platform Go SDK typically indicates a mismatch between the configuration in your application code and the settings in the Lark Developer Console [1]. To resolve this issue, ensure the following steps are taken: 1. Verify Encryption Configuration: In the Lark Developer Console under "Event Subscriptions," if "Encrypt Key" (Event Encrypt Key) is enabled, it must be correctly provided when initializing the EventDispatcher [1]. If the keys in your code do not match the keys configured in the developer console, decryption will fail [1]. Initialize your dispatcher as follows: dispatcher.NewEventDispatcher("your_verification_token", "your_event_encrypt_key") 2. Verify Token and Keys: Ensure both the "Verification Token" and "Event Encrypt Key" are copied exactly from the "Event Subscriptions" or "Credentials & Basic Info" section of your app in the developer console [1]. Even trailing or leading whitespace in these configuration strings can cause failures. 3. Check Event Protocol Version: Lark uses different event protocols (e.g., v1.0 vs v2.0). Ensure you are registering the correct event handler type (e.g., OnP1... for v1.0, OnP2... for v2.0) [1]. Using the wrong registration method can lead to unexpected unmarshaling errors or failure to process the decrypted payload [1]. 4. Inspect Request/Response Body: If you are getting JSON unmarshal errors (such as "invalid character"), inspect the raw body of the incoming request [2]. This often happens if the encryption key is incorrect (causing the SDK to attempt to decrypt garbage data) or if the server is receiving an unexpected format [2]. You can use the logger provided by the SDK to debug the incoming request: larkevent.WithLogLevel(larkcore.LogLevelDebug) [3][1] If the issue persists, ensure your SDK version is up to date, as issues regarding specific data type unmarshaling (e.g., string vs number for timestamps) have been addressed in later versions [4].

Citations:


🌐 Web query:

github.com larksuite oapi-sdk-go event dispatcher decryptSubscriptionEnvelope "subscription event decryption failed"

💡 Result:

The error "decryptSubscriptionEnvelope subscription event decryption failed" typically indicates a mismatch or misconfiguration in the event encryption settings when using the Lark/Feishu Open Platform SDK for Go [1][2][3]. When the Lark Open Platform pushes events with encryption enabled, the SDK must use the correct Encryption Key (AES Key) to decrypt the payload [4]. If decryption fails, it is almost always due to one of the following reasons: 1. Incorrect Encryption Key: Ensure the "Encrypt Key" configured in your application's "Event Subscriptions" page on the Lark/Feishu Developer Console matches exactly the key being passed to the SDK's dispatcher [4]. 2. Mismatch in Encryption Status: If you have enabled encryption in the Developer Console but are attempting to handle events in the SDK without providing the proper key, or vice-versa, decryption will fail [4]. 3. Handler Configuration: The EventDispatcher must be initialized with the correct verification token and encryption key if your app requires them [5][6]. If you are manually handling events (e.g., using OnCustomizedEvent), ensure you are passing the correct cipher data to the decryption methods [7]. Recommended troubleshooting steps: - Verify Encryption Settings: Go to the Lark/Feishu Developer Console, navigate to your app, and check the "Events & Callbacks" (or "Event Subscription") section. Confirm whether "Encrypt Key" is enabled and match that value against your code [5][4]. - Check SDK Initialization: When creating your event dispatcher, ensure you are passing the correct credentials [6]. For example: dispatcher.NewEventDispatcher("VERIFICATION_TOKEN", "ENCRYPTION_KEY") - Use Debug Logging: Enable debug logging in the SDK to inspect the incoming payload and the error details [5][6]. This will help determine if the payload itself is malformed or if the decryption process is rejecting it due to key mismatch. larkcore.WithLogLevel(larkcore.LogLevelDebug) If you are using the OnCustomizedEvent handler, ensure you are correctly parsing the event and calling handler.DecryptEvent(ctx, cipherEventJsonStr) as demonstrated in the official SDK samples [7].

Citations:


🌐 Web query:

raw GitHub github.com larksuite oapi-sdk-go event dispatcher dispatcher.go decryptSubscriptionEnvelope subscription event decryption failed

💡 Result:

If you encounter a subscription event decryption failure when using the Lark/Feishu Go SDK (larksuite/oapi-sdk-go), it typically indicates a mismatch between the encryption configuration in your application's settings and the values provided to the SDK's EventDispatcher [1][2][3]. Key troubleshooting steps include: 1. Verify Encryption Configuration: Ensure that the Encrypt Key (or Encryption Key) used in your code matches exactly with the one configured in the Lark Developer Console under "Event & Callback > Encryption Strategy" [1][2][3]. 2. Verify Dispatcher Initialization: When initializing the EventDispatcher, both the Verification Token and Encrypt Key must be passed correctly if encryption is enabled in the developer console [2][3]. Example initialization: handler:= dispatcher.NewEventDispatcher("your_verification_token", "your_encrypt_key") 3. Check Request Data: Ensure the raw request body is being passed correctly to the dispatcher. Decryption will fail if the payload is malformed, truncated, or if the wrong part of the HTTP request is being parsed [4][5]. 4. SDK Versioning: Ensure you are using the latest version of the SDK, as older versions may have different handling logic or bugs related to event decryption [6][7]. For further debugging, you can use the low-level event.EventDecrypt function provided by the SDK to isolate whether the failure is in the dispatcher's automatic handling or the decryption logic itself [6][8]. func EventDecrypt(encrypt string, secret string) ([]byte, error) If the decryption continues to fail, verify that no middleware or framework (e.g., Gin, standard net/http) is consuming or modifying the request body before it reaches the SDK handler [4][5].

Citations:


🌐 Web query:

raw.githubusercontent.com/larksuite/oapi-sdk-go v3.7.2 event/dispatcher/dispatcher.go subscription event decryption failed decryption failed

💡 Result:

The "decryption failed" error when using the larksuite/oapi-sdk-go event dispatcher typically indicates a mismatch between the configuration on the Lark Developer Platform and the credentials provided to the SDK [1][2][3]. When encryption is enabled in the Lark Developer Platform (under Event & Callback > Encryption Strategy), the SDK must be initialized with both the correct Verification Token and the correct Encrypt Key [1][2][3]. Troubleshooting steps: 1. Verify Credentials: Ensure that the Verification Token and Encrypt Key passed to dispatcher.NewEventDispatcher(verificationToken, encryptKey) exactly match those provided in the Lark developer console [2][3]. Even a slight mismatch will cause decryption to fail [1]. 2. Confirm Encryption Status: If you have not enabled "Encrypt Key" in the developer backend, ensure you are not passing a key to the dispatcher that the SDK attempts to use for decryption [2][3]. 3. Check SDK Initialization: Ensure the dispatcher is initialized correctly. In older or specific versions, incorrect parameter ordering or empty strings for disabled encryption can lead to unexpected behavior [2][3]. 4. Debugging: Use larkcore.WithLogLevel(larkcore.LogLevelDebug) when registering the event handler to see more detailed logs about the request processing [2][3]. The oapi-sdk-go performs decryption internally using the provided Encrypt Key [1]. If your application receives the event request but fails to decrypt it, the issue is almost certainly due to the Encrypt Key not matching the key used by the Lark server to encrypt the payload [1][2]. If the data itself is malformed or truncated before reaching the SDK, standard base64 decoding errors may also occur, which are often related to improper handling of character encoding or padding in the request body [4][5].

Citations:


🌐 Web query:

"subscription event decryption failed" "larksuite/oapi-sdk-go" "subscription_id"

💡 Result:

If you encounter a "subscription event decryption failed" error when using the Lark/Feishu Open API SDK for Go, it is almost certainly caused by a mismatch between the encryption settings on the Lark Developer Platform and the configuration passed to your SDK's event handler [1][2][3]. When encryption is enabled in the Developer Platform (under Event & Callback > Encryption Strategy), you must provide the correct Encrypt Key and Verification Token when initializing the EventDispatcher [1][4][3]. To resolve this, ensure the following steps are verified: 1. Validate Credentials: Confirm that the Encrypt Key and Verification Token in your code match exactly what is displayed on the Event & Callback > Encryption Strategy page of your application in the Lark Developer Console [2]. 2. Initialize Dispatcher Correctly: When creating your dispatcher, ensure both parameters are provided [1][4][3]: handler:= dispatcher.NewEventDispatcher("your_verification_token", "your_encrypt_key") 3. Check SDK Usage: If you are using the EventDispatcher, ensure you are not attempting to manually decrypt the event payload using a different method while the dispatcher is already configured to handle it automatically [1][5]. The SDK's handler automatically manages decryption if these keys are provided [1][3]. 4. Debugging: Enable debug logging in your event handler to inspect the raw request and identify if the decryption failure occurs during initial parsing or during the decryption phase [1][5][6]: http.HandleFunc("/webhook/event", httpserverext.NewEventHandlerFunc(handler, larkevent.WithLogLevel(larkcore.LogLevelDebug))) If you are using the low-level event.EventDecrypt function directly, ensure you are passing the correct encrypted string extracted from the request headers (often the X-Lark-Content-V1-Encrypted header) and the correct secret key [7][8]. Note that if you prefer to avoid the complexities of manual decryption and webhook configuration, you can use the SDK's persistent connection (WebSocket) mode, which encapsulates authentication and delivers plain text data directly, eliminating the need for decryption logic on your server [6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Feishu source relevant section:"
sed -n '520,650p' internal/event/source/feishu.go

echo
echo "Feishu tests around decrypt failure fixtures:"
sed -n '450,525p' internal/event/source/feishu_test.go

echo
echo "Check whether tests import/pull live SDK sources or just own fixtures:"
rg -n 'TryDecryptEnvelope|decryptSubscriptionEnvelope|subscription event decryption failed|DecryptSubscription|oapi-sdk-go.*v3' internal/event/source/internal/event/source/feishu_test.go internal/event/source/feishu.go || true

Repository: larksuite/cli

Length of output: 10929


Gate decrypt-failure logs on a positive SDK-error match.

redactDecryptFailureLine only passes the raw crypto/padding tail through when the pinned SDK wording no longer matches, creating fail-open drift on a security control. Make SDK upgrades fail loudly (for example, assert the current SDK error text) and require the encrypt-mismatch prefix for decryption failures instead of any error containing encrypt mismatch/invalid token.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/event/source/feishu.go` around lines 556 - 590, Harden
redactDecryptFailureLine and subscriptionDecryptFailureRe so redaction occurs
only for the exact pinned SDK decrypt-failure format with the required
encrypt-mismatch prefix, not merely arbitrary “encrypt mismatch” or “invalid
token” text. Add an explicit assertion of the expected SDK error wording so SDK
changes fail loudly rather than silently passing raw crypto details through.

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown

PR Quality Summary

CI did not complete successfully. Use the failed check links below to decide whether this PR needs a code change or a rerun.

Failed checks

  • license-header — failure — details
  • fast-gate — failure — details
  • results — failure — details

@leave330 leave330 closed this Jul 31, 2026
@leave330
leave330 force-pushed the feat/oapi-event-subscribe branch from c3ee4a9 to cfe76ad Compare July 31, 2026 04:12
@leave330 leave330 changed the title feat(event): refined event subscription support (management, consume, encryption) wip Jul 31, 2026
@leave330
leave330 deleted the feat/oapi-event-subscribe branch July 31, 2026 04:13
@larksuite larksuite deleted a comment from coderabbitai Bot Jul 31, 2026
@larksuite larksuite deleted a comment from coderabbitai Bot Jul 31, 2026
@larksuite larksuite deleted a comment from coderabbitai Bot Jul 31, 2026
@larksuite larksuite deleted a comment from coderabbitai Bot Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XL Architecture-level or global-impact change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant