Repository navigation
Commit cde492d
feat(plugin-auth): a page navigation to the mobile hand-off that has to sign in first goes to the sign-in page (#30010)
domain:server#2 · hotlong · session_c967d7a472b0485c94711bfcb22159c8
Fixes #29997
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:手机 App 登录现在只需打开交接接口这一个地址。没登录、或会话超过 5
分钟的用户在系统浏览器里打开它,服务端直接跳到本部署的 Console 登录页,登录后回到交接接口,再把一次性令牌交回 App。App
自己的探测请求仍然拿到 401。登录页也一起改了:服务端跳转时写明拒绝时间,登录页按这个时间判断会话,不再只看设备时钟。
- **为什么改**:Google 这类不允许内嵌 WebView 的登录方式走这条流程,装好的 App 会用好几个月。原来 App 要自己知道
Console 登录页的路径和它怎么处理 `redirect`,Console 一升级改了其中一个,这些用户就卡在没有退路的页面上,App
也察觉不到。
- **风险与回滚**:目前还没有已发布的 App 在用这条流程,所以不存在受影响的旧版;现有入口(直接打开 Console
登录页)对用它的构建照常可用。这是会话转移流程,但没有新增能力:令牌仍只发给 5 分钟内登录的会话,跨站打开和非法参数仍先回
400,不会跳到登录页;没挂 Console 的部署保持 401。回滚即还原本 PR。
- **先看**:`packages/plugins/plugin-auth/src/one-time-token-handoff.ts` 里的
`refusedSessionAnswer`(什么时候跳、什么时候还回 401)和
`resolveHandoffSignInEntry`(只取路径不换源、怎么判断有没有
Console);`ui/apps/console/src/pages/auth/handoffRedirect.ts` 里的
`isHandoffSessionFresh`(登录页何时放行)。
- **两分钟验证**:`pnpm --filter '@objectstack/plugin-auth^...' build && pnpm
--filter @objectstack/plugin-auth exec vitest run
src/one-time-token-handoff.test.ts`,预期 88 条全部通过;`pnpm --dir ui exec
vitest run
apps/console/src/pages/auth/__tests__/handoffRedirect-29971.test.ts
apps/console/src/pages/auth/__tests__/signInHandoff-29971.test.tsx`,预期
68 条全部通过。
- **需要你做的**:这是会话转移(auth)流程,请维护者审阅并合并本 PR。
## What changes
`GET /api/v1/auth/one-time-token/handoff`, for a valid request whose
session is missing or older than five minutes:
| Request | Answer |
| --- | --- |
| A page navigation | `302` to this deployment's Console sign-in page,
with `redirect` naming the same request and `handoff_refused_at`
carrying the server's time of the refusal |
| Anything else (the app's probe included) | `401 UNAUTHENTICATED`, as
before |
| A page navigation reporting `Sec-Fetch-Site: same-origin`, which is
the sign-in page forwarding | `401`, never another redirect |
| A page navigation when this process does not answer the sign-in page
with `200 text/html`, or when the path would start with `//` | `401`,
logged once at `warn` |
Nothing else on the route moves. `Sec-Fetch-Site: cross-site` is still
judged first, then the closed query set, the registered scheme and the
challenge; an invalid request gets `400` and never reaches the sign-in
page. A token is still minted only for a session under five minutes old
(ruling A on #29714).
The Console's sign-in page (`handoffRedirect.ts`): when the query has
`handoff_refused_at`, a session with a `createdAt` is fresh only if it
was created after that time. The device's clock is not read for it. A
value that is not a whole number of milliseconds is ignored, and a
session with no `createdAt` keeps its existing branch. The links between
`/login` and `/register` carry the mark beside a hand-off `redirect`,
and every other link is byte-identical.
Spec: docblock only. Route ledger: the note. No schema, export or
generated artefact moved.
## Design
**Telling a page navigation from an API call (constraint 1).**
`Sec-Fetch-Dest` decides when a browser sends it: `document` is a
navigation, and `empty` (a `fetch`) or `iframe` is not, whatever
`Accept` says. With no Fetch Metadata, `Accept` naming `text/html`
counts. A request with neither gets the `401`, which is what this route
answered before. So the rule rests on no single header, and if a real
system auth session turned out to send neither, the result is today's
JSON rather than a wrong redirect.
NOT MEASURED: the headers a real `ASWebAuthenticationSession` or Custom
Tabs sends. I cannot measure those here. Measured in headless Chromium
151 opening the route directly: `Sec-Fetch-Dest: document`,
`Sec-Fetch-Site: none`, `Sec-Fetch-Mode: navigate`, an HTML `Accept`.
The WebKit and Firefox builds Playwright has on this machine do not
match its version, so they were not read either.
**Staying on the origin (constraint 3).** `AuthManager.getConsoleUrl`
composes `${canonical origin}${uiBasePath}${path}` (confirmed in
source), so using it as `Location` would move a browser that reached the
deployment on another host. `Location` is root-relative and takes only
the path of that URL, which keeps `uiBasePath` and any base path in
`baseUrl` honoured without a second composer. Measured through a proxy
on another port: the browser stayed on the proxy's origin through the
sign-in and ended at the app. A `baseUrl` ending in `//` or a
`uiBasePath` starting with `//` would compose a path starting with `//`,
which as a root-relative `Location` names another host. Config alone
reaches that and no token rides that redirect, but the route treats it
as no sign-in page: the `401`, one `warn`, and the server is not asked.
**`redirect` (constraint 4)** is rebuilt from the validated parameters
only: `redirect_uri`, `code_challenge`, and `code_challenge_method` if
the request had it. The app's `redirect_uri` stays nested inside
`redirect`, because `redirect_uri` at the top of the page's query, with
`client_id`, is what the page reads as an OAuth authorize replay.
**No Console (constraint 6).** The route asks the composed server,
in-process and per request, for the sign-in page, and redirects only
when that answers `200` with an HTML document. `readShellConfig` and
`anonymousFormDoorServes` in `packages/cli/src/utils/console.ts` already
ask the composed app the same way. A route table would not do: `os
serve` mounts a `503` responder on the same paths when the Console was
not built. So these all keep the `401`: `--no-ui`, a Console not built,
a host that redirects `/_console` to the control plane. So does a
Console served by something in front of this process, which is the cost
of asking in-process: that deployment stays on today's JSON.
**The execution context.** `resolveHandoffSignInEntry` calls
`server.fetch` with `env` and no execution context, and I left it that
way because it cannot matter on any host read:
- `c.executionCtx` is read nowhere in this repo, nor in
`objectstack-ai/cloud` at `f874972e`.
- In Hono itself only `middleware/cache` and
`middleware/method-override` read it unguarded, and neither is imported
here. `.mount()` reads it inside a `try`.
- No real request has one either. `plugin-hono-server` and cloud's
`node-server.ts` serve through `@hono/node-server`, which calls
`fetch(req, { incoming, outgoing })`, so the probe matches the request
it came from. Cloud's Workers front a Node container and use
`ctx.waitUntil` outside Hono. Cloud's Vercel entry calls
`app.fetch(request)` alone.
`readShellConfig` already dispatches the same way, on every Console
document. If a middleware ever did read it unguarded, the probe answers
`500`, the route keeps the `401` and logs `it answers 500` once. A new
pin holds that: it fails closed and says why.
**The loop (constraint 5) is real, and no longer happens.** Measured at
`584f1e43fc`, in headless Chromium; the commit after it changes none of
what these runs read. Every run below used a session really older than
five minutes (each waited 5m10s). The device clock was set 15 minutes
behind where the row says so.
| Run | Hand-off requests | Where it ended |
| --- | --- | --- |
| With Fetch Metadata, device clock behind, old session | 1: `302`
(`none`) | The form, held for the 10 s watched; a sign-in on it ends in
the `302` to the app |
| No Fetch Metadata, device clock behind, old session | 1: `302` | The
form, held for the 10 s watched; a sign-in on it ends in the `302` to
the app |
| With Fetch Metadata, session created after the mark, same URL reopened
once stale | 1: `401` (`same-origin`) | JSON, and it stops |
| No Fetch Metadata, session created after the mark, same URL reopened
once stale | 1: `302` | The form, under the newer mark, held for the 10
s watched |
For scale, the loop itself, measured earlier at `fe2b8b4907` with #29979
merged in, the same-origin stop removed and no page half: **228 hand-off
requests in 20 s, all `302`**.
The session-created-after-the-mark rows are the remaining way the page
forwards a session the route refuses: a URL reopened more than five
minutes later. The same path covers a session the page can read and the
route cannot, such as a bearer token with no cookie, which was read from
the code and not run. "No Fetch Metadata" is Chromium behind a proxy
that strips `sec-fetch-*`, so the server really sees none; no browser
that truly lacks those headers was run.
**Why the mark is a time and not a bare mark.** `SocialSignInButtons`
defaults `callbackURL` to `window.location.href`, so a provider sign-in
comes back to the same URL with the mark still on it. A bare mark would
then hold the form over the brand-new session, which is a dead end for
exactly the Google users this flow exists for. With a time, the new
session's `createdAt` is later and the page forwards. It also spares a
device whose clock is ahead, where a fresh provider session read as too
old. No identity provider exists on this machine, so that return was
measured as a sign-in made elsewhere followed by opening the same URL
again: it ended at the app with the device clock 15 minutes behind and
15 minutes ahead.
On one process the mark and `createdAt` are two readings of one clock.
On several nodes behind one host they are two clocks, so skew can hold
the form until a sign-in makes a later `createdAt`, which clears it. The
docblocks say so.
**The mark across the sign-in pages.** `registerUrl` on `/login`, and on
`/register` both `loginUrl` and the bounce a closed sign-up makes, now
carry the mark beside a hand-off `redirect` (`handoffMarkQuery`). Beside
any other `redirect`, or none, those links are byte-identical, and a
test pins each.
Reading of `RegisterPage` with the mark, from the code and its pins.
`useHandoffRedirect` reads the mark from the URL when it judges a
session, so `/register` behaves as `/login` does. A session not created
after the mark holds the form, whatever the device's clock says. A
sign-up on that form changes the session to one with no `createdAt`,
which is not the one found on arrival, so it forwards. A session created
after the mark, which is a provider returning to this URL, forwards.
Measured at this PR's head, under the real router, with a route-issued
mark, an old session and the device clock 15 minutes behind: `/login`
held, "Sign up" to `/register` with the mark kept and the form held,
"Sign in" back to `/login` with the mark kept and the form held, one
hand-off request throughout; a sign-in then made the second hand-off
request. The showcase is invite-only, so showing "Sign up" took patching
the config response with Playwright interception, and under interception
a `302` to the app's scheme reports `net::ERR_ABORTED`. A control with
interception and no round trip reads the same. The `/register` to
`/login` direction, run with no interception, read that second request
as the `302`.
## Landing order and release
#29979 and #30004 are both on `main`, and the route and the page now
change together in this PR, so nothing has to land first.
This carries in the next release after it merges: 18.4.0 as pending
changesets stand (13 of 62 are `minor`, and these packages version
together). objectstack-ai/mobile#190 can change its entry only once its
server runs that release, because an older server answers the new entry
with JSON.
## Tests
- `one-time-token-handoff.test.ts`: 88 tests on the real `AuthManager`,
the real better-auth plugin and the real route registration. Page
navigation with no session and with an old session gets the `302`; JSON,
no headers, `*/*`, a `fetch` asking for HTML and an iframe get `401`; an
unregistered scheme, a bad challenge, a plain method and an unknown
parameter get `400` with no redirect; cross-site gets `400` first; the
`redirect` it names signs in, comes back and redeems. A path starting
with `//` gets the `401` and one `warn` and asks the server nothing.
`resolveHandoffSignInEntry` and `isPageNavigation` have their own
tables.
- `handoffRedirect-29971.test.ts` and `signInHandoff-29971.test.tsx`
(the existing harness files for this component, so the new pins are
top-level `describe`s there): a session created after
`handoff_refused_at` is fresh and one not created after it is held, each
with the device clock agreeing, an hour behind and an hour ahead; a
value that is not digits is ignored and the five-minute check decides; a
session with no `createdAt` keeps its branch; on the real `LoginPage`
and `RegisterPage`, the form holds, a sign-in or sign-up on it forwards,
and a session created after the mark forwards. Each of the three links
carries the mark beside a hand-off `redirect` and stays byte-identical
beside any other `redirect`, with no mark on the page, and with no
`redirect`.
- Pins: the two existing `401` pins on the route send no navigation
headers, so they stay and their titles now say so. No other pin of this
route's `401` exists in the repo.
- `examples/app-showcase/e2e/mobile-signin-handoff.spec.ts`, two tests.
Signed out, open the hand-off, land on the form on this origin, sign in,
see the `302` to the registered scheme, and redeem the token with the
verifier (and fail without it). And a sign-in made elsewhere that
returns to the same URL forwards on a device whose clock is ahead, which
is what notices the route and the page disagreeing on the parameter's
name.
## Acceptance notes
- **Where the browser test lives, and that it is nightly.** Not
`ui/e2e`: `Live E2E UI` serves the Console from `vite preview` at the
root and the backend there has no built Console to serve, so the route's
redirect has nothing to land on. `examples/app-showcase/e2e` is the one
lane where the backend serves the Console at its real mount. It runs in
Nightly Tiers and a red night files a card, so a Console change that
breaks this shows up the next night, not on that PR. The two tests add
about two seconds to that suite.
- **Scheme registration.** A test-only stack config works only at the
app root (the compile resolves connector spec paths from the config's
directory) and through `composeStacks` (a spread loses the `defineStack`
provenance, and `defineStack` on a built stack reports 71
cross-reference issues), and every smoke spec would then boot a composed
stack. `OS_TRUSTED_APP_SCHEMES` on the `webServer` is one line and the
path a deployment uses.
- **File surface beyond the first claim:**
`examples/app-showcase/playwright.config.ts` (that one line), the doc
comment of `examples/app-showcase/test/showcase-smoke.live.test.ts`, the
page half, and `LoginPage.tsx` and `RegisterPage.tsx` only for the mark
on the links between them, per the claim updates on the card.
- **One path still drops the mark.** `/verify-email-prompt` is given
only `redirect` by both pages, and `VerifyEmailPromptPage`'s "back to
login" builds `/login?redirect=…` from that. So a person sent there
holding an old session, on a device whose clock is behind, who goes back
is judged by the age check. A forward then ends at the route's `401`
with Fetch Metadata, or at one more redirect whose mark holds the form
without it. Read from the code, not run. It stops rather than loops.
Left alone: `VerifyEmailPromptPage.tsx` is outside this card's surface.
- **Cloud does not offer this flow yet:** objectstack-ai/cloud#3549.
-
`packages/qa/dogfood/test/principal-session-console-boot.dogfood.test.ts`
skips on every CI run as far as the workflows read: none sets its
Chromium path, and none builds the Console on that job. Noted, not
filed.
## Verification
Head `3e59db0af0`, one commit on `584f1e43fc`, which the contract review
read. `main` was not merged again: no conflict and nothing red forced
it, so the merge with `origin/main` stays `f68b47cace`.
| Check | Result |
| --- | --- |
| `pnpm --filter @objectstack/plugin-auth exec vitest run` | 220 files;
3864 passed, 10 skipped |
| `pnpm --dir ui exec vitest run apps/console/src/pages/auth/__tests__/`
| 27 files, 217 passed |
| `pnpm --filter @objectstack/plugin-auth run typecheck`, `tsc --noEmit`
on `@objectstack/spec` | exit 0 |
| `pnpm --filter @object-ui/console run type-check` | exit 0 |
| eslint, `--no-inline-config` on the server-side files, `pnpm --dir ui
exec eslint` on the page files | exit 0; 0 errors, and 5 warnings, the
`react-hooks/set-state-in-effect` these pages and this hook already
carried |
| `dispatch-gates --commands` (12 paths, 88 commands) | 87 exit 0, 1
exit 1: `pnpm --dir ui exec vitest run scripts/`, read below |
| `dispatch-gates --ran` | `88 derived famil(ies) accounted for — 88
run, 0 NOT-MEASURED` |
| The five roster gates whose list lives under a touched directory
(`check:meta-url-spelling`, `check:authz-resolver`,
`check:changeset-fixed`, `check:error-code-casing`,
`check:filter-alias-parity`) | exit 0 each |
| `pnpm --filter @objectstack/spec check:generated` | exit 0: all 14
generated artifacts up to date, so the docblock moved none |
| Browser spec, Console from `pnpm console:build` at this head | 2
passed |
**The one non-zero gate.** `pnpm --dir ui exec vitest run scripts/` read
5 failed of 4987 tests on this Mac, none in a file this PR touches.
Three were whole-tree scans hitting the 15 s test timeout, and those
files pass alone with a longer timeout; which ones hit it varies by run.
The other two are in `select-affected-tests-wiring.test.ts` and stop at
`mapfile: command not found`: macOS ships bash 3.2. CI's run on Linux is
the authority for this family: **`Test UI` concluded `SUCCESS` on this
head, all 16 shards**, and the shard logs show those files, and the ones
the previous head's local run timed out on, passing there.
<details>
<summary>Ablations on the browser spec (Chromium 151, headless; each
restored and rebuilt)</summary>
| Mutation | Red |
| --- | --- |
| Route's redirect removed, plugin-auth rebuilt,
`ablation-dist-preflight` confirmed absent from `dist/` | Both: 25 s
timeout at `#login-email`, on the JSON page |
| Page stops recognising the hand-off (`handoffNavigationTarget` returns
null), Console rebuilt | Both: 45 s timeout waiting for the `302` to the
app, which is what a Console upgrade could do |
| Route's parameter renamed, plugin-auth rebuilt,
`ablation-dist-preflight` confirmed in `dist/` | The second: 45 s
timeout, the page held its form. The first stayed green. |
Taken at `584f1e43fc`. After restoring and rebuilding, the spec read 2
passed.
</details>
<details>
<summary>Unit ablations, through scripts/ablation-replace.mjs (each
restored; blob equals HEAD)</summary>
| Mutation | Red |
| --- | --- |
| Page: `refusedAt` branch deleted | 8 |
| Page: reader not wired into the verdict | 3, all on the real
`LoginPage` |
| Page: digit check removed | 5 |
| Page: `handoffMarkQuery` not limited to a hand-off `redirect` | 2 |
| `/login`: "Sign up" without the mark | 1 |
| `/register`: "Sign in" without the mark | 1 |
| `/register`: closed sign-up bounce without the mark | 1 |
| Route: `isPageNavigation` guard deleted | 5 |
| Route: same-origin stop deleted | 1 |
| Route: `//` guard off | 4 |
| Route: `//` guard on the sign-in page only, not the hand-off | 1 |
| Route: served check made `true` | 6 |
| Route: the `302` replaced by the refusal | 8 |
| Route: `Location` given the full URL instead of the path | 6 |
| Route: `handoff_refused_at` deleted | 2 |
The `//` guard and link rows are at `3e59db0af0`, the others at
`584f1e43fc`.
</details>
**Declared narrowing — verification ran UNLOCKED.**
`scripts/pm/os-verify-lock.sh` could not take the shared verify lock on
this host: no usable `flock`. The shared verify lock is declared
Linux-only (`flock` is util-linux, and a stock macOS does not ship it),
so the commands above were run directly, without the lock — a declared
narrowing, not a silent one. No serialization guarantee held for these
runs, nor for any sibling agent in this container while they ran.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(Docblock only.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; eslint on the touched files passes. (CI's
`pnpm lint` and `Lint UI` are the authority.)
- [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: no generated artifact moved.
- [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. (The maintainer brief
above is Chinese by the project's convention.)
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
Body written 2026-11-04T17:22Z.
---
_Generated by [Claude
Code](https://claude.ai/code/session_c967d7a472b0485c94711bfcb22159c8)_
---------
Co-authored-by: Jack Zhuang <50353452+hotlong@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>1 parent b7075d9 commit cde492d
14 files changed
Lines changed: 1145 additions & 75 deletions
File tree
- .changeset
- examples/app-showcase
- e2e
- test
- packages
- plugins/plugin-auth/src
- spec/src/api
- ui/apps/console/src/pages/auth
- __tests__
| 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 | + | |
| 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 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
34 | 40 | | |
35 | 41 | | |
36 | 42 | | |
| |||
58 | 64 | | |
59 | 65 | | |
60 | 66 | | |
| 67 | + | |
61 | 68 | | |
62 | 69 | | |
63 | 70 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
14 | | - | |
15 | | - | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
16 | 17 | | |
17 | 18 | | |
18 | 19 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
82 | 82 | | |
83 | 83 | | |
84 | 84 | | |
85 | | - | |
| 85 | + | |
86 | 86 | | |
87 | 87 | | |
88 | 88 | | |
| |||
4030 | 4030 | | |
4031 | 4031 | | |
4032 | 4032 | | |
4033 | | - | |
4034 | | - | |
4035 | | - | |
| 4033 | + | |
| 4034 | + | |
| 4035 | + | |
| 4036 | + | |
| 4037 | + | |
4036 | 4038 | | |
| 4039 | + | |
4037 | 4040 | | |
4038 | 4041 | | |
4039 | 4042 | | |
| |||
4050 | 4053 | | |
4051 | 4054 | | |
4052 | 4055 | | |
| 4056 | + | |
| 4057 | + | |
| 4058 | + | |
| 4059 | + | |
| 4060 | + | |
| 4061 | + | |
| 4062 | + | |
| 4063 | + | |
| 4064 | + | |
| 4065 | + | |
| 4066 | + | |
| 4067 | + | |
| 4068 | + | |
| 4069 | + | |
| 4070 | + | |
| 4071 | + | |
| 4072 | + | |
| 4073 | + | |
| 4074 | + | |
4053 | 4075 | | |
4054 | 4076 | | |
4055 | 4077 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
289 | 289 | | |
290 | 290 | | |
291 | 291 | | |
292 | | - | |
| 292 | + | |
293 | 293 | | |
294 | 294 | | |
295 | 295 | | |
| |||
0 commit comments