@tier0/sdk-v2 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +8 -6
- package/dist/cjs/cloud-server.d.ts +6 -6
- package/dist/cjs/cloud-server.js +5 -6
- package/dist/cjs/cloud-server.js.map +1 -1
- package/dist/esm/cloud-server.d.ts +6 -6
- package/dist/esm/cloud-server.js +5 -6
- package/dist/esm/cloud-server.js.map +1 -1
- package/package.json +1 -1
- package/skills/tier0-sdk-v2/references/integration.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.11.0 (2026-11-11)
|
|
4
|
+
|
|
5
|
+
- Breaking: `Visitor.subType` is only `employee`. `parseVisitorHeader()` refuses every other identity, including old machine headers with or without a grant. Employee grants remain mandatory (1–128 bytes); canonical encoding, field validation and App binding are unchanged. No machine alias or compatibility API is provided.
|
|
6
|
+
- Built on 0.10.1: keeps its committed Backend contract `07ef8cd30a5465a98c8e6ee0892d02360aee8761` and the unified Builder grant guidance, without editing generated contracts. App Key authentication (`TIER0_API_KEY` → one `X-API-Key`), employee roles, data cancellation, data and Stream APIs are unchanged.
|
|
7
|
+
- Existing deployed Apps retain their bundled parser. Retiring machine entry points and stripping untrusted identity headers at the platform front door invalidates old call tokens without republishing those Apps; updating this package alone does not secure an alternate ingress.
|
|
8
|
+
|
|
9
|
+
## 0.10.1-aenv.13 (local remove-apps-in-use integration)
|
|
10
|
+
|
|
11
|
+
- Rebase the employee-only visitor cutover onto 0.10.1, retaining its committed Backend contract `07ef8cd30a5465a98c8e6ee0892d02360aee8761` and unified Builder grant guidance without editing generated contracts.
|
|
12
|
+
- Repack for the scaffold as a distinct local artifact rather than replacing the historical 0.10.0-aenv.12 archive. No npm publication or existing App republication is performed.
|
|
13
|
+
- Machine visitor identities remain rejected; employee grants, platform App Keys, data and Stream APIs keep the latest base behavior.
|
|
14
|
+
|
|
3
15
|
## 0.10.1 (2026-11-10)
|
|
4
16
|
|
|
5
17
|
- Clarify Builder grant management for existing collections across Apps in the same workspace. Document incremental `appDataPolicyUpdate` changes, revision checks and receipt verification while preserving existing grants and disabled policy status; App runtime credentials cannot manage grants.
|
|
6
18
|
- Align README and bundled Skills with the Backend's unified authorization rules: management uses the current principal's effective workspace, App and resource permissions, without a separate Agent denial or platform-import exception.
|
|
7
19
|
- Synchronize generated contracts to committed Backend `07ef8cd30a5465a98c8e6ee0892d02360aee8761`, including the optional `permission` field in the platform identity response. No SDK operation is removed.
|
|
8
20
|
|
|
21
|
+
## 0.10.0-aenv.12 (local access-settings package)
|
|
22
|
+
|
|
23
|
+
- Breaking: `Visitor.subType` is only `employee`. `parseVisitorHeader()` refuses every other identity, including old machine headers with or without a grant. Employee grants remain mandatory; canonical encoding, field validation and App binding are unchanged. No machine alias or compatibility API is provided.
|
|
24
|
+
- App Key authentication (`TIER0_API_KEY` → one `X-API-Key`), employee roles, data cancellation and realtime APIs are unchanged. This local package keeps the existing 0.10.0 generated Backend contracts; no contract synchronization or Backend upgrade is part of this change.
|
|
25
|
+
- Existing deployed Apps retain their bundled parser. Retiring machine entry points and stripping untrusted identity headers at the platform front door invalidates old call tokens without republishing those Apps; updating this package alone does not secure an alternate ingress.
|
|
26
|
+
|
|
9
27
|
## 0.10.0 (2026-11-10)
|
|
10
28
|
|
|
11
29
|
First stable npm release of the new-architecture migration, including the changes recorded below for the local aenv-next prereleases and the 0.9.11 candidate.
|
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Tier0 云端统一 TypeScript / JavaScript SDK,数据管理与 App Stream 交互均使用 `@tier0/sdk-v2`。旧 SDK 不再作为依赖。
|
|
4
4
|
|
|
5
|
+
**`0.11.0` 是破坏性变更**:访问者只认员工,`Visitor.subType` 仅为 `employee`,`parseVisitorHeader()` 拒绝旧机器头(无论是否带 grant),不提供兼容别名。基于 `0.10.1`,沿用其 Backend 契约 `07ef8cd30a5465a98c8e6ee0892d02360aee8761` 与统一授权说明,不手改生成物;App Key、员工角色、数据与 Stream 接口不变。存量部署仍含旧解析器,靠前门关闭机器入口并剥离外来身份头使旧调用令牌失效,不强制重新发布 App。
|
|
6
|
+
|
|
5
7
|
`0.10.1` 补齐 Builder 为同一工作区内的 App 主动关联已有集合的指引,并同步接口类型与 Backend 契约 `07ef8cd30a5465a98c8e6ee0892d02360aee8761`。需搭配已部署统一集合授权规则的 Backend;本次 npm 包包含更新后的 Skills,平台托管 Skill 的版本由平台单独管理。
|
|
6
8
|
|
|
7
9
|
`0.10.0` 是新架构重构分支的正式版本,包含此前 aenv-next 预发布的全部变更、MQTT 修复、逐请求异步凭据和 Sample 精确删除接口。合同同步至 Backend `91b5f3dc4024f44df32ee7aafcd83a8d470b655d`;升级前需确认目标环境已部署相应合同及运行环境,详见下方「aenv-next 运行环境」和 CHANGELOG。
|
|
@@ -45,10 +47,10 @@ Source / Stream 管理已统一为 Enterprise 参数:SourceDefinition 使用
|
|
|
45
47
|
安装正式版:
|
|
46
48
|
|
|
47
49
|
```sh
|
|
48
|
-
npm install @tier0/sdk-v2@0.
|
|
50
|
+
npm install @tier0/sdk-v2@0.11.0
|
|
49
51
|
```
|
|
50
52
|
|
|
51
|
-
维护者发布前在本仓库运行 `npm ci`、`npm run check`,并验证生成的 `tier0-sdk-v2-0.
|
|
53
|
+
维护者发布前在本仓库运行 `npm ci`、`npm run check`,并验证生成的 `tier0-sdk-v2-0.11.0.tgz`。
|
|
52
54
|
|
|
53
55
|
个人 Key 可直接读取已有集合,无需创建平台 App;网关配置、运行示例与排错见[本地连接指南](skills/tier0-sdk-v2/references/local-connection.md)。
|
|
54
56
|
|
|
@@ -108,7 +110,7 @@ App 的 Key(平台注入的这把)只能调 `/openapi/v2/uns/**`、`assets/f
|
|
|
108
110
|
|
|
109
111
|
本地开发照旧设 `TIER0_API_HOST`、`TIER0_API_KEY`,不要设 `TIER0_RUNTIME`(也不要写进 `.env` 或 `artifact.toml`)。
|
|
110
112
|
|
|
111
|
-
- `@tier0/sdk-v2/server`:`parseVisitorHeader(request.headers.get(VISITOR_HEADER))` 解析前门注入的 `X-Tier0-Visitor`,返回 `{ subType, subject, grant, workspaceID, appID, runtime, appRoles, activeRole? }`。头的值是 UTF-8 JSON 对象的无填充 base64url(编码前最多 8192 字节),不签名:前门转发前剥掉客户端带来的全部 `X-Tier0-*`,再注入这一个头。解析是严格的:只接受规范的无填充 base64url;BOM、重复键、孤立的 UTF-16 代理项(如 `"\ud800"`)、未知字段、缺必填字段、类型或长度不符、`ws` 不是正整数十进制串(不带符号和前导 0
|
|
113
|
+
- `@tier0/sdk-v2/server`:`parseVisitorHeader(request.headers.get(VISITOR_HEADER))` 解析前门注入的 `X-Tier0-Visitor`,返回 `{ subType, subject, grant, workspaceID, appID, runtime, appRoles, activeRole? }`。头的值是 UTF-8 JSON 对象的无填充 base64url(编码前最多 8192 字节),不签名:前门转发前剥掉客户端带来的全部 `X-Tier0-*`,再注入这一个头。解析是严格的:只接受规范的无填充 base64url;BOM、重复键、孤立的 UTF-16 代理项(如 `"\ud800"`)、未知字段、缺必填字段、类型或长度不符、`ws` 不是正整数十进制串(不带符号和前导 0)、非 employee 访问者、空 grant、`app` 不等于 `TIER0_APP_ID` 都抛 `VisitorError`(`VISITOR_HEADER_INVALID`);没有 `TIER0_APP_ID` 时是 `VISITOR_CONFIGURATION_INVALID`。App 用 `appRoles` 自己决定当前用户能看、能改哪些数据;平台对 App 的调用不带访问者身份,不会替 App 收窄。
|
|
112
114
|
|
|
113
115
|
```ts
|
|
114
116
|
import { parseVisitorHeader, VISITOR_HEADER, VisitorError } from '@tier0/sdk-v2/server';
|
|
@@ -128,9 +130,9 @@ App 的 Key(平台注入的这把)只能调 `/openapi/v2/uns/**`、`assets/f
|
|
|
128
130
|
| `workspaceID` | `ws` | 工作区 ID(十进制数字串) |
|
|
129
131
|
| `appID` | `app` | 等于 `TIER0_APP_ID` |
|
|
130
132
|
| `runtime` | `runtime` | `deployed` 或 `preview` |
|
|
131
|
-
| `subType` | `sub_type` | `employee`
|
|
132
|
-
| `subject` | `sub` | 员工用户 ID
|
|
133
|
-
| `grant` | `grant` | 员工访问的 grant ID(1–128
|
|
133
|
+
| `subType` | `sub_type` | 仅 `employee` |
|
|
134
|
+
| `subject` | `sub` | 员工用户 ID(1–128 字节) |
|
|
135
|
+
| `grant` | `grant` | 员工访问的 grant ID(1–128 字节,必填) |
|
|
134
136
|
| `appRoles` | `app_roles` | 访问者在 App 里的角色(不限项数,每项 1–128 字节,整个 JSON 不超过 8192 字节;重复的去掉);预览里是「以某角色查看」选中的角色 |
|
|
135
137
|
| `activeRole` | `active_role` | 可省略,只在预览的「以某角色查看」里出现 |
|
|
136
138
|
- `@tier0/sdk-v2/page-runtime`:页面运行时 v1 和平台错误(`x-sbx-error`)。
|
|
@@ -7,10 +7,10 @@ export declare class VisitorError extends Error {
|
|
|
7
7
|
}
|
|
8
8
|
/** The visitor of a request, from `X-Tier0-Visitor`. */
|
|
9
9
|
export interface Visitor {
|
|
10
|
-
readonly subType: 'employee'
|
|
11
|
-
/** Employee user ID
|
|
10
|
+
readonly subType: 'employee';
|
|
11
|
+
/** Employee user ID. */
|
|
12
12
|
readonly subject: string;
|
|
13
|
-
/** Grant of
|
|
13
|
+
/** Grant of the employee's access. */
|
|
14
14
|
readonly grant: string;
|
|
15
15
|
readonly workspaceID: string;
|
|
16
16
|
readonly appID: string;
|
|
@@ -30,9 +30,9 @@ export interface ParseVisitorOptions {
|
|
|
30
30
|
* is the only way into the App and replaces whatever the client sent. Parsing
|
|
31
31
|
* is strict: a byte order mark, a duplicate key, a lone UTF-16 surrogate
|
|
32
32
|
* (e.g. `"\ud800"`), an unknown or missing field, a wrong type or length, a `ws` that
|
|
33
|
-
* is not a positive decimal ID without sign or leading zero,
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* is not a positive decimal ID without sign or leading zero, a non-employee
|
|
34
|
+
* visitor, an empty grant, or an `app` other than this App's throws
|
|
35
|
+
* `VISITOR_HEADER_INVALID`. Without an App ID
|
|
36
36
|
* (`TIER0_APP_ID` unset) it throws `VISITOR_CONFIGURATION_INVALID`.
|
|
37
37
|
*/
|
|
38
38
|
export declare function parseVisitorHeader(value: string | null | undefined, options?: ParseVisitorOptions): Visitor;
|
package/dist/cjs/cloud-server.js
CHANGED
|
@@ -63,9 +63,9 @@ const sized = (value, min, max) => typeof value === 'string' && !LONE_SURROGATE.
|
|
|
63
63
|
* is the only way into the App and replaces whatever the client sent. Parsing
|
|
64
64
|
* is strict: a byte order mark, a duplicate key, a lone UTF-16 surrogate
|
|
65
65
|
* (e.g. `"\ud800"`), an unknown or missing field, a wrong type or length, a `ws` that
|
|
66
|
-
* is not a positive decimal ID without sign or leading zero,
|
|
67
|
-
*
|
|
68
|
-
*
|
|
66
|
+
* is not a positive decimal ID without sign or leading zero, a non-employee
|
|
67
|
+
* visitor, an empty grant, or an `app` other than this App's throws
|
|
68
|
+
* `VISITOR_HEADER_INVALID`. Without an App ID
|
|
69
69
|
* (`TIER0_APP_ID` unset) it throws `VISITOR_CONFIGURATION_INVALID`.
|
|
70
70
|
*/
|
|
71
71
|
function parseVisitorHeader(value, options = {}) {
|
|
@@ -100,10 +100,9 @@ function parseVisitorHeader(value, options = {}) {
|
|
|
100
100
|
const { ws, app, runtime, sub_type: subType, sub, grant, app_roles: appRoles, active_role: activeRole } = claims;
|
|
101
101
|
if (!validID(ws) || app !== appID ||
|
|
102
102
|
(runtime !== 'deployed' && runtime !== 'preview') ||
|
|
103
|
-
|
|
103
|
+
subType !== 'employee' ||
|
|
104
104
|
!sized(sub, 1, VISITOR_MAX_STRING_BYTES) ||
|
|
105
|
-
|
|
106
|
-
typeof grant !== 'string' || (subType === 'employee' ? !sized(grant, 1, VISITOR_MAX_STRING_BYTES) : grant !== '') ||
|
|
105
|
+
!sized(grant, 1, VISITOR_MAX_STRING_BYTES) ||
|
|
107
106
|
!Array.isArray(appRoles) || appRoles.some((role) => !sized(role, 1, VISITOR_MAX_STRING_BYTES)) ||
|
|
108
107
|
(activeRole !== undefined && !sized(activeRole, 1, VISITOR_MAX_STRING_BYTES)))
|
|
109
108
|
throw fail();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloud-server.js","sourceRoot":"","sources":["../../src/cloud-server.ts"],"names":[],"mappings":"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","sourcesContent":["// Server-only entry point: the App's view of the request it is serving.\nimport { getEnvVar } from './runtime-env.js';\n\nconst maxID = BigInt('9223372036854775807');\n/** A positive decimal int64 without sign or leading zero. */\nconst validID = (value: unknown): value is string =>\n typeof value === 'string' && /^[1-9]\\d{0,18}$/.test(value) && BigInt(value) <= maxID;\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n !!value && typeof value === 'object' && !Array.isArray(value);\n\n/** Set by the front door on every request it forwards to the App, after stripping any `X-Tier0-*` the client sent. */\nexport const VISITOR_HEADER = 'X-Tier0-Visitor';\n\n/** Limit of the JSON before encoding. */\nconst VISITOR_MAX_JSON_BYTES = 8192;\n/** Unpadded base64url of `VISITOR_MAX_JSON_BYTES`. */\nconst VISITOR_MAX_HEADER_LENGTH = Math.ceil((VISITOR_MAX_JSON_BYTES * 4) / 3);\nconst VISITOR_MAX_STRING_BYTES = 128;\nconst VISITOR_FIELDS: ReadonlySet<string> = new Set(['ws', 'app', 'runtime', 'sub_type', 'sub', 'grant', 'app_roles', 'active_role']);\n\nexport type VisitorErrorCode = 'VISITOR_HEADER_INVALID' | 'VISITOR_CONFIGURATION_INVALID';\n\nexport class VisitorError extends Error {\n constructor(public readonly code: VisitorErrorCode) {\n super(code);\n this.name = 'VisitorError';\n }\n}\n\n/** The visitor of a request, from `X-Tier0-Visitor`. */\nexport interface Visitor {\n readonly subType: 'employee' | 'machine';\n /** Employee user ID, or the call token ID of a machine visitor. */\n readonly subject: string;\n /** Grant of an employee's access; empty for a machine visitor. */\n readonly grant: string;\n readonly workspaceID: string;\n readonly appID: string;\n readonly runtime: 'deployed' | 'preview';\n /** Roles of this visitor in the App; in Preview, the roles selected in \"view as\". */\n readonly appRoles: readonly string[];\n /** Only in Preview's \"view as\". */\n readonly activeRole?: string;\n}\n\nexport interface ParseVisitorOptions {\n /** The App the header must name. Defaults to `TIER0_APP_ID`. */\n appID?: string;\n}\n\n/** A UTF-16 surrogate without its pair, as JSON escapes like \"\\ud800\" can produce. */\nconst LONE_SURROGATE = /[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]/;\n\n/**\n * Raw key tokens of the top-level object, for spotting duplicates\n * (`JSON.parse` keeps the last silently). Values of the visitor are strings\n * and arrays of strings, so only depth 1 holds keys.\n */\nfunction topLevelKeys(json: string): string[] {\n const keys: string[] = [];\n let depth = 0;\n for (let i = 0; i < json.length; i += 1) {\n const char = json[i];\n if (char === '{' || char === '[') depth += 1;\n else if (char === '}' || char === ']') depth -= 1;\n else if (char === '\"') {\n const start = i;\n for (i += 1; i < json.length && json[i] !== '\"'; i += 1) if (json[i] === '\\\\') i += 1;\n let next = i + 1;\n while (next < json.length && /\\s/.test(json[next])) next += 1;\n if (depth === 1 && json[next] === ':') keys.push(json.slice(start, i + 1));\n }\n }\n return keys;\n}\n\nconst bytes = (value: string) => Buffer.byteLength(value, 'utf8');\nconst sized = (value: unknown, min: number, max: number): value is string =>\n typeof value === 'string' && !LONE_SURROGATE.test(value) && bytes(value) >= min && bytes(value) <= max;\n\n/**\n * Parses `X-Tier0-Visitor`: the unpadded base64url (RFC 4648 §5) of a UTF-8\n * JSON object of at most 8192 bytes. The header is not signed; the front door\n * is the only way into the App and replaces whatever the client sent. Parsing\n * is strict: a byte order mark, a duplicate key, a lone UTF-16 surrogate\n * (e.g. `\"\\ud800\"`), an unknown or missing field, a wrong type or length, a `ws` that\n * is not a positive decimal ID without sign or leading zero, an employee\n * without a grant or a machine visitor with one, or an `app` other than this\n * App's throws `VISITOR_HEADER_INVALID`. Without an App ID\n * (`TIER0_APP_ID` unset) it throws `VISITOR_CONFIGURATION_INVALID`.\n */\nexport function parseVisitorHeader(value: string | null | undefined, options: ParseVisitorOptions = {}): Visitor {\n const appID = options.appID ?? getEnvVar('TIER0_APP_ID')?.trim();\n if (!appID) throw new VisitorError('VISITOR_CONFIGURATION_INVALID');\n const fail = () => new VisitorError('VISITOR_HEADER_INVALID');\n if (typeof value !== 'string' || value.length === 0 || value.length > VISITOR_MAX_HEADER_LENGTH || !/^[A-Za-z0-9_-]+$/.test(value)) throw fail();\n const raw = Buffer.from(value, 'base64url');\n // Only the canonical encoding: no padding, no stray trailing bits.\n if (raw.length > VISITOR_MAX_JSON_BYTES || raw.toString('base64url') !== value) throw fail();\n // A UTF-8 byte order mark is not part of the contract (TextDecoder would drop it silently).\n if (raw[0] === 0xef && raw[1] === 0xbb && raw[2] === 0xbf) throw fail();\n let claims: unknown;\n let json: string;\n try {\n json = new TextDecoder('utf-8', { fatal: true }).decode(raw);\n claims = JSON.parse(json);\n } catch {\n throw fail();\n }\n if (!isObject(claims) || Object.keys(claims).some((key) => !VISITOR_FIELDS.has(key))) throw fail();\n // Every key at most once, each spelling decoded (\"\\u0077s\" is \"ws\").\n const keys = topLevelKeys(json).map((token) => JSON.parse(token) as string);\n if (new Set(keys).size !== keys.length) throw fail();\n const { ws, app, runtime, sub_type: subType, sub, grant, app_roles: appRoles, active_role: activeRole } = claims;\n if (!validID(ws) || app !== appID ||\n (runtime !== 'deployed' && runtime !== 'preview') ||\n (subType !== 'employee' && subType !== 'machine') ||\n !sized(sub, 1, VISITOR_MAX_STRING_BYTES) ||\n // An employee's access always has a grant; a machine visitor has none.\n typeof grant !== 'string' || (subType === 'employee' ? !sized(grant, 1, VISITOR_MAX_STRING_BYTES) : grant !== '') ||\n !Array.isArray(appRoles) || appRoles.some((role) => !sized(role, 1, VISITOR_MAX_STRING_BYTES)) ||\n (activeRole !== undefined && !sized(activeRole, 1, VISITOR_MAX_STRING_BYTES))) throw fail();\n return Object.freeze({\n subType, subject: sub, grant, workspaceID: ws, appID, runtime,\n appRoles: Object.freeze([...new Set(appRoles as string[])]),\n ...(activeRole === undefined ? {} : { activeRole }),\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"cloud-server.js","sourceRoot":"","sources":["../../src/cloud-server.ts"],"names":[],"mappings":"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","sourcesContent":["// Server-only entry point: the App's view of the request it is serving.\nimport { getEnvVar } from './runtime-env.js';\n\nconst maxID = BigInt('9223372036854775807');\n/** A positive decimal int64 without sign or leading zero. */\nconst validID = (value: unknown): value is string =>\n typeof value === 'string' && /^[1-9]\\d{0,18}$/.test(value) && BigInt(value) <= maxID;\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n !!value && typeof value === 'object' && !Array.isArray(value);\n\n/** Set by the front door on every request it forwards to the App, after stripping any `X-Tier0-*` the client sent. */\nexport const VISITOR_HEADER = 'X-Tier0-Visitor';\n\n/** Limit of the JSON before encoding. */\nconst VISITOR_MAX_JSON_BYTES = 8192;\n/** Unpadded base64url of `VISITOR_MAX_JSON_BYTES`. */\nconst VISITOR_MAX_HEADER_LENGTH = Math.ceil((VISITOR_MAX_JSON_BYTES * 4) / 3);\nconst VISITOR_MAX_STRING_BYTES = 128;\nconst VISITOR_FIELDS: ReadonlySet<string> = new Set(['ws', 'app', 'runtime', 'sub_type', 'sub', 'grant', 'app_roles', 'active_role']);\n\nexport type VisitorErrorCode = 'VISITOR_HEADER_INVALID' | 'VISITOR_CONFIGURATION_INVALID';\n\nexport class VisitorError extends Error {\n constructor(public readonly code: VisitorErrorCode) {\n super(code);\n this.name = 'VisitorError';\n }\n}\n\n/** The visitor of a request, from `X-Tier0-Visitor`. */\nexport interface Visitor {\n readonly subType: 'employee';\n /** Employee user ID. */\n readonly subject: string;\n /** Grant of the employee's access. */\n readonly grant: string;\n readonly workspaceID: string;\n readonly appID: string;\n readonly runtime: 'deployed' | 'preview';\n /** Roles of this visitor in the App; in Preview, the roles selected in \"view as\". */\n readonly appRoles: readonly string[];\n /** Only in Preview's \"view as\". */\n readonly activeRole?: string;\n}\n\nexport interface ParseVisitorOptions {\n /** The App the header must name. Defaults to `TIER0_APP_ID`. */\n appID?: string;\n}\n\n/** A UTF-16 surrogate without its pair, as JSON escapes like \"\\ud800\" can produce. */\nconst LONE_SURROGATE = /[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]/;\n\n/**\n * Raw key tokens of the top-level object, for spotting duplicates\n * (`JSON.parse` keeps the last silently). Values of the visitor are strings\n * and arrays of strings, so only depth 1 holds keys.\n */\nfunction topLevelKeys(json: string): string[] {\n const keys: string[] = [];\n let depth = 0;\n for (let i = 0; i < json.length; i += 1) {\n const char = json[i];\n if (char === '{' || char === '[') depth += 1;\n else if (char === '}' || char === ']') depth -= 1;\n else if (char === '\"') {\n const start = i;\n for (i += 1; i < json.length && json[i] !== '\"'; i += 1) if (json[i] === '\\\\') i += 1;\n let next = i + 1;\n while (next < json.length && /\\s/.test(json[next])) next += 1;\n if (depth === 1 && json[next] === ':') keys.push(json.slice(start, i + 1));\n }\n }\n return keys;\n}\n\nconst bytes = (value: string) => Buffer.byteLength(value, 'utf8');\nconst sized = (value: unknown, min: number, max: number): value is string =>\n typeof value === 'string' && !LONE_SURROGATE.test(value) && bytes(value) >= min && bytes(value) <= max;\n\n/**\n * Parses `X-Tier0-Visitor`: the unpadded base64url (RFC 4648 §5) of a UTF-8\n * JSON object of at most 8192 bytes. The header is not signed; the front door\n * is the only way into the App and replaces whatever the client sent. Parsing\n * is strict: a byte order mark, a duplicate key, a lone UTF-16 surrogate\n * (e.g. `\"\\ud800\"`), an unknown or missing field, a wrong type or length, a `ws` that\n * is not a positive decimal ID without sign or leading zero, a non-employee\n * visitor, an empty grant, or an `app` other than this App's throws\n * `VISITOR_HEADER_INVALID`. Without an App ID\n * (`TIER0_APP_ID` unset) it throws `VISITOR_CONFIGURATION_INVALID`.\n */\nexport function parseVisitorHeader(value: string | null | undefined, options: ParseVisitorOptions = {}): Visitor {\n const appID = options.appID ?? getEnvVar('TIER0_APP_ID')?.trim();\n if (!appID) throw new VisitorError('VISITOR_CONFIGURATION_INVALID');\n const fail = () => new VisitorError('VISITOR_HEADER_INVALID');\n if (typeof value !== 'string' || value.length === 0 || value.length > VISITOR_MAX_HEADER_LENGTH || !/^[A-Za-z0-9_-]+$/.test(value)) throw fail();\n const raw = Buffer.from(value, 'base64url');\n // Only the canonical encoding: no padding, no stray trailing bits.\n if (raw.length > VISITOR_MAX_JSON_BYTES || raw.toString('base64url') !== value) throw fail();\n // A UTF-8 byte order mark is not part of the contract (TextDecoder would drop it silently).\n if (raw[0] === 0xef && raw[1] === 0xbb && raw[2] === 0xbf) throw fail();\n let claims: unknown;\n let json: string;\n try {\n json = new TextDecoder('utf-8', { fatal: true }).decode(raw);\n claims = JSON.parse(json);\n } catch {\n throw fail();\n }\n if (!isObject(claims) || Object.keys(claims).some((key) => !VISITOR_FIELDS.has(key))) throw fail();\n // Every key at most once, each spelling decoded (\"\\u0077s\" is \"ws\").\n const keys = topLevelKeys(json).map((token) => JSON.parse(token) as string);\n if (new Set(keys).size !== keys.length) throw fail();\n const { ws, app, runtime, sub_type: subType, sub, grant, app_roles: appRoles, active_role: activeRole } = claims;\n if (!validID(ws) || app !== appID ||\n (runtime !== 'deployed' && runtime !== 'preview') ||\n subType !== 'employee' ||\n !sized(sub, 1, VISITOR_MAX_STRING_BYTES) ||\n !sized(grant, 1, VISITOR_MAX_STRING_BYTES) ||\n !Array.isArray(appRoles) || appRoles.some((role) => !sized(role, 1, VISITOR_MAX_STRING_BYTES)) ||\n (activeRole !== undefined && !sized(activeRole, 1, VISITOR_MAX_STRING_BYTES))) throw fail();\n return Object.freeze({\n subType, subject: sub, grant, workspaceID: ws, appID, runtime,\n appRoles: Object.freeze([...new Set(appRoles as string[])]),\n ...(activeRole === undefined ? {} : { activeRole }),\n });\n}\n"]}
|
|
@@ -7,10 +7,10 @@ export declare class VisitorError extends Error {
|
|
|
7
7
|
}
|
|
8
8
|
/** The visitor of a request, from `X-Tier0-Visitor`. */
|
|
9
9
|
export interface Visitor {
|
|
10
|
-
readonly subType: 'employee'
|
|
11
|
-
/** Employee user ID
|
|
10
|
+
readonly subType: 'employee';
|
|
11
|
+
/** Employee user ID. */
|
|
12
12
|
readonly subject: string;
|
|
13
|
-
/** Grant of
|
|
13
|
+
/** Grant of the employee's access. */
|
|
14
14
|
readonly grant: string;
|
|
15
15
|
readonly workspaceID: string;
|
|
16
16
|
readonly appID: string;
|
|
@@ -30,9 +30,9 @@ export interface ParseVisitorOptions {
|
|
|
30
30
|
* is the only way into the App and replaces whatever the client sent. Parsing
|
|
31
31
|
* is strict: a byte order mark, a duplicate key, a lone UTF-16 surrogate
|
|
32
32
|
* (e.g. `"\ud800"`), an unknown or missing field, a wrong type or length, a `ws` that
|
|
33
|
-
* is not a positive decimal ID without sign or leading zero,
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* is not a positive decimal ID without sign or leading zero, a non-employee
|
|
34
|
+
* visitor, an empty grant, or an `app` other than this App's throws
|
|
35
|
+
* `VISITOR_HEADER_INVALID`. Without an App ID
|
|
36
36
|
* (`TIER0_APP_ID` unset) it throws `VISITOR_CONFIGURATION_INVALID`.
|
|
37
37
|
*/
|
|
38
38
|
export declare function parseVisitorHeader(value: string | null | undefined, options?: ParseVisitorOptions): Visitor;
|
package/dist/esm/cloud-server.js
CHANGED
|
@@ -58,9 +58,9 @@ const sized = (value, min, max) => typeof value === 'string' && !LONE_SURROGATE.
|
|
|
58
58
|
* is the only way into the App and replaces whatever the client sent. Parsing
|
|
59
59
|
* is strict: a byte order mark, a duplicate key, a lone UTF-16 surrogate
|
|
60
60
|
* (e.g. `"\ud800"`), an unknown or missing field, a wrong type or length, a `ws` that
|
|
61
|
-
* is not a positive decimal ID without sign or leading zero,
|
|
62
|
-
*
|
|
63
|
-
*
|
|
61
|
+
* is not a positive decimal ID without sign or leading zero, a non-employee
|
|
62
|
+
* visitor, an empty grant, or an `app` other than this App's throws
|
|
63
|
+
* `VISITOR_HEADER_INVALID`. Without an App ID
|
|
64
64
|
* (`TIER0_APP_ID` unset) it throws `VISITOR_CONFIGURATION_INVALID`.
|
|
65
65
|
*/
|
|
66
66
|
export function parseVisitorHeader(value, options = {}) {
|
|
@@ -95,10 +95,9 @@ export function parseVisitorHeader(value, options = {}) {
|
|
|
95
95
|
const { ws, app, runtime, sub_type: subType, sub, grant, app_roles: appRoles, active_role: activeRole } = claims;
|
|
96
96
|
if (!validID(ws) || app !== appID ||
|
|
97
97
|
(runtime !== 'deployed' && runtime !== 'preview') ||
|
|
98
|
-
|
|
98
|
+
subType !== 'employee' ||
|
|
99
99
|
!sized(sub, 1, VISITOR_MAX_STRING_BYTES) ||
|
|
100
|
-
|
|
101
|
-
typeof grant !== 'string' || (subType === 'employee' ? !sized(grant, 1, VISITOR_MAX_STRING_BYTES) : grant !== '') ||
|
|
100
|
+
!sized(grant, 1, VISITOR_MAX_STRING_BYTES) ||
|
|
102
101
|
!Array.isArray(appRoles) || appRoles.some((role) => !sized(role, 1, VISITOR_MAX_STRING_BYTES)) ||
|
|
103
102
|
(activeRole !== undefined && !sized(activeRole, 1, VISITOR_MAX_STRING_BYTES)))
|
|
104
103
|
throw fail();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloud-server.js","sourceRoot":"","sources":["../../src/cloud-server.ts"],"names":[],"mappings":"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","sourcesContent":["// Server-only entry point: the App's view of the request it is serving.\nimport { getEnvVar } from './runtime-env.js';\n\nconst maxID = BigInt('9223372036854775807');\n/** A positive decimal int64 without sign or leading zero. */\nconst validID = (value: unknown): value is string =>\n typeof value === 'string' && /^[1-9]\\d{0,18}$/.test(value) && BigInt(value) <= maxID;\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n !!value && typeof value === 'object' && !Array.isArray(value);\n\n/** Set by the front door on every request it forwards to the App, after stripping any `X-Tier0-*` the client sent. */\nexport const VISITOR_HEADER = 'X-Tier0-Visitor';\n\n/** Limit of the JSON before encoding. */\nconst VISITOR_MAX_JSON_BYTES = 8192;\n/** Unpadded base64url of `VISITOR_MAX_JSON_BYTES`. */\nconst VISITOR_MAX_HEADER_LENGTH = Math.ceil((VISITOR_MAX_JSON_BYTES * 4) / 3);\nconst VISITOR_MAX_STRING_BYTES = 128;\nconst VISITOR_FIELDS: ReadonlySet<string> = new Set(['ws', 'app', 'runtime', 'sub_type', 'sub', 'grant', 'app_roles', 'active_role']);\n\nexport type VisitorErrorCode = 'VISITOR_HEADER_INVALID' | 'VISITOR_CONFIGURATION_INVALID';\n\nexport class VisitorError extends Error {\n constructor(public readonly code: VisitorErrorCode) {\n super(code);\n this.name = 'VisitorError';\n }\n}\n\n/** The visitor of a request, from `X-Tier0-Visitor`. */\nexport interface Visitor {\n readonly subType: 'employee' | 'machine';\n /** Employee user ID, or the call token ID of a machine visitor. */\n readonly subject: string;\n /** Grant of an employee's access; empty for a machine visitor. */\n readonly grant: string;\n readonly workspaceID: string;\n readonly appID: string;\n readonly runtime: 'deployed' | 'preview';\n /** Roles of this visitor in the App; in Preview, the roles selected in \"view as\". */\n readonly appRoles: readonly string[];\n /** Only in Preview's \"view as\". */\n readonly activeRole?: string;\n}\n\nexport interface ParseVisitorOptions {\n /** The App the header must name. Defaults to `TIER0_APP_ID`. */\n appID?: string;\n}\n\n/** A UTF-16 surrogate without its pair, as JSON escapes like \"\\ud800\" can produce. */\nconst LONE_SURROGATE = /[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]/;\n\n/**\n * Raw key tokens of the top-level object, for spotting duplicates\n * (`JSON.parse` keeps the last silently). Values of the visitor are strings\n * and arrays of strings, so only depth 1 holds keys.\n */\nfunction topLevelKeys(json: string): string[] {\n const keys: string[] = [];\n let depth = 0;\n for (let i = 0; i < json.length; i += 1) {\n const char = json[i];\n if (char === '{' || char === '[') depth += 1;\n else if (char === '}' || char === ']') depth -= 1;\n else if (char === '\"') {\n const start = i;\n for (i += 1; i < json.length && json[i] !== '\"'; i += 1) if (json[i] === '\\\\') i += 1;\n let next = i + 1;\n while (next < json.length && /\\s/.test(json[next])) next += 1;\n if (depth === 1 && json[next] === ':') keys.push(json.slice(start, i + 1));\n }\n }\n return keys;\n}\n\nconst bytes = (value: string) => Buffer.byteLength(value, 'utf8');\nconst sized = (value: unknown, min: number, max: number): value is string =>\n typeof value === 'string' && !LONE_SURROGATE.test(value) && bytes(value) >= min && bytes(value) <= max;\n\n/**\n * Parses `X-Tier0-Visitor`: the unpadded base64url (RFC 4648 §5) of a UTF-8\n * JSON object of at most 8192 bytes. The header is not signed; the front door\n * is the only way into the App and replaces whatever the client sent. Parsing\n * is strict: a byte order mark, a duplicate key, a lone UTF-16 surrogate\n * (e.g. `\"\\ud800\"`), an unknown or missing field, a wrong type or length, a `ws` that\n * is not a positive decimal ID without sign or leading zero, an employee\n * without a grant or a machine visitor with one, or an `app` other than this\n * App's throws `VISITOR_HEADER_INVALID`. Without an App ID\n * (`TIER0_APP_ID` unset) it throws `VISITOR_CONFIGURATION_INVALID`.\n */\nexport function parseVisitorHeader(value: string | null | undefined, options: ParseVisitorOptions = {}): Visitor {\n const appID = options.appID ?? getEnvVar('TIER0_APP_ID')?.trim();\n if (!appID) throw new VisitorError('VISITOR_CONFIGURATION_INVALID');\n const fail = () => new VisitorError('VISITOR_HEADER_INVALID');\n if (typeof value !== 'string' || value.length === 0 || value.length > VISITOR_MAX_HEADER_LENGTH || !/^[A-Za-z0-9_-]+$/.test(value)) throw fail();\n const raw = Buffer.from(value, 'base64url');\n // Only the canonical encoding: no padding, no stray trailing bits.\n if (raw.length > VISITOR_MAX_JSON_BYTES || raw.toString('base64url') !== value) throw fail();\n // A UTF-8 byte order mark is not part of the contract (TextDecoder would drop it silently).\n if (raw[0] === 0xef && raw[1] === 0xbb && raw[2] === 0xbf) throw fail();\n let claims: unknown;\n let json: string;\n try {\n json = new TextDecoder('utf-8', { fatal: true }).decode(raw);\n claims = JSON.parse(json);\n } catch {\n throw fail();\n }\n if (!isObject(claims) || Object.keys(claims).some((key) => !VISITOR_FIELDS.has(key))) throw fail();\n // Every key at most once, each spelling decoded (\"\\u0077s\" is \"ws\").\n const keys = topLevelKeys(json).map((token) => JSON.parse(token) as string);\n if (new Set(keys).size !== keys.length) throw fail();\n const { ws, app, runtime, sub_type: subType, sub, grant, app_roles: appRoles, active_role: activeRole } = claims;\n if (!validID(ws) || app !== appID ||\n (runtime !== 'deployed' && runtime !== 'preview') ||\n (subType !== 'employee' && subType !== 'machine') ||\n !sized(sub, 1, VISITOR_MAX_STRING_BYTES) ||\n // An employee's access always has a grant; a machine visitor has none.\n typeof grant !== 'string' || (subType === 'employee' ? !sized(grant, 1, VISITOR_MAX_STRING_BYTES) : grant !== '') ||\n !Array.isArray(appRoles) || appRoles.some((role) => !sized(role, 1, VISITOR_MAX_STRING_BYTES)) ||\n (activeRole !== undefined && !sized(activeRole, 1, VISITOR_MAX_STRING_BYTES))) throw fail();\n return Object.freeze({\n subType, subject: sub, grant, workspaceID: ws, appID, runtime,\n appRoles: Object.freeze([...new Set(appRoles as string[])]),\n ...(activeRole === undefined ? {} : { activeRole }),\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"cloud-server.js","sourceRoot":"","sources":["../../src/cloud-server.ts"],"names":[],"mappings":"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","sourcesContent":["// Server-only entry point: the App's view of the request it is serving.\nimport { getEnvVar } from './runtime-env.js';\n\nconst maxID = BigInt('9223372036854775807');\n/** A positive decimal int64 without sign or leading zero. */\nconst validID = (value: unknown): value is string =>\n typeof value === 'string' && /^[1-9]\\d{0,18}$/.test(value) && BigInt(value) <= maxID;\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n !!value && typeof value === 'object' && !Array.isArray(value);\n\n/** Set by the front door on every request it forwards to the App, after stripping any `X-Tier0-*` the client sent. */\nexport const VISITOR_HEADER = 'X-Tier0-Visitor';\n\n/** Limit of the JSON before encoding. */\nconst VISITOR_MAX_JSON_BYTES = 8192;\n/** Unpadded base64url of `VISITOR_MAX_JSON_BYTES`. */\nconst VISITOR_MAX_HEADER_LENGTH = Math.ceil((VISITOR_MAX_JSON_BYTES * 4) / 3);\nconst VISITOR_MAX_STRING_BYTES = 128;\nconst VISITOR_FIELDS: ReadonlySet<string> = new Set(['ws', 'app', 'runtime', 'sub_type', 'sub', 'grant', 'app_roles', 'active_role']);\n\nexport type VisitorErrorCode = 'VISITOR_HEADER_INVALID' | 'VISITOR_CONFIGURATION_INVALID';\n\nexport class VisitorError extends Error {\n constructor(public readonly code: VisitorErrorCode) {\n super(code);\n this.name = 'VisitorError';\n }\n}\n\n/** The visitor of a request, from `X-Tier0-Visitor`. */\nexport interface Visitor {\n readonly subType: 'employee';\n /** Employee user ID. */\n readonly subject: string;\n /** Grant of the employee's access. */\n readonly grant: string;\n readonly workspaceID: string;\n readonly appID: string;\n readonly runtime: 'deployed' | 'preview';\n /** Roles of this visitor in the App; in Preview, the roles selected in \"view as\". */\n readonly appRoles: readonly string[];\n /** Only in Preview's \"view as\". */\n readonly activeRole?: string;\n}\n\nexport interface ParseVisitorOptions {\n /** The App the header must name. Defaults to `TIER0_APP_ID`. */\n appID?: string;\n}\n\n/** A UTF-16 surrogate without its pair, as JSON escapes like \"\\ud800\" can produce. */\nconst LONE_SURROGATE = /[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]/;\n\n/**\n * Raw key tokens of the top-level object, for spotting duplicates\n * (`JSON.parse` keeps the last silently). Values of the visitor are strings\n * and arrays of strings, so only depth 1 holds keys.\n */\nfunction topLevelKeys(json: string): string[] {\n const keys: string[] = [];\n let depth = 0;\n for (let i = 0; i < json.length; i += 1) {\n const char = json[i];\n if (char === '{' || char === '[') depth += 1;\n else if (char === '}' || char === ']') depth -= 1;\n else if (char === '\"') {\n const start = i;\n for (i += 1; i < json.length && json[i] !== '\"'; i += 1) if (json[i] === '\\\\') i += 1;\n let next = i + 1;\n while (next < json.length && /\\s/.test(json[next])) next += 1;\n if (depth === 1 && json[next] === ':') keys.push(json.slice(start, i + 1));\n }\n }\n return keys;\n}\n\nconst bytes = (value: string) => Buffer.byteLength(value, 'utf8');\nconst sized = (value: unknown, min: number, max: number): value is string =>\n typeof value === 'string' && !LONE_SURROGATE.test(value) && bytes(value) >= min && bytes(value) <= max;\n\n/**\n * Parses `X-Tier0-Visitor`: the unpadded base64url (RFC 4648 §5) of a UTF-8\n * JSON object of at most 8192 bytes. The header is not signed; the front door\n * is the only way into the App and replaces whatever the client sent. Parsing\n * is strict: a byte order mark, a duplicate key, a lone UTF-16 surrogate\n * (e.g. `\"\\ud800\"`), an unknown or missing field, a wrong type or length, a `ws` that\n * is not a positive decimal ID without sign or leading zero, a non-employee\n * visitor, an empty grant, or an `app` other than this App's throws\n * `VISITOR_HEADER_INVALID`. Without an App ID\n * (`TIER0_APP_ID` unset) it throws `VISITOR_CONFIGURATION_INVALID`.\n */\nexport function parseVisitorHeader(value: string | null | undefined, options: ParseVisitorOptions = {}): Visitor {\n const appID = options.appID ?? getEnvVar('TIER0_APP_ID')?.trim();\n if (!appID) throw new VisitorError('VISITOR_CONFIGURATION_INVALID');\n const fail = () => new VisitorError('VISITOR_HEADER_INVALID');\n if (typeof value !== 'string' || value.length === 0 || value.length > VISITOR_MAX_HEADER_LENGTH || !/^[A-Za-z0-9_-]+$/.test(value)) throw fail();\n const raw = Buffer.from(value, 'base64url');\n // Only the canonical encoding: no padding, no stray trailing bits.\n if (raw.length > VISITOR_MAX_JSON_BYTES || raw.toString('base64url') !== value) throw fail();\n // A UTF-8 byte order mark is not part of the contract (TextDecoder would drop it silently).\n if (raw[0] === 0xef && raw[1] === 0xbb && raw[2] === 0xbf) throw fail();\n let claims: unknown;\n let json: string;\n try {\n json = new TextDecoder('utf-8', { fatal: true }).decode(raw);\n claims = JSON.parse(json);\n } catch {\n throw fail();\n }\n if (!isObject(claims) || Object.keys(claims).some((key) => !VISITOR_FIELDS.has(key))) throw fail();\n // Every key at most once, each spelling decoded (\"\\u0077s\" is \"ws\").\n const keys = topLevelKeys(json).map((token) => JSON.parse(token) as string);\n if (new Set(keys).size !== keys.length) throw fail();\n const { ws, app, runtime, sub_type: subType, sub, grant, app_roles: appRoles, active_role: activeRole } = claims;\n if (!validID(ws) || app !== appID ||\n (runtime !== 'deployed' && runtime !== 'preview') ||\n subType !== 'employee' ||\n !sized(sub, 1, VISITOR_MAX_STRING_BYTES) ||\n !sized(grant, 1, VISITOR_MAX_STRING_BYTES) ||\n !Array.isArray(appRoles) || appRoles.some((role) => !sized(role, 1, VISITOR_MAX_STRING_BYTES)) ||\n (activeRole !== undefined && !sized(activeRole, 1, VISITOR_MAX_STRING_BYTES))) throw fail();\n return Object.freeze({\n subType, subject: sub, grant, workspaceID: ws, appID, runtime,\n appRoles: Object.freeze([...new Set(appRoles as string[])]),\n ...(activeRole === undefined ? {} : { activeRole }),\n });\n}\n"]}
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@ export function visitorOf(request: Request): Visitor | null {
|
|
|
19
19
|
}
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
返回 `{ subType: 'employee'
|
|
22
|
+
返回 `{ subType: 'employee', subject, grant, workspaceID, appID, runtime: 'deployed' | 'preview', appRoles, activeRole? }`。仅接受员工,`grant` 必须为 1–128 字节;旧 machine 头无论是否带 grant 均拒绝,不提供兼容 API。`appRoles` 是访问者在 App 里的角色(预览里是「以某角色查看」选中的角色),`activeRole` 只在预览的「以某角色查看」里出现,只用于展示。
|
|
23
23
|
|
|
24
24
|
App 调平台只有一种身份:App 自身(App Key,平台注入在运行进程的 `TIER0_API_KEY` 里,SDK 直接用)。平台不按访问者收窄 App 的数据,按角色决定当前用户能看、能改哪些记录是 App 自己的事:在服务端按 `appRoles` 过滤查询条件、拒绝写入,返回给浏览器前裁掉不该看的字段。不要把访问者 ID 或角色当成平台调用的参数去「代表」用户。
|
|
25
25
|
|