Repository navigation
Commit 1d79556
feat(spec,core,runtime,plugin-auth)!: API keys get a lifecycle: 90-day default expiry, one-year ceiling, 90-day inactivity expiry, one-year grace, distinct refusal codes (#23541)
Fixes #23367
Clause-②: yes
Executes ruling `6159880505` (letter A, maintainer 「同意」) on the
re-scoped claim `6173499602`, as amended by `6178205488` (the three
relays below): the key store stays `sys_api_key`, and the API-key
lifecycle is auth configuration. The changeset declares the arm,
`Clause-②: yes (narrowing)`: the published contract widens (four `auth`
settings keys, `IAuthService.resolveApiKeyLifecyclePolicy`, four
registered codes, new `@objectstack/core` exports), and the key doors
narrow.
## What changed
**1. The policy is auth configuration (`@objectstack/service-settings`,
`@objectstack/plugin-auth`, `@objectstack/spec`).**
- `auth.manifest.ts` gains the group **API keys**:
`api_key_default_expiry_days` (90), `api_key_max_expiry_days` (365),
`api_key_inactivity_days` (90, `0` = off), and
`api_key_policy_started_at`.
- `bindAuthSettings` binds the three numbers into `AuthManager`
(`apiKeyDefaultExpiryDays` / `apiKeyMaxExpiryDays` /
`apiKeyInactivityDays`). Only explicit values apply, as for every key
there.
- `AuthManager.resolveApiKeyLifecyclePolicy()` answers the policy with
every number defaulted to the ruling's. The default never outlives the
ceiling. It is declared on the contract as
`IAuthService.resolveApiKeyLifecyclePolicy` (type
`ApiKeyLifecyclePolicy`).
- **The landing is recorded once, in one place.** On
`kernel:bootstrapped`, after the settings engine binds, plugin-auth
writes `auth.api_key_policy_started_at` when no value is stored, and
never overwrites a stored one. The value survives restarts and every
node reads the same instant; it is never the process start. Measured why
`kernel:bootstrapped`: a write inside the `kernel:ready` window is
refused `SETTINGS_ENGINE_NOT_BOUND`. With no settings service the anchor
cannot be kept: said once at `warn`, and the two clocks it drives stay
off. A failed write is said at `error`.
**2. The signing door (`POST /api/v1/keys`, `@objectstack/runtime`).**
It reads the same source as the seam, through the `auth` service.
- No `expires_at` gives now + `defaultExpiryDays`, echoed in the
response.
- An `expires_at` past now + `maxExpiryDays` is refused `400
API_KEY_EXPIRY_TOO_LONG`, naming the latest date allowed. Exactly the
ceiling is accepted, with 60 s of forgiven client clock skew.
- A kernel with no `auth` service signs as before. An `auth` service
that is registered and broken is the store outage (`503`), never a
policy-less mint.
**3. The key-admission seam (`resolveApiKeyAdmission`,
`@objectstack/core`).**
- The lookup is by hash alone, so a revoked row is found and refused
with its own code. A new outcome, `lifecycle`, carries one of three
distinct codes: `API_KEY_REVOKED`, `API_KEY_EXPIRED` (past `expires_at`,
or past the grace of a key with none) and `API_KEY_INACTIVE`. The
located message names the key's prefix, the date and the remedy. An
unknown key is still `none`.
- **Inactivity reads the PRE-stamp value.** The verdict reads the row as
found, and the stamp runs only for a key that verdict admits. A key's
own call can never revive it, and a refusal writes nothing (not a
revocation).
- **Keys with no recorded use** count from the landing. They count from
their own `created_at` only when that is later, i.e. a key signed after
the landing. An earlier `created_at` is never the anchor: no use was
recorded before the policy, so an old signing date says nothing about
use. **Measured deviation from the seat's wording "never
`created_at`":** counting a key signed after the landing from the
landing would refuse it on its very first call once the landing is more
than 90 days old. The later-of rule keeps the seat's intent for every
pre-policy key.
- **Keys with no expiry** get `expires_at` = landing + 365 days, stamped
on the row the first time the key is admitted (the MFA-grace precedent,
`mfa_required_at`). The keys page then names the date, and the 403 after
it names it too.
- `resolveAuthzContext` takes the policy as a **REQUIRED** input,
`apiKeyLifecycle`, beside `userAuthGates` (the #23508 shape). It raises
a `lifecycle` verdict as the branded `ApiKeyLifecycleRefusalError`
(`403`, code, located message), never falls through to the session, and
logs one operator line.
**4. `last_used_at` is written, and its description is now true.** The
field read "Automatically updated on each API call"
(`sys-api-key.object.ts`), and nothing in this repository wrote it. The
seat carried this as the dev's finding on the claim. The seam now stamps
it on an admitted or held call, at most once an hour per key
(`API_KEY_LAST_USED_STAMP_INTERVAL_MS`), through
`ql.update('sys_api_key', { id, ...fields })` in the system context. A
held key is stamped too: ruling C′ item 4 serves it again when its owner
remediates, which an idle clock run out during the hold would defeat.
- **Measured against the durability log-level rule.** A failed stamp
never fails the request, because the key was judged on the row as found.
It is a durability degradation all the same: the field claims each use,
and an unrecorded use lets a key in daily use read as idle. So it is
said at `error`, with the consequence and the fix, **once per data
engine** (a `WeakSet` latch), never once per request.
- The DB-held throttle needs no in-memory state, survives restarts and
is multi-node safe; a race costs one duplicate write. The description
now reads "Recorded when the key is used, at most once an hour…". The
`en` bundle was regenerated; `zh-CN`, `ja-JP` and `es-ES` were
translated by hand.
**5. Every call site that builds `ResolveAuthzInput` passes the source**
(`apiKeyLifecyclePolicySourceOf(authService)`). They are the 8 faces
plus the two test seams; the test seams pass an explicit `undefined` (no
auth service) or the real source (dogfood `armed.ts`). The storage gate
reader's direct `resolveApiKeyAdmission` call passes it too. The
refusal's answer on each face:
| Face | Answer to a revoked / expired / inactive key |
|:--|:--|
| REST data API (`rest-server.ts`) | `403` + code + located message.
`computeExecCtx` resolves no context, keeps the refusal beside the
request (a `WeakMap`), and `enforceAuth` answers it. Every other door
keeps its fail-closed answer. |
| MCP stdio (`mcp/src/plugin.ts`) | per call: the thrown `403` envelope;
at start: refuses to start, naming the code |
| datasource admin (`admin-routes.ts`) | `403` + code (re-raised ahead
of the routes' `try`; the host's thrown-envelope render answers it) |
| share links (`sharing-plugin.ts`) | `403` + code (the family door
relays a throw that declares its status) |
| marketplace install-local (`marketplace-install-local-plugin.ts`) |
`403` + code (raw-app routes; the host's `onError` envelope render) |
| runtime dispatcher (`http-dispatcher.ts` identity step), incl. MCP
HTTP | `403` + code (re-raised by brand beside the store outage; the
transport's `errorResponseBase` renders the declared status and
registered code) |
| settings (`settings-service-plugin.ts` → `settings-routes.ts`) | `403`
+ code (the resolver re-raises the brand; the family door relays it
beside `relayAuthzStoreOutage`) |
| storage download (`storage-service-plugin.ts` → `storage-routes.ts`) |
`403` + code (the authorizer re-raises the brand; the download gate
relays it beside the outage relay) |
**6. Codes.** All four are registered in `ERROR_CODE_LEDGER` and
`content/docs/api/error-catalog.mdx`. `API_KEY_EXPIRED`,
`API_KEY_INACTIVE` and `API_KEY_REVOKED` sit under `@objectstack/core`;
`API_KEY_EXPIRY_TOO_LONG` sits under `@objectstack/runtime`.
`EXPIRED_TOKEN` was judged and not reused. Its catalog meaning is a JWT
or session token whose remedy is a refresh, and its retry is
`retry_immediate` (after refreshing); a key has no refresh, and its
remedy is a new key. Each code is declared as its wire pair (`{ code,
status }`), so `check:error-status-conformance` derives a producer for
it.
**7. Docs.** `content/docs/ai/connect-mcp.mdx` gains **Key lifetime,
inactivity and rotation**: the policy, the codes, rotation without a
gap, and which faces answer the codes. `content/docs/api/index.mdx`
points to it, and the stdio paragraph names inactive keys. Cloud's
agent-connection page is cloud's (the seat's pointer on cloud#2868).
## `OS_CLOUD_API_KEY`
Read first, as ruling item 4 orders. In this repository it is the
runtime's **outbound** bearer to the control plane
(`cloud-connection-plugin.ts:166`,
`marketplace-install-local-plugin.ts`, the marketplace fetch). It is
never admitted by `resolveApiKeyAdmission`, and
`cloud-connection-plugin.ts` records that the `sys_api_key`-backed data
API rejects it with 401. It is **not a `sys_api_key` row: untouched**,
and no exemption question arises. Its second use, the CLI publish
`--token` env fallback, carries whatever key the user sets; a personal
`osk_` key there is an ordinary key on the cloud runtime. **Cloud's own
verification of the variable: NOT MEASURED** (no cloud checkout in this
container).
## The three relays (claim amendment `6178205488`)
The seat widened the claim by three files so ruling item 3's codes reach
every face. Each relay is matched by the brand
(`isApiKeyLifecycleRefusalError`); every other throw keeps today's
answer, and an unknown key keeps the generic 401.
- **runtime dispatcher**: `packages/runtime/src/http-dispatcher.ts`, the
identity step's `catch` only: `if (isApiKeyLifecycleRefusalError(err))
throw err;` ahead of `rethrowAuthzStoreUnavailable(err)`, plus the name
in the file's `@objectstack/core` import. `dispatch()` leaves with the
refusal, and `dispatcher-plugin`'s `errorResponseBase` answers its `403`
and code. **This includes the MCP HTTP endpoint** (`/api/v1/mcp`) and
the declarative-endpoint fallback, which renders through the same
`errorResponse`.
- **settings**: `settings-routes.ts` gains `relayApiKeyLifecycleRefusal`
beside `relayAuthzStoreOutage`, called from the same family-door
`catch`; `settings-service-plugin.ts`'s resolver re-raises the brand
instead of returning the fail-closed `{ enforced: true }`. Two doc
comments in the door now name the relay.
- **storage**: `storage-routes.ts`, the download gate's `catch`
(`authorizeDownload`, shared by the URL and redirect routes): the brand
is relayed beside the outage. `storage-service-plugin.ts`'s
`buildFileReadAuthorizer` re-raises it instead of returning
`'unauthenticated'`.
- **The storage gate catch (`refuseHeldSession`) is not edited**,
because no brand can reach it. Its reader (`buildAuthGateSessionReader`)
calls `resolveApiKeyAdmission`, which returns a `lifecycle` outcome and
never throws one. The reader answers that outcome with no posture, as it
answers `refused`, and leaves the key to the route's own identity
resolution. On a download, that is the authorizer above, which relays
the code. The upload routes read the session cookie alone
(`buildAuthSessionResolver`) and never admit a key, so no key face is
left behind. A relay there would be unreachable code.
**The CI red at `d18cfe5488`** (Test Core 1/6, `packages/spec`
`test:repo`) is fixed in `469acd6a41`. Reproduced locally:
`src/api/error-catalog-docs.test.ts` › "the advertised code count
matches the wire face" (`expected 52 to be 56`). The catalog page states
how many codes reach the wire, and that claim is pinned to
`deriveWireFace()`. The four codes registered here moved the face from
52 to 56, and the page still said 52. The page now says 56. No test was
touched.
## Tests
Core: `api-key-lifecycle.test.ts` (20). Runtime:
`api-key-lifecycle.pin.test.ts` (14). It covers the signing door; on the
dispatcher, `dispatch()` raising `403 API_KEY_REVOKED`, and `403
API_KEY_EXPIRED` with a valid cookie beside the key, nothing minted; an
unbranded throw at the identity step spelling the same code and status,
still answered the generic `401 UNAUTHENTICATED`; an unknown key, `401`;
a live control, `201`; and, on the wire through the mounted `POST
/api/v1/mcp` route, `403 API_KEY_REVOKED` in a schema-valid envelope and
`403 API_KEY_INACTIVE`. Settings:
`settings-routes.api-key-lifecycle.pin.test.ts` (5): revoked on every
mounted route, expired, inactive, unknown key `401 UNAUTHENTICATED`, and
a live control. Storage: `storage-routes.api-key-lifecycle.pin.test.ts`
(4): revoked and expired give `403` with the code and nothing minted, an
unknown key gives `401 AUTH_REQUIRED`, and a live key gets its signed
URL. REST: `rest-api-key-lifecycle.pin.test.ts` (4). plugin-auth: four
policy cases in `auth-plugin.test.ts`. Dogfood:
`api-key-lifecycle.dogfood.test.ts` (3, the showcase stack end to end).
It proves the landing is recorded in the `auth` settings, the 90-day
default, the `400 API_KEY_EXPIRY_TOO_LONG`, and that **one admitted call
really writes `last_used_at` through the real engine**. It also proves a
pre-policy key gets its grace stamped. The three existing dogfood key
probes now read a refused key as `401`, or as `403` with code
`API_KEY_REVOKED` and only that code.
Pins against the dispatch's list: no `expires_at` gives +90 d; +365 d
accepted, +366 d refused with its code; idle > 90 d refused
`API_KEY_INACTIVE`, the row not written, and re-issue works. A no-expiry
key is served until the grace end and refused after it, naming the date;
the date is on `expires_at` from first use. Revoked, expired and
inactive give three codes. Each number comes from the source, so a
changed setting changes the verdict. The stamp pin: one admitted call
writes `last_used_at`, and the throttle holds. The pre-stamp pin and the
NULL-anchor pin are listed above. The missed-face pin is now the
dispatcher's unbranded-throw case: a face that misses the brand fails
closed to its generic 401.
Ablations (`scripts/ablation-replace.mjs`, run against committed HEAD
`204fb33318`; the core suite imports `./api-key.js` from source, so no
build stands between the mutation and the run):
- **Inactivity check.** `nowMs - idle.ms > policy.inactivityDays *
DAY_MS` was replaced by `false`. Anchor x1 → x0, blob `2c7db74f5e18` →
`e3f45c2db200`: **5 failed / 15 passed** (three codes, pre-stamp,
landing anchor, signing anchor, source number). Restored: blob == HEAD
`2c7db74f5e18`, `git diff HEAD` empty.
- **The stamp.** The `ql.update('sys_api_key', …)` line was replaced by
`void patch;`. Anchor x1 → x0, blob → `48112ae9cdca`: **5 failed / 15
passed** (grace stamp, one call writes, throttle, held key stamped,
said-once). Restored: blob == HEAD, `git diff HEAD` empty.
- **The dispatcher relay** (at committed HEAD `e50fb94b57`; the pin
imports `./http-dispatcher.js` and `./dispatcher-plugin.js` from source,
so no build stands between). `if (isApiKeyLifecycleRefusalError(err))
throw err;` was replaced by `void 0;`. Anchor x1 → x0, blob
`22d1fe62b338` → `e0459ffb1954`: **4 failed / 10 passed** (the two
`dispatch()` codes and the two MCP HTTP wire codes). The
unbranded-throw, unknown-key and live controls stayed green. Restored:
blob == HEAD `22d1fe62b338`, `git diff HEAD` empty.
## Local verification (head `3c2b4152f9`)
This round touched runtime, service-settings, service-storage, the
changeset and two docs pages. The readings for the packages it did not
touch are the previous round's, at `d18cfe5488` after merging
`origin/main` (`989aa483fd`). Those were: full suites green for core
(105 files / 2544), plugin-auth (140 / 2819), mcp (37 / 420),
service-datasource (45 / 838), plugin-sharing (46 / 1061) and
cloud-connection (46 / 555); rest, the 30 key/resolver/`enforceAuth`
files (474); typecheck exit 0 for spec, core, rest, mcp, plugin-auth,
plugin-sharing, service-datasource, cloud-connection, plugin-security,
cli and dogfood; and the CLI integration file (9).
- **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 128 families on this head. All 128
ran with their exit codes captured before any pipe. 125 exited 0 on the
first pass. `check:skill-examples`, `check:dual-build-cjs-loads` and
`check:i18n` exited 3 (PREREQUISITE NOT MET: packages not yet built, so
nothing was measured), then exited 0 when re-run on the built workspace.
`--ran`: **128 derived, 128 run, 0 NOT-MEASURED, 0 UNRUN** (a derived
zero: every family recorded an exit code). Local-only:
`check:dev-prereqs` and `check-nul-bytes` exit 0.
- **Typecheck** (exit 0): runtime (`tsc` plus `check:test-typecheck`),
service-settings (`tsc`; its file list includes the new pin, counted
with `--listFiles`) and service-storage (`tsc`, `tsconfig.scripts.json`,
`check:test-typecheck`).
- **Tests:**
- full suites: service-settings (47 files / 831) and service-storage (57
/ 991);
- runtime: the 76 test files that name the key, the resolver or the
identity step (2324 passed, 18 skipped). That set comes from a `git
grep`, which is blind to a test that reaches the identity step without
naming it; CI runs the full suite;
- spec, the `repo` project in full (`pnpm --filter @objectstack/spec run
test:repo`): **61 files / 1033 tests passed**, including
`error-catalog-docs.test.ts`, the file red at `d18cfe5488`;
- dogfood, on the showcase stack: the four key tests, the lifecycle test
and `showcase-mcp-http-identity` (5 files / 29 passed).
## Acceptance notes
- **Change size: 81 files, +2741 / −178 = 2919 changed lines** (against
merge-base `989aa483fd`), under the 3000-line class.
- `ApiKeyLifecycleRefusalError` is a throw rather than an envelope
field, for the reason `authz-store-unavailable.ts` records. A face that
misses a throw fails closed by default; a face that misses a field
answers the old quiet value.
- `API_KEY_LEGACY_GRACE_DAYS` (365) is a constant, not a fifth setting.
The grace is a one-time transition stamped onto each key, so a setting
could not move a date already stamped. Moving the landing
(`api_key_policy_started_at`) moves it for keys not yet stamped.
- The amendment's three files each gained one name in their
`@objectstack/core` import (`isApiKeyLifecycleRefusalError`), which the
relay needs. `settings-routes.ts`'s two doc comments that list what the
door relays now name the new relay. The changeset, `error-catalog.mdx`
and `connect-mcp.mdx` now list every face instead of naming three as
answering the generic 401.
- `packages/core/src/security/index.ts` gained the new exports (the
barrel of the claimed `api-key.ts`).
`scripts/engine-double-contract.pinned.json` records the new pinned
double (`--write`, as the gate prescribes).
- `packages/objectql/src/internal-fields.test.ts` still describes the
verifier's lookup as `{ key, revoked: false }`. The test is true (that
query still works), but it is no longer the seam's query. Noted, not
edited: it is outside the surface. Carrier: none.
---
_Generated by [Claude
Code](https://claude.ai/code/session_0121ooBYRmS8ziFXm9ai5YHm)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 8226755 commit 1d79556
81 files changed
Lines changed: 2741 additions & 178 deletions
File tree
- .changeset
- content/docs
- ai
- api
- references/api
- packages
- cli/test
- cloud-connection/src
- core/src/security
- mcp/src
- platform-objects/src
- apps/translations
- identity
- plugins
- plugin-auth/src
- plugin-security/src
- plugin-sharing/src
- qa/dogfood/test
- rest/src
- runtime/src
- domains
- security
- services
- service-datasource/src
- service-settings/src
- manifests
- translations
- service-storage/src
- spec
- api-surface
- export-origins
- src
- api
- contracts
- scripts
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
121 | 121 | | |
122 | 122 | | |
123 | 123 | | |
124 | | - | |
| 124 | + | |
| 125 | + | |
125 | 126 | | |
126 | 127 | | |
127 | 128 | | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
128 | 177 | | |
129 | 178 | | |
130 | 179 | | |
| |||
159 | 208 | | |
160 | 209 | | |
161 | 210 | | |
162 | | - | |
163 | | - | |
| 211 | + | |
| 212 | + | |
164 | 213 | | |
165 | 214 | | |
166 | 215 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
7 | | - | |
| 7 | + | |
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
| |||
81 | 81 | | |
82 | 82 | | |
83 | 83 | | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
84 | 89 | | |
85 | 90 | | |
86 | 91 | | |
| |||
429 | 434 | | |
430 | 435 | | |
431 | 436 | | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
432 | 454 | | |
433 | 455 | | |
434 | 456 | | |
| |||
942 | 964 | | |
943 | 965 | | |
944 | 966 | | |
945 | | - | |
| 967 | + | |
946 | 968 | | |
947 | 969 | | |
948 | 970 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
69 | 69 | | |
70 | 70 | | |
71 | 71 | | |
72 | | - | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
73 | 77 | | |
74 | 78 | | |
75 | 79 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | | - | |
| 31 | + | |
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| |||
96 | 96 | | |
97 | 97 | | |
98 | 98 | | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
99 | 103 | | |
100 | 104 | | |
101 | 105 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
263 | 263 | | |
264 | 264 | | |
265 | 265 | | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
266 | 270 | | |
267 | 271 | | |
268 | 272 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
99 | 99 | | |
100 | 100 | | |
101 | 101 | | |
| 102 | + | |
102 | 103 | | |
103 | 104 | | |
104 | 105 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
120 | 120 | | |
121 | 121 | | |
122 | 122 | | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
123 | 126 | | |
124 | 127 | | |
125 | 128 | | |
| |||
3366 | 3369 | | |
3367 | 3370 | | |
3368 | 3371 | | |
| 3372 | + | |
| 3373 | + | |
3369 | 3374 | | |
3370 | 3375 | | |
3371 | 3376 | | |
| |||
3391 | 3396 | | |
3392 | 3397 | | |
3393 | 3398 | | |
| 3399 | + | |
| 3400 | + | |
| 3401 | + | |
| 3402 | + | |
| 3403 | + | |
3394 | 3404 | | |
3395 | 3405 | | |
3396 | 3406 | | |
| |||
0 commit comments