Repository navigation
Commit cb8988e
fix(plugin-auth): store the session client address in RFC 5952 text and write last_activity_at at creation (#27003)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XWvbPrZ7kbgMnvn7DKaifV4)_
Fixes #26638
Clause-②: yes (widening)
Metadata-protocol: none
## 维护者速读
- **改了什么:** 登录时写进 `sys_session` 的两项数据。IP 地址存服务端认定的客户端地址,IPv6 按 RFC 5952
压缩(`::1`、`2001:db8::1`),IPv4 原样;`last_activity_at` 在会话创建时写入,等于
`created_at`。会话列表照旧显示纯文本,界面不动。
- **为什么改:** 本机登录的会话 IP 显示成 `0000:0000:…:0000`,令牌会话的「最后活动时间」是「—」(裁决
D:不加渲染面,修数据)。实测发现那串零不是「未指定地址」:better-auth 把 IPv6 展开并按 /64 掩码后才写库,`::1`
变成全零,`2001:db8::1` 变成 `2001:0db8:0000:…:0000`,丢了地址本身。
- **风险与回滚:** 只影响今后新建的会话,旧行不迁移。IPv4 映射地址(双栈监听下的 IPv4
客户端,`::ffff:203.0.113.7`)按席位裁决存成点分 IPv4 `203.0.113.7`,与 main
上会话列表的显示一致;`last_login_ip` 原来存映射写法,现在也是点分
IPv4。心跳首写从「登录后第一个请求」推迟到「登录满一分钟后的第一个请求」。回滚即还原本 PR。
- **先看:**
`auth-manager-database-hooks.ts`(会话创建钩子取服务端认定的地址);`session-first-activity.ts`(为何是
ObjectQL 钩子而非适配器);`client-address-canonical.ts`(压缩规则)。
- **两分钟验证:** `pnpm --filter @objectstack/plugin-auth exec vitest run
src/session-client-address-capture.test.ts
src/session-first-activity.test.ts`,预期 17 例全绿(加上
`src/client-address-canonical.test.ts` 共 42 例)。
- **需要你做的:** 无,席位落地。
## What changes
Ruling 6455478505 (letter D, the maintainer's): no renderer faces, fix
the data.
1. **The session row stores the client address in RFC 5952 text.** A new
internal module, `client-address-canonical.ts`, holds the one
canonicaliser: `isIPv6` from `node:net`, lowercase hex, no leading
zeros, the longest run of two or more zero groups (the first on a tie)
written `::`, an IPv4-mapped address (`::ffff:0:0/96`) stored as the
dotted IPv4 address it maps (`::ffff:203.0.113.7` → `203.0.113.7`, per
the seat ruling on this PR, comment 6457840890 on #26638), a zone
identifier kept, IPv4 and anything unparseable returned exactly as it
came (never throws, never refuses a sign-in).
2. **The address stored is the one the server established, not
better-auth's derivation of it.** Measured on `origin/main` `912076f8`:
`handleRequest` already hands better-auth the trust rule's answer
(`client-address.ts`, through the platform-owned header in
`client-address-vendor.ts`), but better-auth's `getIP` then re-derives
it with `normalizeIP(ip, { ipv6Subnet: 64 })` and
`internalAdapter.createSession` stores that: every IPv6 address expanded
to eight groups and masked to its /64 network, an IPv4-mapped one
unmapped. That is its rate-limit bucket, not an address: `::1` is stored
as `0000:0000:0000:0000:0000:0000:0000:0000` (the card's "unspecified
address" is loopback's /64), and `2001:db8::1` as
`2001:0db8:0000:0000:0000:0000:0000:0000`. So `session.create.before`
(now always composed, running before the host hook and the ADR-0093 D9
active-organization default) sets `ipAddress` to the canonical form of
the address in the request state (`readRequestClientAddress`), falling
back to better-auth's value, canonicalised, when no address was
established (a programmatic `auth.api.*` call). A host hook that sets
`ipAddress` itself still wins.
3. **Every path that stores a sign-in's address goes through the
canonicaliser:**
- the session row's `ip_address` (`auth-manager-database-hooks.ts`,
`session.create.before`), which every session creation passes: password
and social sign-in, sign-up auto sign-in, the development admin seed,
impersonation and its bearer rotation, the principal-session mint — all
are `internalAdapter.createSession`;
- the `login` / `logout` audit rows, copied from that row
(`auth-session-audit.ts`, unchanged);
- the `login_failed` / `account_locked` fingerprint
(`signInClientFingerprint`, now taking the established address as an
optional second argument, read by both callers in
`auth-manager-sign-in-lockout.ts`), so a refused and a successful
sign-in from one client still carry one text;
- ADR-0069 D7 `sys_user.last_login_ip` (`stampLastLogin`).
4. **`last_activity_at` is written at creation, equal to `created_at`**
(`session-first-activity.ts`). better-auth's session model does not
declare the column, so its `session.create.before` hook cannot set it
(the adapter factory drops undeclared fields). The adapter could, but
measured live on `os dev`: ObjectQL's own `beforeInsert` stamp
(`sys_stamp_audit_insert`, priority 10) replaces the caller's
`created_at` with the insert instant, so an adapter-side copy landed
about 5 ms before the stored `created_at`. The hook is an ObjectQL
`beforeInsert` on `sys_session` at priority 15, after that stamp,
copying the stored value; it is registered in `AuthPlugin.start()`, not
on `kernel:ready`, because the development admin seed (a `kernel:ready`
hook) mints a session too. A `last_activity_at` the insert already
carries is kept. The #25343 heartbeat throttle is untouched.
Not changed: `user_agent` / `ip_address` stay plain text with no
`format`; nothing under `ui/**`, `packages/spec/**` or
`packages/platform-objects`; better-auth's own rate limiter keeps its
/64 key; the ADR-0069 D5 allow-list still judges
`resolveClientAddress`'s answer as it came (canonicalising inside the
rule would change which exact IPv6 entries match, so the canonicaliser
is applied only where an address is stored); existing rows are not
migrated.
## Why
Setup › Sessions printed a localhost session's IP as
`0000:0000:0000:0000:0000:0000:0000:0000` and left Last Activity 「—」 for
a session with no request after sign-in. The maintainer ruled that the
cells stay raw text and the data is fixed.
## Tests
- `client-address-canonical.test.ts` (new): the four forms the card
names (unspecified and loopback in full, a documentation address with a
zero run, IPv4), the RFC 5952 §4 rules one by one, the IPv4-mapped form
unmapped in five spellings (mixed, loopback, hexadecimal, full
uppercase, with a zone), the neighbouring `::ffff:0:0:0/96` prefix and
an embedded IPv4 that are not mapped, a zone identifier, and five values
returned exactly as they came (unparseable, bracketed, with a port,
padded, empty).
- `session-client-address-capture.test.ts` (new): the client-address
suites' real-mount fixture (`AuthPlugin.registerAuthRoutes` on Hono in
front of a real `AuthManager` on the installed better-auth). A refused
then a successful sign-in from `::1`; from a full-form `X-Forwarded-For`
behind a declared proxy; from `::ffff:203.0.113.7`; from `203.0.113.7`.
Each asserts the session row, the `login` and `login_failed` rows and
`last_login_ip`.
- `session-first-activity.test.ts` (new): a real kernel (SQL driver,
`ObjectQLPlugin` with its `created_at` stamp, `AuthPlugin`,
`SecurityPlugin`). The development admin seed's session and a sign-in
from `::1` are each born with `last_activity_at` equal to the stored
`created_at`, the latter with `ip_address` `::1`; the heartbeat's first
write comes only past the minute.
- Updated: two pins that asserted `session.create.before` is absent when
`autoActiveOrganization` is off (`auth-manager.test.ts`,
`auth-session-audit.test.ts`). The hook is now always present; the first
now asserts the behaviour its title names (no active organization is
filled), the second keeps its point (the `login` writer is wired)
without the absence line.
## Acceptance notes
- **The mechanism hypothesis in the dispatch order did not hold, and the
fix follows the ruling's intent instead.** Canonicalising inside
`client-address.ts` changes nothing that is stored: better-auth
re-expands and masks whatever it is handed. The canonicaliser lives in
its own internal module because `client-address.ts` is a published entry
the runtime's inbound rate limiter loads and is kept import-free, and
because the D5 allow-list compares exact IPv6 entries as text. The
ruling's examples (`::1`, `2001:db8::1`) are only reachable by storing
the established address; canonicalising better-auth's masked value would
store `::` and `2001:db8::` (ablation C below measures exactly that).
- **`Clause-②: yes (widening)`**, re-measured against the ruling's `no`:
`signInClientFingerprint` is exported from the package root (`export *`
of `auth-session-audit.ts`) and gains an optional second parameter.
`Metadata-protocol: none`, so the changeset is `patch`.
- **Visible consequences:** an IPv4 client accepted by a dual-stack
listener (`::ffff:203.0.113.7`) reads `203.0.113.7` on every row, as its
session already did on `main` (better-auth unmapped it);
`last_login_ip`, which carried the mapped text on `main`, now carries
the dotted one. The order's line keeping RFC 5952 §5 mixed notation is
superseded by the seat ruling (#26638 comment 6457840890): the
maintainer's ruling keeps IPv4 unchanged. The heartbeat's first write
now comes on the first request more than a minute after sign-in.
- **Live IPv6 not measured:** this container has no IPv6 (a bare Node
listener binds `0.0.0.0`), so the `os dev` probe covers IPv4 and
`last_activity_at`; the `::1` path is covered through the real kernel
and the real mount instead.
- **Branch base:** `origin/main` `912076f8`; `git merge-tree` against
`origin/main` `fd246c22c9` is clean, and no commit on `main` since
touches `packages/plugins/plugin-auth/`.
## Verification
All readings at head `41b24e18e3` unless stated.
<details><summary>Red on origin/main (912076f + the new tests only,
commit 193576f)</summary>
```
× a loopback IPv6 peer is stored as ::1, not as its expanded /64 network
Expected: "::1" Received: "0000:0000:0000:0000:0000:0000:0000:0000"
× a forwarded full-form IPv6 client address is stored compressed, …
Expected: "2001:db8::1" Received: "2001:0db8:0000:0000:0000:0000:0000:0000"
× an IPv4-mapped peer keeps the mixed notation of RFC 5952 §5
Expected: "::ffff:203.0.113.7" Received: "203.0.113.7"
(the case before the seat ruling; re-read at 41b24e1 with the fix files
restored to 912076f, the reworded case is red on last_login_ip alone:
Expected: "203.0.113.7" Received: "::ffff:203.0.113.7" — 6 failed | 11 passed (17))
× the minted row carries last_activity_at equal to created_at (null)
× the heartbeat's first beat still waits out its one-minute throttle
Tests 5 failed | 32 passed (37)
```
</details>
<details><summary>Green: plugin-auth suite and typecheck</summary>
```
pnpm --filter @objectstack/plugin-auth test
Test Files 201 passed (201)
Tests 3837 passed | 10 skipped (3847)
os-verify-lock: VERDICT command-exit 0
pnpm --filter @objectstack/plugin-auth typecheck
check:test-typecheck: OK — 10 file(s) / 88 error(s) / 23 pinned signature(s) held in test-typecheck-debt.json
os-verify-lock: VERDICT command-exit 0
eslint (the 12 changed .ts files at d0cf335, and the 4 touched again at 41b24e1): exit 0
```
</details>
<details><summary>Ablations (scripts/ablation-replace.mjs, each restored
to the HEAD blob with an empty git diff HEAD; the suites import src/, so
no build leg)</summary>
```
A auth-manager-database-hooks.ts: captureSessionAddress skipped
4 failed: session ip "0000:0000:0000:0000:0000:0000:0000:0000" / "2001:0db8:0000:…:0000" / "203.0.113.7",
and the real-kernel "::1" case
C auth-session-audit.ts: established address ignored by the fingerprint
3 failed: login_failed "::" / "2001:db8::" / "203.0.113.7"
B auth-plugin.ts: registerSessionFirstActivity not called
3 failed: seed session, ::1 sign-in and heartbeat cases (last_activity_at null)
(A, B, C read at d0cf335, before the seat ruling.)
D client-address-canonical.ts: the unmap replaced by the §5 mixed notation (at 41b24e1's content)
6 failed: five IPv4-mapped unit cases ("::ffff:203.0.113.7" for "203.0.113.7", …)
and the dual-stack sign-in case — 6 failed | 36 passed (42)
```
</details>
<details><summary>Live os dev probe (showcase, --fresh, sign-in over
127.0.0.1, GET /api/v1/data/sys_session)</summary>
```
ip_address | created_at | last_activity_at | user_agent
'' | 2026-10-28T00:23:18.450Z | 2026-10-28T00:23:18.450Z | '' (development admin seed's session)
'127.0.0.1' | 2026-10-28T00:23:23.337Z | 2026-10-28T00:23:23.337Z | 'curl/8.5.0'
```
The adapter-side first attempt read `last_activity_at` 5 ms before
`created_at` and left the seed's session null; both are what moved the
write into the ObjectQL hook registered in `start()`.
</details>
<details><summary>Derived gates at 41b24e1 — 65 of 65 exit
0</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derives 65 commands; the same 65 are derived
on a local merge with origin/main fd246c2 (merge-tree clean, no
upstream change under plugin-auth). Each was run after a full `pnpm
build` of this head, exit code captured before any pipe:
```
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dev-prereqs.mjs --self-test :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/check-tenant-audit-census.mjs :: exit 0
node scripts/check-tenant-audit-census.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:auth-mount-ledger :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 0
pnpm check:durability-log-level :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:error-status-conformance :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:route-envelope :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
```
`dispatch-gates --ran`: 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN (a
derived zero — every line carries its exit code). Also: `pnpm --filter
@objectstack/plugin-auth test` 201 files / 3837 passed / 10 skipped,
typecheck green, `eslint` on the 12 touched `.ts` files exit 0.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed. (Not touched.)
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5m5t6zNbh2Xwovtric)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent c79eda5 commit cb8988e
13 files changed
Lines changed: 683 additions & 34 deletions
File tree
- .changeset
- packages/plugins/plugin-auth/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
| 59 | + | |
| 60 | + | |
59 | 61 | | |
60 | 62 | | |
61 | 63 | | |
| |||
266 | 268 | | |
267 | 269 | | |
268 | 270 | | |
269 | | - | |
| 271 | + | |
270 | 272 | | |
271 | 273 | | |
272 | 274 | | |
| |||
282 | 284 | | |
283 | 285 | | |
284 | 286 | | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
285 | 312 | | |
286 | 313 | | |
287 | 314 | | |
288 | 315 | | |
289 | 316 | | |
290 | | - | |
| 317 | + | |
291 | 318 | | |
292 | 319 | | |
293 | 320 | | |
| |||
378 | 405 | | |
379 | 406 | | |
380 | 407 | | |
381 | | - | |
| 408 | + | |
382 | 409 | | |
383 | 410 | | |
384 | 411 | | |
| |||
426 | 453 | | |
427 | 454 | | |
428 | 455 | | |
429 | | - | |
430 | | - | |
431 | | - | |
432 | | - | |
| 456 | + | |
433 | 457 | | |
434 | 458 | | |
435 | 459 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
| 50 | + | |
| 51 | + | |
50 | 52 | | |
51 | 53 | | |
52 | 54 | | |
| |||
87 | 89 | | |
88 | 90 | | |
89 | 91 | | |
90 | | - | |
| 92 | + | |
91 | 93 | | |
92 | 94 | | |
93 | 95 | | |
| |||
164 | 166 | | |
165 | 167 | | |
166 | 168 | | |
167 | | - | |
| 169 | + | |
168 | 170 | | |
169 | 171 | | |
170 | 172 | | |
| |||
242 | 244 | | |
243 | 245 | | |
244 | 246 | | |
245 | | - | |
246 | | - | |
247 | | - | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
248 | 252 | | |
249 | 253 | | |
250 | 254 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5078 | 5078 | | |
5079 | 5079 | | |
5080 | 5080 | | |
5081 | | - | |
| 5081 | + | |
| 5082 | + | |
| 5083 | + | |
| 5084 | + | |
5082 | 5085 | | |
5083 | 5086 | | |
5084 | 5087 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
68 | 68 | | |
69 | 69 | | |
70 | 70 | | |
| 71 | + | |
71 | 72 | | |
72 | 73 | | |
73 | 74 | | |
| |||
999 | 1000 | | |
1000 | 1001 | | |
1001 | 1002 | | |
| 1003 | + | |
| 1004 | + | |
| 1005 | + | |
| 1006 | + | |
| 1007 | + | |
| 1008 | + | |
| 1009 | + | |
| 1010 | + | |
| 1011 | + | |
| 1012 | + | |
| 1013 | + | |
| 1014 | + | |
| 1015 | + | |
| 1016 | + | |
1002 | 1017 | | |
1003 | 1018 | | |
1004 | 1019 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
158 | 158 | | |
159 | 159 | | |
160 | 160 | | |
161 | | - | |
| 161 | + | |
162 | 162 | | |
163 | 163 | | |
164 | 164 | | |
165 | | - | |
166 | 165 | | |
167 | 166 | | |
168 | 167 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
70 | 70 | | |
71 | 71 | | |
72 | 72 | | |
73 | | - | |
74 | | - | |
| 73 | + | |
| 74 | + | |
75 | 75 | | |
76 | 76 | | |
77 | 77 | | |
| |||
81 | 81 | | |
82 | 82 | | |
83 | 83 | | |
| 84 | + | |
84 | 85 | | |
85 | 86 | | |
86 | 87 | | |
| |||
205 | 206 | | |
206 | 207 | | |
207 | 208 | | |
208 | | - | |
209 | | - | |
210 | | - | |
211 | | - | |
212 | | - | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
213 | 217 | | |
214 | | - | |
215 | | - | |
| 218 | + | |
| 219 | + | |
216 | 220 | | |
217 | | - | |
| 221 | + | |
218 | 222 | | |
219 | 223 | | |
220 | 224 | | |
221 | | - | |
222 | | - | |
223 | | - | |
224 | | - | |
225 | | - | |
226 | | - | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
227 | 233 | | |
| 234 | + | |
228 | 235 | | |
229 | 236 | | |
230 | 237 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
0 commit comments