@joymerrevent/porters-connect 0.19.0 → 0.19.1
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 +65 -1
- package/dist/index.d.cts +21 -8
- package/dist/index.d.ts +21 -8
- package/dist/index.js +96 -16
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,68 @@
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.19.1] - 2026-09-20
|
|
9
|
+
|
|
10
|
+
**定期レビューで見つけた 7 件を塞いだ版**です。破壊的変更はありません。公開 API の形は変わらず、
|
|
11
|
+
**壊れたときの倒れ方**が変わります。
|
|
12
|
+
|
|
13
|
+
いちばん重いのは **Option の選択肢 alias から書き込み XML を注入できた**こと。PORTERS は Option の値を
|
|
14
|
+
**タグ名**として書くため(`<FieldAlias><OptionAlias/></FieldAlias>`)、その値を検証していないと
|
|
15
|
+
**呼び出し側が指定していないレコードが書き換わり**ます。要素名はエスケープできないので、検証して弾く形にしました
|
|
16
|
+
([ADR-0085][adr85])。
|
|
17
|
+
|
|
18
|
+
### Security
|
|
19
|
+
|
|
20
|
+
- **Option の選択肢 alias と書き込み項目 alias を、XML の名前として妥当か検証するようになりました**
|
|
21
|
+
([ADR-0085][adr85])。公開型が `string[]` なので cast なしで到達でき、`<Item>` を閉じて開き直す文字列を
|
|
22
|
+
渡すと **well-formed な XML に別レコードを名指す `<Item>` を注入**できていました(更新先が
|
|
23
|
+
すり替わる/頼んでいない項目が書き足される)。
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
await t.candidate.update(10001, { P_Phase: [userInput] });
|
|
27
|
+
// 不正なら送信前に PortersConfigError(category: "validation")
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
通るのは **XML の `Name`**(英数字・`_`・`-`・`.`・日本語など。先頭に数字や `-` は置けません)。
|
|
31
|
+
出典が alias の書式を定めていないので**XML が許すものはすべて許します** — `Option.P_東京` のような
|
|
32
|
+
alias も従来どおり書けます。**本文になる値(テキスト項目など)の扱いは変わりません。**
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- **`createThrottle` が実質 0 件の上限を受け付けて永久に待つのをやめました**。バケットのトークンは
|
|
37
|
+
`floor(上限 × safety)` 個で、既定 `safety` は 0.9。そのため `createThrottle({ readPerMin: 1 })` は
|
|
38
|
+
容量 0 になり、**すべての呼び出しが返らなく**なっていました(例外もログも無し)。
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
createThrottle({ readPerMin: 1 }); // PortersConfigError(floor(1 × 0.9) = 0)
|
|
42
|
+
createThrottle({ readPerMin: 2 }); // OK(floor(1.8) = 1)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`readPerMin` / `writePerMin` は**正の整数**、`safety` は **0 より大きく 1 以下**。加えて
|
|
46
|
+
**積が 1 以上**であることを見ます。「1 件も通さない」は `take()` が解決しない `Throttle` を
|
|
47
|
+
自分で渡してください。
|
|
48
|
+
|
|
49
|
+
- **XML の解析に失敗したとき、例外が必ず `PortersError` になるようになりました**。
|
|
50
|
+
`fast-xml-parser` は `prototype` / `constructor` / `__proto__` をタグ名として拒否します。
|
|
51
|
+
これらは妥当な XML Name なので書き込みは通り、**読み取りだけ**が素の `Error` で落ちていました
|
|
52
|
+
— `catch (e) { if (e instanceof PortersError) … }` に**引っかからず**、アプリの最上位まで
|
|
53
|
+
素通りします。Read は `PortersResourceError`、認証は `PortersAuthError` に包み、パーサ自身の
|
|
54
|
+
説明は `cause` に残します。壊れた XML も同じ経路になりました。
|
|
55
|
+
|
|
56
|
+
- **公開 API リファレンス**(`docs/usage/api/`)に残っていた日本語を英語に直しました。
|
|
57
|
+
公開サーフェスの JSDoc は英語、README とガイドは日本語ファースト、という切り分けは変わりません。
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
|
|
61
|
+
- **CI が `engines` の下限そのものを走らせるようになりました**。`engines.node` は `>=22.12.0` を
|
|
62
|
+
約束していますが、テストの Node マトリクスは `22`(その時点の 22 系最新に解決される)だったため、
|
|
63
|
+
**22.12.0 は一度も走っていません**でした。Node 22.12.0 で動かしている場合、そのバージョンが
|
|
64
|
+
実際に検証されるようになります。
|
|
65
|
+
|
|
66
|
+
- 内部の検査を 2 本増やしました(利用者への影響はありません)。契約後に実機確認する仮定と
|
|
67
|
+
コード側のコメントの対応を双方向で見る検査と、リファレンスへの日本語混入を見る検査です。
|
|
68
|
+
前者では**コードに仮定があるのに一覧に無いもの**が 3 件見つかり、起票しました。
|
|
69
|
+
|
|
8
70
|
## [0.19.0] - 2026-09-18
|
|
9
71
|
|
|
10
72
|
**CJS からの入口を開け、Node の下限を 22.12 に上げた版**です。**破壊的変更を 1 つ**含みます
|
|
@@ -1062,11 +1124,13 @@ Attachment)あるのに、受け口の形が 3 つとも違っていました
|
|
|
1062
1124
|
[adr82]: docs/adr/0082-module-format-and-node-baseline.md
|
|
1063
1125
|
[limits]: docs/usage/concepts/limits.md
|
|
1064
1126
|
[failures]: docs/usage/howto/handle-failures.md
|
|
1127
|
+
[adr85]: docs/adr/0085-option-alias-validation.md
|
|
1065
1128
|
[lv]: docs/live-verification.md
|
|
1066
1129
|
[ref]: docs/usage/reference/README.md
|
|
1067
1130
|
[kac]: https://keepachangelog.com/en/1.1.0/
|
|
1068
1131
|
[semver]: https://semver.org/
|
|
1069
|
-
[unreleased]: https://github.com/Joymerrevent/porters-connect/compare/v0.19.
|
|
1132
|
+
[unreleased]: https://github.com/Joymerrevent/porters-connect/compare/v0.19.1...HEAD
|
|
1133
|
+
[0.19.1]: https://github.com/Joymerrevent/porters-connect/compare/v0.19.0...v0.19.1
|
|
1070
1134
|
[0.19.0]: https://github.com/Joymerrevent/porters-connect/compare/v0.18.0...v0.19.0
|
|
1071
1135
|
[0.18.0]: https://github.com/Joymerrevent/porters-connect/compare/v0.17.0...v0.18.0
|
|
1072
1136
|
[0.17.0]: https://github.com/Joymerrevent/porters-connect/compare/v0.16.0...v0.17.0
|
package/dist/index.d.cts
CHANGED
|
@@ -110,9 +110,22 @@ type Throttle = {
|
|
|
110
110
|
take(write: boolean): Promise<void>;
|
|
111
111
|
};
|
|
112
112
|
type ThrottleOptions = {
|
|
113
|
+
/**
|
|
114
|
+
* Reads allowed per minute before headroom. Default 2000 (PORTERS' own cap). A positive
|
|
115
|
+
* integer, and **`readPerMin * safety` must still leave at least one token** — see
|
|
116
|
+
* {@link ThrottleOptions.safety}.
|
|
117
|
+
*/
|
|
113
118
|
readPerMin?: number;
|
|
119
|
+
/** Writes allowed per minute before headroom. Default 500. Same rules as `readPerMin`. */
|
|
114
120
|
writePerMin?: number;
|
|
115
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* Fraction of the limit to actually use (headroom). Default 0.9. Greater than 0, at most 1.
|
|
123
|
+
*
|
|
124
|
+
* The bucket holds `floor(limit * safety)` tokens, so a small limit and a small `safety`
|
|
125
|
+
* multiply into **zero capacity** — `{ readPerMin: 1 }` at the default 0.9 already does.
|
|
126
|
+
* A bucket that can never hold a token would make every call wait forever, so the
|
|
127
|
+
* combination is rejected at construction rather than hanging (RV-49).
|
|
128
|
+
*/
|
|
116
129
|
safety?: number;
|
|
117
130
|
now?: () => number;
|
|
118
131
|
};
|
|
@@ -1736,8 +1749,8 @@ type SalesPage = ResourcePage<typeof FIELDS$7>;
|
|
|
1736
1749
|
type SalesSearchQuery = SearchQuery<typeof FIELDS$7, typeof REFERENCES$2>;
|
|
1737
1750
|
/**
|
|
1738
1751
|
* Fields for `create`: only `P_Owner` is unconditionally required. The six references are
|
|
1739
|
-
* required *conditionally* (a dependency chain PORTERS validates server-side)
|
|
1740
|
-
* module comment and docs/usage/concepts/limits.md.
|
|
1752
|
+
* required *conditionally* (a dependency chain PORTERS validates server-side), so they stay
|
|
1753
|
+
* optional here (ADR-0083) — see the module comment and docs/usage/concepts/limits.md.
|
|
1741
1754
|
*/
|
|
1742
1755
|
type SalesCreateInput = CreateInput<typeof FIELDS$7, (typeof REQUIRED_ON_CREATE$3)[number]>;
|
|
1743
1756
|
/** Fields for `update`: all optional (`null` omits, `""` clears a text field). */
|
|
@@ -2337,8 +2350,8 @@ type AttachmentUpdate = {
|
|
|
2337
2350
|
*
|
|
2338
2351
|
* ```ts
|
|
2339
2352
|
* const files = t.attachment.of("resume");
|
|
2340
|
-
* await files.search({ resourceId: 10006 }); //
|
|
2341
|
-
* await files.get(900); //
|
|
2353
|
+
* await files.search({ resourceId: 10006 }); // metadata only
|
|
2354
|
+
* await files.get(900); // with the file body
|
|
2342
2355
|
* ```
|
|
2343
2356
|
*
|
|
2344
2357
|
* PORTERS requires `resource=` on every Attachment Read, and the same value goes into the
|
|
@@ -2646,7 +2659,7 @@ type ReadCustomCatalogOptions = {
|
|
|
2646
2659
|
* The default is `-1` deliberately. Narrowing to `1` would hide fields that exist but are
|
|
2647
2660
|
* unused, and a comparison against a declaration would then report them as **missing** — a
|
|
2648
2661
|
* false alarm. `generateFieldDecls` overrides it to `1`, where "only what is in use" is what
|
|
2649
|
-
* you want in a template (ADR-0069, accept
|
|
2662
|
+
* you want in a template (ADR-0069, decided on accept).
|
|
2650
2663
|
*/
|
|
2651
2664
|
readonly active?: -1 | 0 | 1;
|
|
2652
2665
|
};
|
|
@@ -2669,7 +2682,7 @@ type GenerateFieldDeclsOptions = {
|
|
|
2669
2682
|
* Field Read's `active` filter. Defaults to `1` — **in-use fields only**, which is what belongs
|
|
2670
2683
|
* in a template; there is no reason to declare a field the tenant is not using. This is the
|
|
2671
2684
|
* opposite of `verifyFields`, where narrowing would cause false "missing" reports
|
|
2672
|
-
* (ADR-0069, accept
|
|
2685
|
+
* (ADR-0069, decided on accept).
|
|
2673
2686
|
*/
|
|
2674
2687
|
readonly active?: -1 | 0 | 1;
|
|
2675
2688
|
/**
|
|
@@ -2758,7 +2771,7 @@ type VerifyFieldsOptions = {
|
|
|
2758
2771
|
/**
|
|
2759
2772
|
* Field Read's `active` filter. Defaults to `-1` (every field) and should stay there: with `1`
|
|
2760
2773
|
* a field that exists but is currently unused is absent from the response, and its declaration
|
|
2761
|
-
* would be reported as {@link MissingField} — a false alarm (ADR-0069, accept
|
|
2774
|
+
* would be reported as {@link MissingField} — a false alarm (ADR-0069, decided on accept).
|
|
2762
2775
|
*/
|
|
2763
2776
|
readonly active?: -1 | 0 | 1;
|
|
2764
2777
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -110,9 +110,22 @@ type Throttle = {
|
|
|
110
110
|
take(write: boolean): Promise<void>;
|
|
111
111
|
};
|
|
112
112
|
type ThrottleOptions = {
|
|
113
|
+
/**
|
|
114
|
+
* Reads allowed per minute before headroom. Default 2000 (PORTERS' own cap). A positive
|
|
115
|
+
* integer, and **`readPerMin * safety` must still leave at least one token** — see
|
|
116
|
+
* {@link ThrottleOptions.safety}.
|
|
117
|
+
*/
|
|
113
118
|
readPerMin?: number;
|
|
119
|
+
/** Writes allowed per minute before headroom. Default 500. Same rules as `readPerMin`. */
|
|
114
120
|
writePerMin?: number;
|
|
115
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* Fraction of the limit to actually use (headroom). Default 0.9. Greater than 0, at most 1.
|
|
123
|
+
*
|
|
124
|
+
* The bucket holds `floor(limit * safety)` tokens, so a small limit and a small `safety`
|
|
125
|
+
* multiply into **zero capacity** — `{ readPerMin: 1 }` at the default 0.9 already does.
|
|
126
|
+
* A bucket that can never hold a token would make every call wait forever, so the
|
|
127
|
+
* combination is rejected at construction rather than hanging (RV-49).
|
|
128
|
+
*/
|
|
116
129
|
safety?: number;
|
|
117
130
|
now?: () => number;
|
|
118
131
|
};
|
|
@@ -1736,8 +1749,8 @@ type SalesPage = ResourcePage<typeof FIELDS$7>;
|
|
|
1736
1749
|
type SalesSearchQuery = SearchQuery<typeof FIELDS$7, typeof REFERENCES$2>;
|
|
1737
1750
|
/**
|
|
1738
1751
|
* Fields for `create`: only `P_Owner` is unconditionally required. The six references are
|
|
1739
|
-
* required *conditionally* (a dependency chain PORTERS validates server-side)
|
|
1740
|
-
* module comment and docs/usage/concepts/limits.md.
|
|
1752
|
+
* required *conditionally* (a dependency chain PORTERS validates server-side), so they stay
|
|
1753
|
+
* optional here (ADR-0083) — see the module comment and docs/usage/concepts/limits.md.
|
|
1741
1754
|
*/
|
|
1742
1755
|
type SalesCreateInput = CreateInput<typeof FIELDS$7, (typeof REQUIRED_ON_CREATE$3)[number]>;
|
|
1743
1756
|
/** Fields for `update`: all optional (`null` omits, `""` clears a text field). */
|
|
@@ -2337,8 +2350,8 @@ type AttachmentUpdate = {
|
|
|
2337
2350
|
*
|
|
2338
2351
|
* ```ts
|
|
2339
2352
|
* const files = t.attachment.of("resume");
|
|
2340
|
-
* await files.search({ resourceId: 10006 }); //
|
|
2341
|
-
* await files.get(900); //
|
|
2353
|
+
* await files.search({ resourceId: 10006 }); // metadata only
|
|
2354
|
+
* await files.get(900); // with the file body
|
|
2342
2355
|
* ```
|
|
2343
2356
|
*
|
|
2344
2357
|
* PORTERS requires `resource=` on every Attachment Read, and the same value goes into the
|
|
@@ -2646,7 +2659,7 @@ type ReadCustomCatalogOptions = {
|
|
|
2646
2659
|
* The default is `-1` deliberately. Narrowing to `1` would hide fields that exist but are
|
|
2647
2660
|
* unused, and a comparison against a declaration would then report them as **missing** — a
|
|
2648
2661
|
* false alarm. `generateFieldDecls` overrides it to `1`, where "only what is in use" is what
|
|
2649
|
-
* you want in a template (ADR-0069, accept
|
|
2662
|
+
* you want in a template (ADR-0069, decided on accept).
|
|
2650
2663
|
*/
|
|
2651
2664
|
readonly active?: -1 | 0 | 1;
|
|
2652
2665
|
};
|
|
@@ -2669,7 +2682,7 @@ type GenerateFieldDeclsOptions = {
|
|
|
2669
2682
|
* Field Read's `active` filter. Defaults to `1` — **in-use fields only**, which is what belongs
|
|
2670
2683
|
* in a template; there is no reason to declare a field the tenant is not using. This is the
|
|
2671
2684
|
* opposite of `verifyFields`, where narrowing would cause false "missing" reports
|
|
2672
|
-
* (ADR-0069, accept
|
|
2685
|
+
* (ADR-0069, decided on accept).
|
|
2673
2686
|
*/
|
|
2674
2687
|
readonly active?: -1 | 0 | 1;
|
|
2675
2688
|
/**
|
|
@@ -2758,7 +2771,7 @@ type VerifyFieldsOptions = {
|
|
|
2758
2771
|
/**
|
|
2759
2772
|
* Field Read's `active` filter. Defaults to `-1` (every field) and should stay there: with `1`
|
|
2760
2773
|
* a field that exists but is currently unused is absent from the response, and its declaration
|
|
2761
|
-
* would be reported as {@link MissingField} — a false alarm (ADR-0069, accept
|
|
2774
|
+
* would be reported as {@link MissingField} — a false alarm (ADR-0069, decided on accept).
|
|
2762
2775
|
*/
|
|
2763
2776
|
readonly active?: -1 | 0 | 1;
|
|
2764
2777
|
};
|
package/dist/index.js
CHANGED
|
@@ -265,6 +265,30 @@ var createMockTransport = (handler, options = {}) => {
|
|
|
265
265
|
|
|
266
266
|
// src/http/throttle.ts
|
|
267
267
|
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
268
|
+
var capacityOf = (limit, safety, option) => {
|
|
269
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
270
|
+
throw new PortersConfigError(
|
|
271
|
+
`${option} must be a positive integer, got ${JSON.stringify(limit)}`,
|
|
272
|
+
{
|
|
273
|
+
category: "config",
|
|
274
|
+
hint: "Pass requests per minute (defaults: readPerMin 2000, writePerMin 500)."
|
|
275
|
+
}
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
const capacity = Math.floor(limit * safety);
|
|
279
|
+
if (capacity < 1) {
|
|
280
|
+
throw new PortersConfigError(
|
|
281
|
+
`${option} ${limit} with safety ${safety} leaves no capacity (floor(${limit * safety}) = 0), so every call would wait forever`,
|
|
282
|
+
{
|
|
283
|
+
category: "config",
|
|
284
|
+
// 「まったく通さない」を表現したいなら、それ専用の Throttle を書くのが正しい道。
|
|
285
|
+
// 上限 0 を黙って受けて永久に待つのは、許可と沈黙を混ぜる形(ADR-0047 と同じ考え)。
|
|
286
|
+
hint: `Raise ${option} to at least ${Math.ceil(1 / safety)} at this safety, or raise safety. To block every request on purpose, pass your own Throttle whose take() never resolves.`
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
return capacity;
|
|
291
|
+
};
|
|
268
292
|
var makeBucket = (capacity, now) => {
|
|
269
293
|
const ratePerMs = capacity / 6e4;
|
|
270
294
|
let tokens = capacity;
|
|
@@ -284,9 +308,24 @@ var makeBucket = (capacity, now) => {
|
|
|
284
308
|
};
|
|
285
309
|
var createThrottle = (opts = {}) => {
|
|
286
310
|
const safety = opts.safety ?? 0.9;
|
|
311
|
+
if (!(safety > 0) || safety > 1) {
|
|
312
|
+
throw new PortersConfigError(
|
|
313
|
+
`safety must be greater than 0 and at most 1, got ${JSON.stringify(opts.safety)}`,
|
|
314
|
+
{
|
|
315
|
+
category: "config",
|
|
316
|
+
hint: "safety is the fraction of the limit to use (default 0.9 = 90%). Use 1 to run right at PORTERS' cap."
|
|
317
|
+
}
|
|
318
|
+
);
|
|
319
|
+
}
|
|
287
320
|
const now = opts.now ?? (() => Date.now());
|
|
288
|
-
const read = makeBucket(
|
|
289
|
-
|
|
321
|
+
const read = makeBucket(
|
|
322
|
+
capacityOf(opts.readPerMin ?? 2e3, safety, "readPerMin"),
|
|
323
|
+
now
|
|
324
|
+
);
|
|
325
|
+
const write = makeBucket(
|
|
326
|
+
capacityOf(opts.writePerMin ?? 500, safety, "writePerMin"),
|
|
327
|
+
now
|
|
328
|
+
);
|
|
290
329
|
return { take: (isWrite) => isWrite ? write() : read() };
|
|
291
330
|
};
|
|
292
331
|
var createThrottleRegistry = (make = () => createThrottle()) => {
|
|
@@ -415,19 +454,28 @@ var parser = new XMLParser({
|
|
|
415
454
|
// Stryker disable next-line ArrowFunction,ConditionalExpression,StringLiteral: equivalent — asArray() normalizes regardless
|
|
416
455
|
isArray: (name) => name === "Item"
|
|
417
456
|
});
|
|
457
|
+
var parseXml = (xml, unparseable) => {
|
|
458
|
+
try {
|
|
459
|
+
return parser.parse(xml);
|
|
460
|
+
} catch (cause) {
|
|
461
|
+
throw unparseable(cause);
|
|
462
|
+
}
|
|
463
|
+
};
|
|
418
464
|
var toInt = (v) => {
|
|
419
465
|
const s = asString(v);
|
|
420
466
|
return s === void 0 ? 0 : Number(s);
|
|
421
467
|
};
|
|
422
468
|
var MIDDLEBOX_HINT = "A middlebox (proxy, captive portal, SSO login page, WAF notice) may be answering instead of PORTERS. Check PORTERS_HOST and the network path from this process to the API.";
|
|
423
469
|
var parseResourcePage = (xml, resource) => {
|
|
424
|
-
const
|
|
470
|
+
const unparseable = (cause) => new PortersResourceError("unparseable resource response", {
|
|
471
|
+
category: "unknown",
|
|
472
|
+
cause
|
|
473
|
+
});
|
|
474
|
+
const root = asRecord(parseXml(xml, unparseable));
|
|
425
475
|
const rootKey = root ? Object.keys(root)[0] : void 0;
|
|
426
476
|
const body = root && rootKey ? asRecord(root[rootKey]) : void 0;
|
|
427
477
|
if (!body) {
|
|
428
|
-
throw
|
|
429
|
-
category: "unknown"
|
|
430
|
-
});
|
|
478
|
+
throw unparseable();
|
|
431
479
|
}
|
|
432
480
|
if (rootKey !== resource) {
|
|
433
481
|
throw new PortersResourceError(
|
|
@@ -453,13 +501,15 @@ var parseResourcePage = (xml, resource) => {
|
|
|
453
501
|
};
|
|
454
502
|
};
|
|
455
503
|
var parseWriteResult = (xml) => {
|
|
456
|
-
const
|
|
504
|
+
const unparseable = (cause) => new PortersResourceError("unparseable write response", {
|
|
505
|
+
category: "unknown",
|
|
506
|
+
cause
|
|
507
|
+
});
|
|
508
|
+
const root = asRecord(parseXml(xml, unparseable));
|
|
457
509
|
const rootKey = root ? Object.keys(root)[0] : void 0;
|
|
458
510
|
const body = root && rootKey ? asRecord(root[rootKey]) : void 0;
|
|
459
511
|
if (!body) {
|
|
460
|
-
throw
|
|
461
|
-
category: "unknown"
|
|
462
|
-
});
|
|
512
|
+
throw unparseable();
|
|
463
513
|
}
|
|
464
514
|
const code = toInt(body.Code);
|
|
465
515
|
if (code !== 0) {
|
|
@@ -474,12 +524,14 @@ var parseWriteResult = (xml) => {
|
|
|
474
524
|
});
|
|
475
525
|
};
|
|
476
526
|
var parseAuthentication = (xml) => {
|
|
477
|
-
const
|
|
527
|
+
const unparseable = (cause) => new PortersAuthError("unparseable authentication response", {
|
|
528
|
+
category: "unknown",
|
|
529
|
+
cause
|
|
530
|
+
});
|
|
531
|
+
const root = asRecord(parseXml(xml, unparseable));
|
|
478
532
|
const body = root ? asRecord(root.Authentication) : void 0;
|
|
479
533
|
if (!body) {
|
|
480
|
-
throw
|
|
481
|
-
category: "unknown"
|
|
482
|
-
});
|
|
534
|
+
throw unparseable();
|
|
483
535
|
}
|
|
484
536
|
const error = toInt(body.Error);
|
|
485
537
|
if (error !== 0) {
|
|
@@ -978,6 +1030,12 @@ var paginateOnce = async function* (prepare) {
|
|
|
978
1030
|
yield* paginate(prepare());
|
|
979
1031
|
};
|
|
980
1032
|
|
|
1033
|
+
// src/util/xml-name.ts
|
|
1034
|
+
var NAME_START_CHAR = ":A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD\\u{10000}-\\u{EFFFF}";
|
|
1035
|
+
var NAME_CHAR = NAME_START_CHAR + "\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040";
|
|
1036
|
+
var XML_NAME = new RegExp(`^[${NAME_START_CHAR}][${NAME_CHAR}]*$`, "u");
|
|
1037
|
+
var isXmlName = (value) => XML_NAME.test(value);
|
|
1038
|
+
|
|
981
1039
|
// src/xml/encode.ts
|
|
982
1040
|
var IMAGE_CONTENT_TYPES = [
|
|
983
1041
|
"image/jpeg",
|
|
@@ -989,6 +1047,19 @@ var escapeXml = (s) => s.replace(
|
|
|
989
1047
|
/[&<>]/g,
|
|
990
1048
|
(c) => c === "&" ? "&" : c === "<" ? "<" : ">"
|
|
991
1049
|
);
|
|
1050
|
+
var assertTagName = (name, kind, alias) => {
|
|
1051
|
+
if (isXmlName(name)) return;
|
|
1052
|
+
throw new PortersConfigError(
|
|
1053
|
+
`${alias}: ${kind} ${JSON.stringify(name)} is not a valid XML element name`,
|
|
1054
|
+
{
|
|
1055
|
+
category: "validation",
|
|
1056
|
+
// PORTERS writes this value as a tag (`<FieldAlias><OptionAlias/></FieldAlias>`), so an
|
|
1057
|
+
// arbitrary string cannot be sent. Say that, and say where a real one comes from.
|
|
1058
|
+
hint: "PORTERS writes it as an XML element name, so it must be a valid XML Name (letters, digits, `_`, `-`, `.`, no spaces or markup). Option aliases come from the Option master (`t.option`) or the field's option list in the reference.",
|
|
1059
|
+
context: { operation: "encode" }
|
|
1060
|
+
}
|
|
1061
|
+
);
|
|
1062
|
+
};
|
|
992
1063
|
var text = (v) => typeof v === "object" && !Array.isArray(v) ? JSON.stringify(v) : String(v);
|
|
993
1064
|
var scalar = (v) => escapeXml(text(v));
|
|
994
1065
|
var IMAGE_SUBFIELDS = [
|
|
@@ -1025,7 +1096,10 @@ var encodeField = (type, value, alias) => {
|
|
|
1025
1096
|
// array (ADR-0017, symmetric with read); a lone string is wrapped as a 1-element
|
|
1026
1097
|
// selection (fail-safe).
|
|
1027
1098
|
case "Option":
|
|
1028
|
-
return (Array.isArray(value) ? value : [text(value)]).map((
|
|
1099
|
+
return (Array.isArray(value) ? value : [text(value)]).map((selected) => {
|
|
1100
|
+
assertTagName(selected, "option alias", alias);
|
|
1101
|
+
return `<${selected}/>`;
|
|
1102
|
+
}).join("");
|
|
1029
1103
|
// System[DateTime] (registration/update) is Write-restricted by PORTERS; we still
|
|
1030
1104
|
// serialize it identically — rejecting the write is the input type's job (SD-3).
|
|
1031
1105
|
case "DateTime":
|
|
@@ -1065,6 +1139,7 @@ var encodeItem = (prefix, fields, item) => {
|
|
|
1065
1139
|
const parts = [];
|
|
1066
1140
|
for (const [alias, value] of Object.entries(item)) {
|
|
1067
1141
|
if (value === null || value === void 0) continue;
|
|
1142
|
+
assertTagName(alias, "field alias", alias);
|
|
1068
1143
|
const type = fields.get(alias);
|
|
1069
1144
|
const inner = type === void 0 || type === null ? scalar(value) : encodeField(type, value, alias);
|
|
1070
1145
|
const tag2 = qualify(prefix, alias);
|
|
@@ -1860,7 +1935,8 @@ var FIELDS7 = {
|
|
|
1860
1935
|
P_Memo: "MultilineText",
|
|
1861
1936
|
// VERIFY(live): participants is a `User` field that can hold several people. Whether the
|
|
1862
1937
|
// Read response repeats the nested <User> element (and how) is unconfirmed; the decoder
|
|
1863
|
-
// takes the first, like every other User field
|
|
1938
|
+
// takes the first, like every other User field — so a second participant would be dropped
|
|
1939
|
+
// silently. docs/live-verification.md (LV-27).
|
|
1864
1940
|
P_EventParticipants: "User",
|
|
1865
1941
|
P_EventResources: "Option",
|
|
1866
1942
|
// 削除状態("0" / "1")。PORTERS が Data Type を与えていない項目=`null`(ADR-0056。
|
|
@@ -2454,6 +2530,10 @@ var createFieldAccessor = (deps) => ({
|
|
|
2454
2530
|
var FIELDS16 = {
|
|
2455
2531
|
P_Id: "System[Id]",
|
|
2456
2532
|
P_Name: "SinglelineText",
|
|
2533
|
+
// VERIFY(live): LV-26。選択肢 alias は Write でタグ名になるので ADR-0085 が XML Name を要求するが、
|
|
2534
|
+
// ここは**スカラのテキスト**として読むだけで、読めた値がその範囲に収まる保証は無い
|
|
2535
|
+
// (Read 応答の入れ子タグ名から来る値は定義上 Name — 保証が効かないのはこの経路だけ)。
|
|
2536
|
+
// docs/live-verification.md(LV-26)。
|
|
2457
2537
|
P_Alias: "SinglelineText",
|
|
2458
2538
|
P_ParentId: "Number",
|
|
2459
2539
|
P_Type: "Number",
|