sqlite-web 0.7.2
gitea.db
package_version
Create
Query
access
access_token
action
action_artifact
action_run
action_run_index
action_run_job
action_runner
action_runner_token
action_schedule
action_schedule_spec
action_task
action_task_output
action_task_step
action_tasks_version
action_variable
app_state
attachment
auth_token
badge
branch
collaboration
comment
commit_status
commit_status_index
commit_status_summary
commit_sync_log
commit_sync_status
dbfs_data
dbfs_meta
deploy_key
email_address
email_hash
external_login_user
follow
gpg_key
gpg_key_import
hook_task
issue
issue_assignees
issue_content_history
issue_dependency
issue_index
issue_label
issue_pin
issue_user
issue_watch
label
language_stat
lfs_lock
lfs_meta_object
login_source
milestone
mirror
notice
notification
oauth2_application
oauth2_authorization_code
oauth2_grant
org_user
package
package_blob
package_blob_upload
package_cleanup_rule
package_file
package_property
package_version
project
project_board
project_issue
protected_branch
protected_tag
public_key
pull_auto_merge
pull_request
push_mirror
reaction
release
renamed_branch
repo_archiver
repo_hidden_file
repo_indexer_status
repo_license
repo_redirect
repo_topic
repo_transfer
repo_unit
repository
review
review_state
secret
session
sqlite_sequence
star
stopwatch
system_setting
task
team
team_invite
team_repo
team_unit
team_user
topic
tracked_time
two_factor
upload
user
user_badge
user_blocking
user_open_id
user_redirect
user_setting
version
watch
webauthn_credential
webhook
Toggle helper tables
Structure
Content
Query
Insert
Drop
Import
Export
Update row 26 in package_version
id
Primary key.
INTEGER NOT NULL
package_id
INTEGER NOT NULL
creator_id
INTEGER NOT NULL (default 0
version
1.0.0-rc.6
TEXT NOT NULL
lower_version
1.0.0-rc.6
TEXT NOT NULL
created_unix
INTEGER NOT NULL
is_internal
INTEGER NOT NULL (default 0
metadata_json
{"scope":"@juhai","name":"client-fact","description":"可信事实受理客户端、事务 Outbox 与幂等 Inbox;Kafka 运输不影响领域契约","license":"UNLICENSED","dependencies":{"@prisma/client":"6.19.3"},"development_dependencies":{"@types/node":"22.19.20","typescript":"5.9.3","vitest":"2.1.9"},"peer_dependencies":{"@juhai/kernel":"0.22.0"},"readme":"# @juhai/client-fact\n\n事实干线(M5)的消费者 SDK:Outbox 投递 + Inbox 幂等/版本/Gap/DLQ 判定 + 可信回查。\n上层只允许 exact pin(`check:pins`),判定语义只在本包,**消费者不得复刻第二份**。\n\n本文记录的是**真实上层接入时踩过的坑**(数字员工基座 `os-employment-projection`,回灌台账 U-29 / U-31 / U-34)。\n平台自己的主线 e2e 用合成宿主表,这些坑在平台侧照不出来。\n\n---\n\n## 1. `baseUrl` 必须包含 `/api` 前缀(U-34)\n\n平台 NestJS 应用 `setGlobalPrefix(\"api\")`,而本包用**相对 URL** 解析端点:\n\n```ts\nnew URL(\"./v1/facts/intents\", baseUrl) // createFactPublisher 内部\n```\n\n因此:\n\n| baseUrl | 实际请求 | 结果 |\n| --- | --- | --- |\n| `https://host/api` | `https://host/api/v1/facts/intents` | ✅ 正确 |\n| `https://host` | `https://host/v1/facts/intents` | ❌ 404 |\n| `https://host/api/` | `https://host/api/v1/facts/intents` | ✅(结尾斜杠会被补齐) |\n\n**为什么这条必须写在契约里**:少写 `/api` 不会显式报错,而是让**所有**回查返回 404。消费者若把 404\n当成\"事实不存在\"交给 `FactInbox`,SDK 会判 `UNTRUSTED_FACT` → 计入毒消息 → 三次进死信。\n一个配置漏字母会把整条摄入变成全量死信,而日志读起来像是\"平台在篡改事实\"。\n\n**消费者必须区分三种情况,不要混进毒消息计数**:\n\n| 情况 | 应有处置 |\n| --- | --- |\n| 回查端点返回 404 | 该 fact 确实不存在 → 交 SDK 判 `UNTRUSTED_FACT`(正当的毒消息) |\n| 回查不可达 / 5xx / 超时 | **抛错**,保留 broker offset 等待恢复;不要返回 null |\n| 未配置回查地址 | **失败关闭**:每次调用都拒绝,绝不\"跳过校验\" |\n\n平台 e2e 里 `baseUrl = http://127.0.0.1:${port}/api` 曾是这条约定唯一的载体(一行测试赋值)。\n\n## 2. 迁移模板只给表,授权与策略绑定由消费者补齐(U-31)\n\n`migrations/001_outbox_inbox.sql` 建五张表并对每张表 `ENABLE + FORCE ROW LEVEL SECURITY`,\n策略为:\n\n```sql\nCREATE POLICY tenant_isolation ON %I\n USING (tenant_id = current_setting('app.current_tenant_id', true))\n WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true));\n```\n\n**模板的两个边界,照抄前必须知道**:\n\n1. **策略没有 `TO \u003crole\u003e` 子句** ⇒ 对 PUBLIC 生效。消费者若有 system / 后台角色需要跨租户扫描\n (补 gap、重放、运维对账),它同样会被这条���略挡住——除非该角色 `BYPASSRLS`,\n 而 `FORCE ROW LEVEL SECURITY` 下表属主也不豁免。\n2. **模板不含任何 `GRANT`**。消费者库若用独立的低权限运行角色(而不是表属主连接),\n 照抄模板后该角色对这五张表**没有任何权限**,摄入会直接失败。\n\n**契约**:表 DDL 逐字照用(列名与主键是 SDK 的 SQL 依赖,改了就对不上);\n`GRANT` 与策略的角色绑定**由消费者按自身角色模型补齐**,作为自己的增量迁移。\n参考实现见数字员工基座的 `20260928010000_platform_client_fact_inbox`(tenant / system 两角色 + 逐表显式 GRANT)。\n\n## 3. 处理结果与 rc.3 的行为变更(U-29)\n\n`FactInbox.handle()` 的返回值:\n\n| 返回 | 含义 |\n| --- | --- |\n| `APPLIED` | 首次应用,投影已写 |\n| `DUPLICATE` | 同 `fact_id` 重投,单次效果(digest 不一致则抛 `PROCESSED_FACT_CONFLICT`) |\n| `STALE` | 版本 ≤ 水位,不写投影 |\n| `GAP` | `previous_version ≠ 水位`,落 gap 缓冲等补齐 |\n\n\u003e **`1.0.0-rc.3` 起的行为变更**:`STALE` 之前**不可达**——版本落后时本包抛\n\u003e `FactRejected(\"UNPROCESSED_VERSION_BEHIND_WATERMARK\")`,而 `handle` 把任何 `FactRejected`\n\u003e 计入 attempts,于是至少一次投递下的**正常迟到重投**三次即被打进死信,与\"内容有毒\"共用一套判据。\n\u003e rc.3 改为返回 `\"STALE\"`,并顺手删掉该 fact 可能留在 gap 缓冲里的副本(落后事实永远等不到\n\u003e `previous_version = 水位`,留着只会把缓冲占满到 `GAP_BUFFER_FULL`,drain 循环也会空转)。\n\u003e\n\u003e 消费 rc.2 及更早版本的上层需要在边界做\"错误码 → 结果词\"的翻译;**升到 rc.3 后应删除该翻译**,\n\u003e 判定回到 SDK 单源。\n\n## 4. 写链边界\n\n`appendFactIntent` 必须在**属主自己的业务事务内**调用——它与业务写同 tx,事务回滚则事实意图一并回滚。\n`FactInbox.handle` 同理在消费者事务内完成 `processed_fact` + 投影 + 水位的原子写入。\n本包不提供事务,也不管理连接:宿主传入符合 `FactTransaction` 结构的 tx 即可(不要求宿主生成 Prisma schema)。\n\n## 5. 可信回查读取器 `createFactReader`(候选)\n\n\u003e 本节为 `3074b75` 引入的读取面文档,原文保留(英文),与上面三节的接入契约互补:\n\u003e 上面讲「接进来会踩什么」,本节讲「读取器本身怎么用、边界在哪」。\n\n```ts\nconst reader = createFactReader({\n baseUrl: \"https://platform.example/api\",\n tenantId,\n token: getConsumerBearer,\n});\nconst inbox = new FactInbox({\n tenantId, consumerId, tenant: consumerTenantTransaction,\n trustedLookup: reader.lookup,\n apply: applyOwnerFact,\n});\n// On shutdown: stop consumer delivery, then reader.close().\n```\n\nThe server binds the verified JWT tenant/application/subject to one enrolled consumer. It checks the persisted consumer's activation and source-domain/fact-type scope on every lookup. `replayAllowed` is not required for point lookups; replay remains a separate operation. The candidate server is enabled only with `PLATFORM_FACT_READ_ENABLED=1` in development/test with JWT, `DATABASE_URL_FACT`, and an exact `FACT_READERS_JSON` list of `{tenantId,appId,subject,consumerId}`. It can run independently of intake and relay. Production/default assembly remains unavailable pending CHG-004 and live service identity integration.\n\nThe reader uses HTTPS (HTTP only on literal loopback/localhost), denies redirects and URL credentials, and bounds token/fetch/body processing to 5 seconds and 128 KiB by default. It verifies tenant/id/ordering/receipt and the canonical submission digest. It does not cache authorization or facts. A typed M5 `FACT_NOT_FOUND` returns null; all other transport/authentication/integrity errors throw `FactReadUnavailable`, which is deliberately distinct from `FactRejected`. Dependency failures must retain the Kafka offset and must not increment poison counters. `close()` aborts active lookups and rejects future ones; callbacks that ignore cancellation may continue internally but cannot produce an accepted lookup.\n\nA deactivated or deleted consumer registration answers 403, which the reader turns into a retryable `FactReadUnavailable`: the Kafka offset is kept and nothing is counted as poison. A registration that is still active but no longer covers the fact's source domain or type answers the same 404 as an unknown id, which is a null lookup and therefore `UNTRUSTED_FACT` — a dead letter. Narrowing `sourceDomain`/`factTypes` on a live consumer will dead-letter facts that are already in flight, while deactivating the same consumer only pauses it. Deactivate first, change scope, then reactivate; treat a scope edit as a migration, not as a switch.\n\nThe server refuses a bearer carried in the query string (`FACT_READ_URL_CREDENTIAL_REFUSED`, 401) even when a valid `Authorization` header is also present, because the token has already reached the proxy access log by then. The reader never produces such a URL; the refusal exists for anything else that calls the surface.\n\nAn authenticated M5 response plus the receipt digest is the trust boundary here, not a cryptographic signature proving the Owner. A compromised M5 authority or whole-row rewrite with a recomputed digest is outside this reader's detection. Kafka self-asserted hashes are never sufficient. Consumers still need the Owner's domain validation, bootstrap watermark reconciliation and appropriate action enforcement.\n","repository":{"type":"","url":""}}
TEXT
download_count
INTEGER NOT NULL (default 0
Update
Cancel