@passioncode-ai/fabric-agent-adapter 0.39.1 → 0.40.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.
Files changed (86) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +9 -3
  3. package/package.json +1 -1
  4. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  5. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
  6. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +15 -4
  7. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +64 -0
  8. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/config.example.json +15 -0
  9. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/knowledge.example.json +13 -0
  10. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +264 -0
  11. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +93 -0
  12. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +3 -2
  13. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +128 -0
  14. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/usage.md +2 -0
  15. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +10 -183
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +204 -0
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +286 -0
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-backups.mjs +327 -0
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs +47 -0
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-fixture.mjs +79 -0
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +840 -0
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-shapes.mjs +340 -0
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +42 -1237
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ceremonies.mjs +1 -16
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-crypto.mjs +41 -0
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +13 -45
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +387 -0
  28. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +8 -67
  29. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs +52 -0
  30. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +231 -0
  31. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +2 -2
  32. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-jsvalue.mjs +67 -0
  33. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +202 -0
  34. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-logs.mjs +136 -0
  35. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +335 -0
  36. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +2 -12
  37. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +69 -0
  38. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-platform.mjs +164 -0
  39. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-profiles.mjs +1 -32
  40. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +122 -0
  41. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +177 -2541
  42. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-share.mjs +1 -8
  43. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +400 -0
  44. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-time.mjs +46 -0
  45. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +458 -0
  46. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +402 -0
  47. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_backups.py +399 -0
  48. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +78 -0
  49. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +64 -1398
  50. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py +116 -0
  51. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +902 -0
  52. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_shapes.py +457 -0
  53. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ceremonies.py +36 -38
  54. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_crypto.py +289 -0
  55. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +15 -180
  56. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +586 -0
  57. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +39 -400
  58. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +137 -0
  59. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +402 -0
  60. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +1 -6
  61. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_jsvalue.py +310 -0
  62. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_kit.py +243 -0
  63. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +308 -0
  64. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_logs.py +202 -0
  65. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +431 -0
  66. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +21 -43
  67. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +102 -0
  68. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_platform.py +338 -0
  69. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +31 -38
  70. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +173 -0
  71. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +298 -3450
  72. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_share.py +8 -8
  73. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +412 -0
  74. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_time.py +120 -0
  75. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +697 -0
  76. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +1526 -0
  77. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
  78. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +3 -0
  79. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
  80. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +116 -1392
  81. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +270 -0
  82. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +508 -0
  83. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_hosts.py +265 -0
  84. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mail.py +155 -0
  85. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +335 -0
  86. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/host_hooks.py +1 -0
@@ -30,8 +30,39 @@ from pathlib import Path
30
30
  from typing import Any, Dict, List, Optional, Tuple
31
31
 
32
32
  sys.path.insert(0, str(Path(__file__).resolve().parent))
33
- import fabric_delegation as _delegation # noqa: E402 - beside this file: RFC 8785, Ed25519, small-order keys
33
+ from fabric_crypto import ed25519_verify, is_small_order_key # noqa: E402 - beside this file (FAA-65)
34
+ from fabric_jsvalue import jcs # noqa: E402 - beside this file (FAA-65)
34
35
  import fabric_secrets as _secrets # noqa: E402 - beside this file: the contract's credential detector
36
+ from fabric_crypto import ( # noqa: E402 - the core (FAA-65)
37
+ _P256_B as _B,
38
+ _P256_N as _N,
39
+ _P256_P as _P,
40
+ _der_signature,
41
+ b64url as _b64url,
42
+ b64url_bytes as _b64url_bytes,
43
+ es256_verify,
44
+ is_valid_p256_key,
45
+ node_base64 as _node_base64,
46
+ sha256 as _sha256,
47
+ )
48
+ from fabric_jsvalue import ( # noqa: E402 - the core (FAA-65)
49
+ MISSING as _MISSING,
50
+ contains as _contains,
51
+ get as _get,
52
+ get_string as _s,
53
+ indexed_objects as _indexed,
54
+ js_string as _js_string,
55
+ js_to_number as _js_to_number,
56
+ key_of as _key_of,
57
+ objects as _list,
58
+ safe_integer as _safe_integer,
59
+ same as _same,
60
+ show as _show,
61
+ )
62
+ from fabric_time import ( # noqa: E402 - the core (FAA-65)
63
+ clock_ms as _clock,
64
+ parse_rfc3339_ms as estate_time,
65
+ )
35
66
 
36
67
  ESTATE_RECORD_SCHEMA = "estate-record/1"
37
68
  ESTATE_CHALLENGE_TYPE = "estate-revision/1"
@@ -41,202 +72,12 @@ MAX_VALIDITY_SECONDS = 30 * 24 * 60 * 60
41
72
  CLOCK_SKEW_SECONDS = 60
42
73
  ASSERTION_ALGORITHMS = ("ES256", "EdDSA")
43
74
  FLAGS = {"UP": 0x01, "UV": 0x04, "BE": 0x08, "BS": 0x10, "AT": 0x40, "ED": 0x80}
44
- _MISSING = object()
45
75
  _B64URL = re.compile(r"[A-Za-z0-9_-]+")
46
76
  _SURROGATE = re.compile("[\ud800-\udfff]")
47
77
 
48
78
 
49
79
  # --- JavaScript's view of JSON values, which the contract's answers are written in ---------------------------------
50
80
 
51
- def _js_string(value: Any) -> str:
52
- """String(value) for a JSON value."""
53
- if value is _MISSING:
54
- return "undefined"
55
- if value is None:
56
- return "null"
57
- if value is True:
58
- return "true"
59
- if value is False:
60
- return "false"
61
- if isinstance(value, float):
62
- if math.isnan(value):
63
- return "NaN"
64
- if math.isinf(value):
65
- return "Infinity" if value > 0 else "-Infinity"
66
- return _delegation._number(value)
67
- if isinstance(value, int):
68
- if abs(value) < 2 ** 53:
69
- return str(value)
70
- try:
71
- return _delegation._number(float(value))
72
- except OverflowError: # a JSON number beyond a double is Infinity in JavaScript
73
- return "Infinity" if value > 0 else "-Infinity"
74
- if isinstance(value, dict):
75
- return "[object Object]"
76
- if isinstance(value, list):
77
- return ",".join("" if item is None else _js_string(item) for item in value)
78
- return str(value)
79
-
80
-
81
- def _get(value: Any, key: str) -> Any:
82
- return value.get(key, _MISSING) if isinstance(value, dict) else _MISSING
83
-
84
-
85
- def _s(value: Any, key: str) -> str:
86
- return _js_string(_get(value, key))
87
-
88
-
89
- def _number(value: Any) -> bool:
90
- return isinstance(value, (int, float)) and not isinstance(value, bool)
91
-
92
-
93
- def _double(value: Any) -> float:
94
- """A JSON number as JavaScript holds it: the nearest double, and an integer beyond the double range ±Infinity."""
95
- try:
96
- return float(value)
97
- except OverflowError:
98
- return math.inf if value > 0 else -math.inf
99
-
100
-
101
- def _same(a: Any, b: Any) -> bool:
102
- """JavaScript's === (and a Set's SameValueZero) on parsed JSON: an object equals only itself."""
103
- if a is _MISSING or b is _MISSING:
104
- return a is b
105
- if isinstance(a, (dict, list)) or isinstance(b, (dict, list)):
106
- return a is b
107
- if isinstance(a, bool) or isinstance(b, bool):
108
- return isinstance(a, bool) and isinstance(b, bool) and a == b
109
- if _number(a) and _number(b):
110
- return _double(a) == _double(b) # 2**53 + 1 is 2**53 in JavaScript
111
- return type(a) is type(b) and a == b
112
-
113
-
114
- def _contains(items: List[Any], value: Any) -> bool:
115
- return any(_same(item, value) for item in items)
116
-
117
-
118
- #: The characters JavaScript's StringToNumber trims: WhiteSpace and LineTerminator (ECMA-262 §12.2, §12.3).
119
- _JS_SPACE = "\t\n\v\f\r              

   "
120
- _JS_DECIMAL = re.compile(r"[+-]?(?:(?:[0-9]+\.?[0-9]*|\.[0-9]+)(?:[eE][+-]?[0-9]+)?|Infinity)", re.ASCII)
121
- _JS_RADIX = re.compile(r"0(?:[xX](?P<x>[0-9a-fA-F]+)|[oO](?P<o>[0-7]+)|[bB](?P<b>[01]+))", re.ASCII)
122
-
123
-
124
- def _js_to_number(value: Any) -> float:
125
- """Number(value) for a JSON value, as JavaScript reads it; NaN for what it cannot read as a number."""
126
- if value is _MISSING:
127
- return math.nan
128
- if value is None:
129
- return 0.0
130
- if isinstance(value, bool):
131
- return 1.0 if value else 0.0
132
- if _number(value):
133
- try:
134
- return float(value)
135
- except OverflowError:
136
- return math.inf if value > 0 else -math.inf
137
- if isinstance(value, list):
138
- return _js_to_number(_js_string(value)) # Number([2]) is Number("2"); Number([]) is 0
139
- if isinstance(value, str):
140
- text = value.strip(_JS_SPACE)
141
- if text == "":
142
- return 0.0
143
- if _JS_DECIMAL.fullmatch(text):
144
- return float(text.replace("Infinity", "inf"))
145
- radix = _JS_RADIX.fullmatch(text)
146
- if radix:
147
- digits, base = next((radix.group(k), b) for k, b in (("x", 16), ("o", 8), ("b", 2)) if radix.group(k))
148
- return _double(int(digits, base))
149
- return math.nan
150
-
151
-
152
- def _safe_integer(value: Any) -> bool:
153
- if isinstance(value, int) and not isinstance(value, bool):
154
- return abs(value) <= 2 ** 53 - 1 # checked before any float: a 400-digit integer would overflow
155
- return isinstance(value, float) and math.isfinite(value) and value.is_integer() and abs(value) <= 2 ** 53 - 1
156
-
157
-
158
- def _list(value: Any) -> List[Dict[str, Any]]:
159
- return [item for item in value if isinstance(item, dict)] if isinstance(value, list) else []
160
-
161
-
162
- def _indexed(value: Any) -> List[Tuple[Dict[str, Any], int]]:
163
- """The object items of an array with their index in the array as written, so an instancePath names the item it
164
- flags even when a non-object precedes it (the contract's ``indexed``)."""
165
- return [(item, i) for i, item in enumerate(value) if isinstance(item, dict)] if isinstance(value, list) else []
166
-
167
-
168
- class _Unshowable(Exception):
169
- pass
170
-
171
-
172
- def _strict_string(value: Any) -> str:
173
- """String(value), raising where JavaScript's String() throws: an object whose own ``toString`` is not a function
174
- (every own member of parsed JSON is data), directly or inside an array it joins."""
175
- if isinstance(value, dict):
176
- if "toString" in value:
177
- raise _Unshowable()
178
- return "[object Object]"
179
- if isinstance(value, list):
180
- return ",".join("" if item is None else _strict_string(item) for item in value)
181
- return _js_string(value)
182
-
183
-
184
- _INDEX_KEY = re.compile(r"0|[1-9][0-9]*", re.ASCII)
185
-
186
-
187
- def _js_keys(value: Dict[str, Any]) -> List[str]:
188
- """An object's own keys in the order JavaScript lists them: array-index keys ("0" … "4294967294", no leading zero)
189
- ascending, then the others in the order written."""
190
- index = sorted((k for k in value if _INDEX_KEY.fullmatch(k) and int(k) <= 2 ** 32 - 2), key=int)
191
- taken = set(index)
192
- return index + [k for k in value if k not in taken]
193
-
194
-
195
- def _js_json_string(text: str) -> str:
196
- """JSON.stringify of a string (well-formed, ES2019): a lone surrogate is escaped as \\uXXXX, never refused."""
197
- out = ['"']
198
- for ch in text:
199
- code = ord(ch)
200
- if ch in _delegation._ESCAPES:
201
- out.append(_delegation._ESCAPES[ch])
202
- elif code < 0x20 or 0xD800 <= code <= 0xDFFF:
203
- out.append("\\u%04x" % code)
204
- else:
205
- out.append(ch)
206
- out.append('"')
207
- return "".join(out)
208
-
209
-
210
- def _json_stringify(value: Any) -> str:
211
- """JSON.stringify for a parsed JSON value: members in JavaScript's key order, numbers as JavaScript writes them, a
212
- lone surrogate escaped."""
213
- if isinstance(value, dict):
214
- return "{" + ",".join(_js_json_string(k) + ":" + _json_stringify(value[k]) for k in _js_keys(value)) + "}"
215
- if isinstance(value, list):
216
- return "[" + ",".join(_json_stringify(v) for v in value) + "]"
217
- if value is None:
218
- return "null"
219
- if isinstance(value, bool):
220
- return "true" if value else "false"
221
- if _number(value):
222
- text = _js_string(value)
223
- return "null" if text in ("NaN", "Infinity", "-Infinity") else text
224
- return _js_json_string(value)
225
-
226
-
227
- def _show(value: Any) -> str:
228
- """The contract's ``show``: String(value), or the value's JSON where String() would throw."""
229
- try:
230
- return _strict_string(value)
231
- except _Unshowable:
232
- return _json_stringify(value)
233
-
234
-
235
- def _key_of(*parts: Any) -> str:
236
- """The contract's ``keyOf``: a collision-free key over JSON values, JSON.stringify of the parts (an absent value
237
- is null inside an array, as JavaScript writes it)."""
238
- return _json_stringify([None if part is _MISSING else part for part in parts])
239
-
240
81
 
241
82
  #: DEC-0082: the personal context, and an organization's context ``org:<orgId>`` (the contract's ``context-route.ts``).
242
83
  PERSONAL_CONTEXT = "personal"
@@ -264,161 +105,8 @@ def _context_of(entry: Any) -> Any:
264
105
  return PERSONAL_CONTEXT if context is _MISSING else context
265
106
 
266
107
 
267
- def _b64url_bytes(text: str) -> bytes:
268
- """Buffer.from(text, "base64url") for text of the base64url alphabet: a dangling sixth bit group is ignored."""
269
- return _delegation._b64decode(text)
270
-
271
-
272
- def _b64url(data: bytes) -> str:
273
- return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
274
-
275
-
276
- def _node_base64(text: str) -> bytes:
277
- """Buffer.from(text, "base64"): either alphabet, other characters skipped, stops at the first "=", a dangling
278
- sixth bit group ignored."""
279
- kept = []
280
- for ch in text:
281
- if ch == "=":
282
- break
283
- if ch.isascii() and (ch.isalnum() or ch in "+/-_"):
284
- kept.append({"-": "+", "_": "/"}.get(ch, ch))
285
- body = "".join(kept)
286
- if len(body) % 4 == 1:
287
- body = body[:-1]
288
- return base64.b64decode(body + "=" * (-len(body) % 4))
289
-
290
-
291
- def _sha256(data: bytes) -> bytes:
292
- return hashlib.sha256(data).digest()
293
-
294
-
295
108
  # --- P-256 (DEC-0053; SEC 1 v2, FIPS 186-5) ------------------------------------------------------------------------
296
109
 
297
- _P = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
298
- _A = _P - 3
299
- _B = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B
300
- _N = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551
301
- _GX = 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296
302
- _GY = 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5
303
-
304
-
305
- def is_valid_p256_key(raw: bytes) -> bool:
306
- """SEC 1 §3.2.2 for an uncompressed point: 0x04 ‖ X ‖ Y, X and Y below p, on y² = x³ − 3x + b. The cofactor is 1."""
307
- if len(raw) != 65 or raw[0] != 0x04:
308
- return False
309
- x = int.from_bytes(raw[1:33], "big")
310
- y = int.from_bytes(raw[33:], "big")
311
- if x >= _P or y >= _P:
312
- return False
313
- return (y * y - (x * x * x + _A * x + _B)) % _P == 0
314
-
315
-
316
- def _jacobian_double(p: Tuple[int, int, int]) -> Tuple[int, int, int]:
317
- x, y, z = p
318
- if y == 0 or z == 0:
319
- return (0, 1, 0)
320
- ysq = y * y % _P
321
- s = 4 * x * ysq % _P
322
- zsq = z * z % _P
323
- m = 3 * (x - zsq) * (x + zsq) % _P # a = −3
324
- nx = (m * m - 2 * s) % _P
325
- ny = (m * (s - nx) - 8 * ysq * ysq) % _P
326
- nz = 2 * y * z % _P
327
- return (nx, ny, nz)
328
-
329
-
330
- def _jacobian_add(p: Tuple[int, int, int], q: Tuple[int, int, int]) -> Tuple[int, int, int]:
331
- if p[2] == 0:
332
- return q
333
- if q[2] == 0:
334
- return p
335
- z1z1 = p[2] * p[2] % _P
336
- z2z2 = q[2] * q[2] % _P
337
- u1 = p[0] * z2z2 % _P
338
- u2 = q[0] * z1z1 % _P
339
- s1 = p[1] * q[2] * z2z2 % _P
340
- s2 = q[1] * p[2] * z1z1 % _P
341
- if u1 == u2:
342
- return _jacobian_double(p) if s1 == s2 else (0, 1, 0)
343
- h = (u2 - u1) % _P
344
- r = (s2 - s1) % _P
345
- hh = h * h % _P
346
- hhh = h * hh % _P
347
- v = u1 * hh % _P
348
- nx = (r * r - hhh - 2 * v) % _P
349
- ny = (r * (v - nx) - s1 * hhh) % _P
350
- nz = h * p[2] * q[2] % _P
351
- return (nx, ny, nz)
352
-
353
-
354
- def _shamir(u1: int, u2: int, q: Tuple[int, int]) -> Optional[Tuple[int, int]]:
355
- """u1·G + u2·Q in affine form, or None at infinity."""
356
- g = (_GX, _GY, 1)
357
- qj = (q[0], q[1], 1)
358
- both = _jacobian_add(g, qj)
359
- out = (0, 1, 0)
360
- for bit in range(max(u1.bit_length(), u2.bit_length()) - 1, -1, -1):
361
- out = _jacobian_double(out)
362
- a, b = (u1 >> bit) & 1, (u2 >> bit) & 1
363
- if a and b:
364
- out = _jacobian_add(out, both)
365
- elif a:
366
- out = _jacobian_add(out, g)
367
- elif b:
368
- out = _jacobian_add(out, qj)
369
- if out[2] == 0:
370
- return None
371
- zinv = pow(out[2], -1, _P)
372
- return (out[0] * zinv * zinv % _P, out[1] * zinv * zinv * zinv % _P)
373
-
374
-
375
- def _der_integer(data: bytes, at: int) -> Tuple[Optional[int], int]:
376
- if at + 2 > len(data) or data[at] != 0x02:
377
- return None, at
378
- length = data[at + 1]
379
- start = at + 2
380
- if length == 0 or length > 33 or start + length > len(data):
381
- return None, at
382
- body = data[start:start + length]
383
- if body[0] & 0x80:
384
- return None, at # negative
385
- if length > 1 and body[0] == 0 and not body[1] & 0x80:
386
- return None, at # not minimal
387
- return int.from_bytes(body, "big"), start + length
388
-
389
-
390
- def _der_signature(signature: bytes) -> Optional[Tuple[int, int]]:
391
- """An ASN.1 DER Ecdsa-Sig-Value (RFC 3279 §2.2.3), strictly: one SEQUENCE of two minimal positive INTEGERs."""
392
- if len(signature) < 8 or signature[0] != 0x30:
393
- return None
394
- if signature[1] & 0x80:
395
- return None # a P-256 signature fits a short-form length
396
- if signature[1] != len(signature) - 2:
397
- return None
398
- r, at = _der_integer(signature, 2)
399
- if r is None:
400
- return None
401
- s, at = _der_integer(signature, at)
402
- if s is None or at != len(signature):
403
- return None
404
- return r, s
405
-
406
-
407
- def es256_verify(x: bytes, y: bytes, message: bytes, signature: bytes) -> bool:
408
- """ECDSA P-256 with SHA-256 over `message`, the signature in DER (FIPS 186-5 §6.4.2). False, never an exception."""
409
- if not is_valid_p256_key(b"\x04" + x + y):
410
- return False
411
- parsed = _der_signature(signature)
412
- if parsed is None:
413
- return False
414
- r, s = parsed
415
- if not (1 <= r < _N and 1 <= s < _N):
416
- return False
417
- e = int.from_bytes(_sha256(message), "big")
418
- w = pow(s, -1, _N)
419
- point = _shamir(e * w % _N, r * w % _N, (int.from_bytes(x, "big"), int.from_bytes(y, "big")))
420
- return point is not None and point[0] % _N == r
421
-
422
110
 
423
111
  # --- WebAuthn Level 3 §7.2, offline (src/webauthn.ts) ----------------------------------------------------------------
424
112
 
@@ -451,7 +139,7 @@ def credential_key(public_key: Any, alg: str) -> Dict[str, Any]:
451
139
  raw = _b64url_bytes(public_key["x"])
452
140
  if len(raw) != 32:
453
141
  return {"problem": "unusable-key"}
454
- if _delegation.is_small_order_key(raw):
142
+ if is_small_order_key(raw):
455
143
  return {"problem": "small-order-key"} # DEC-0045 (CT-29): refused before any use
456
144
  return {"key": ("EdDSA", raw)}
457
145
 
@@ -522,7 +210,7 @@ def verify_assertion(assertion: Any, credential: Dict[str, Any], challenge: byte
522
210
  signature = _b64url_bytes(sig)
523
211
  key = found["key"]
524
212
  good = (es256_verify(key[1], key[2], signed, signature) if key[0] == "ES256"
525
- else len(signature) == 64 and _delegation.ed25519_verify(key[1], signed, signature))
213
+ else len(signature) == 64 and ed25519_verify(key[1], signed, signature))
526
214
  if not good:
527
215
  return fail("bad-signature")
528
216
  return {"ok": True, "flags": flags, "signCount": int.from_bytes(auth_data[33:37], "big")}
@@ -534,11 +222,11 @@ def estate_record_digest(record: Dict[str, Any]) -> str:
534
222
  """`sha256:` and the hex SHA-256 of the UTF-8 RFC 8785 form of the record without `digest` and `signature`.
535
223
  Raises TypeError for a value RFC 8785 has no form for."""
536
224
  covered = {k: v for k, v in record.items() if k not in ("digest", "signature")}
537
- return "sha256:" + hashlib.sha256(_delegation.jcs(covered).encode("utf-8")).hexdigest()
225
+ return "sha256:" + hashlib.sha256(jcs(covered).encode("utf-8")).hexdigest()
538
226
 
539
227
 
540
228
  def estate_challenge_input(digest: str) -> bytes:
541
- return _delegation.jcs({"type": ESTATE_CHALLENGE_TYPE, "digest": digest}).encode("utf-8")
229
+ return jcs({"type": ESTATE_CHALLENGE_TYPE, "digest": digest}).encode("utf-8")
542
230
 
543
231
 
544
232
  def estate_challenge(digest: str) -> bytes:
@@ -546,55 +234,6 @@ def estate_challenge(digest: str) -> bytes:
546
234
  return _sha256(estate_challenge_input(digest))
547
235
 
548
236
 
549
- # ASCII digits only: Python's \d also matches other scripts' digits, which Date.parse never reads.
550
- _RFC3339 = re.compile(r"([0-9]{4})-([0-9]{2})-([0-9]{2})[Tt]([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]+)?([Zz]|([+-])([0-9]{2}):([0-9]{2}))", re.ASCII)
551
-
552
-
553
- def _days_from_civil(year: int, month: int, day: int) -> int:
554
- year -= month <= 2
555
- era = (year if year >= 0 else year - 399) // 400
556
- yoe = year - era * 400
557
- doy = (153 * (month + (-3 if month > 2 else 9)) + 2) // 5 + day - 1
558
- doe = yoe * 365 + yoe // 4 - yoe // 100 + doy
559
- return era * 146097 + doe - 719468
560
-
561
-
562
- def estate_time(value: Any) -> float:
563
- """An RFC 3339 date-time as milliseconds since the epoch, or NaN: `T`, an offset and a day that exists are required;
564
- a space and a leap second `:60` are refused (ER-10). Fractions count to the millisecond, as Date.parse reads them."""
565
- match = _RFC3339.fullmatch(value) if isinstance(value, str) else None
566
- if not match:
567
- return math.nan
568
- year, month, day, hour, minute, second = (int(match.group(i)) for i in range(1, 7))
569
- leap = year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
570
- days_in = [31, 29 if leap else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
571
- if not (1 <= month <= 12 and 1 <= day <= days_in[month - 1]) or hour > 23 or minute > 59 or second > 59:
572
- return math.nan
573
- if match.group(9):
574
- oh, om = int(match.group(10)), int(match.group(11))
575
- if oh > 23 or om > 59:
576
- return math.nan
577
- offset = (oh * 60 + om) * (1 if match.group(9) == "+" else -1)
578
- else:
579
- offset = 0
580
- millis = int((match.group(7) or ".")[1:4].ljust(3, "0"))
581
- seconds = _days_from_civil(year, month, day) * 86400 + hour * 3600 + minute * 60 + second - offset * 60
582
- return float(seconds * 1000 + millis)
583
-
584
-
585
- def _clock(at: Any) -> float:
586
- """The verifier's clock as milliseconds: a timezone-aware datetime or epoch seconds; NaN for anything else."""
587
- if isinstance(at, _dt.datetime) and at.tzinfo is not None:
588
- return at.timestamp() * 1000.0
589
- if _number(at):
590
- try:
591
- seconds = float(at)
592
- except OverflowError:
593
- return math.nan
594
- return seconds * 1000.0 if math.isfinite(seconds) else math.nan
595
- return math.nan
596
-
597
-
598
237
  _LABEL = "[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?"
599
238
  #: `sign.` and at least two more labels, lower case: a signing RP ID is never a bare `sign.<tld>` (DEC-0063).
600
239
  _SIGNING_RP = re.compile(r"sign(?:\.%s){2,}" % _LABEL, re.ASCII)
@@ -892,7 +531,7 @@ def _valid_device_key(key: Dict[str, Any]) -> bool:
892
531
  public = _get(key, "publicKey")
893
532
  raw = _node_base64("" if public is _MISSING or public is None else _show(public))
894
533
  if _get(key, "alg") == "ed25519":
895
- return len(raw) == 32 and not _delegation.is_small_order_key(raw)
534
+ return len(raw) == 32 and not is_small_order_key(raw)
896
535
  return is_valid_p256_key(raw)
897
536
 
898
537
 
@@ -932,7 +571,7 @@ def _keys(value: Dict[str, Any], flag) -> None:
932
571
  hub_keys.append(_get(hub, "x"))
933
572
  x = _get(hub, "x")
934
573
  raw = _node_base64("" if x is _MISSING or x is None else _js_string(x))
935
- if _delegation.is_small_order_key(raw):
574
+ if is_small_order_key(raw):
936
575
  flag("%s/hubKey/x" % at, "a hub key of small order verifies forged tokens and is never certified (DEC-0045)")
937
576
  credentials: List[Any] = []
938
577
  for authority, index in _indexed(_get(value, "authorities")):
@@ -1151,7 +790,7 @@ def _record_findings(value: Dict[str, Any]) -> List[Dict[str, str]]:
1151
790
 
1152
791
  def _package_digest(pkg: Dict[str, Any]) -> str:
1153
792
  covered = {k: v for k, v in pkg.items() if k not in ("digest", "signatures")}
1154
- return "sha256:" + hashlib.sha256(_delegation.jcs(covered).encode("utf-8")).hexdigest()
793
+ return "sha256:" + hashlib.sha256(jcs(covered).encode("utf-8")).hexdigest()
1155
794
 
1156
795
 
1157
796
  def _installation(value: Dict[str, Any]) -> List[Dict[str, str]]:
@@ -0,0 +1,137 @@
1
+ """A service's events (DEC-0029): making them, paging them and the JSONL event log. Part of the service kit
2
+ (FAA-65, layer L3)."""
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Any, Callable, Dict, Iterable, List, Optional
9
+
10
+ import sys as _sys # noqa: E402
11
+ from pathlib import Path as _Path # noqa: E402
12
+ if str(_Path(__file__).resolve().parent) not in _sys.path: # the kit loads from any working directory (FAA-65)
13
+ _sys.path.insert(0, str(_Path(__file__).resolve().parent))
14
+ from fabric_base import (
15
+ ServiceError,
16
+ now_iso,
17
+ )
18
+ from fabric_paths import (
19
+ atomic_write,
20
+ )
21
+ from fabric_protocols import (
22
+ feed_data_problems,
23
+ )
24
+
25
+
26
+ EVENTS_DEFAULT_LIMIT = 50
27
+
28
+
29
+ EVENTS_MAX_LIMIT = 200
30
+
31
+
32
+ LEVELS = ("info", "notice", "warning", "error")
33
+
34
+
35
+ _KIND = re.compile(r"^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*){0,5}$")
36
+
37
+
38
+ _TRACE_ID = re.compile(r"^(?!0{32}$)[0-9a-f]{32}$")
39
+
40
+
41
+ _SPAN_ID = re.compile(r"^(?!0{16}$)[0-9a-f]{16}$")
42
+
43
+
44
+ def make_event(event_id: Any, at: str, kind: str, level: str, text: str, *, subject: Optional[Dict[str, str]] = None,
45
+ link: Optional[str] = None, notify: bool = False, trace_id: Optional[str] = None,
46
+ span_id: Optional[str] = None, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
47
+ """One activity event: one sentence a person reads, never a machine id. `data` (DEC-0070) adds machine-readable
48
+ fields beside the sentence, `{schema, value}`, never a secret: it is refused here, so no feed carries one.
49
+
50
+ An event about traced work carries its trace as a pair, `trace_id` and `span_id`
51
+ (fabric-interop/0.1 C3.4 c); fabric_interop.trace_ids(traceparent) gives both."""
52
+ if level not in LEVELS:
53
+ raise ServiceError("level must be one of %s." % ", ".join(LEVELS))
54
+ if not _KIND.match(kind):
55
+ raise ServiceError("kind %r must be dotted lowercase." % kind)
56
+ text = " ".join(str(text).split())
57
+ if not text:
58
+ raise ServiceError("An event needs a sentence.")
59
+ if link is not None and (not link.startswith("/") or link.startswith("//")):
60
+ raise ServiceError("link must be a path on this service.")
61
+ if (trace_id is None) != (span_id is None):
62
+ raise ServiceError("An event carries traceId and spanId together, or neither.")
63
+ if trace_id is not None and not (_TRACE_ID.match(trace_id) and _SPAN_ID.match(str(span_id))):
64
+ raise ServiceError("traceId is 32 and spanId 16 lowercase hex characters, not all zeros.")
65
+ event: Dict[str, Any] = {"id": str(event_id), "at": at, "kind": kind, "level": level, "text": text[:500]}
66
+ if subject:
67
+ event["subject"] = subject
68
+ if link:
69
+ event["link"] = link
70
+ if notify:
71
+ event["notify"] = True
72
+ if trace_id is not None:
73
+ event["traceId"], event["spanId"] = trace_id, span_id
74
+ if data is not None:
75
+ problems = feed_data_problems(data)
76
+ if problems:
77
+ raise ServiceError("Event data: %s." % "; ".join(problems))
78
+ event["data"] = {"schema": data["schema"], "value": data["value"]}
79
+ return event
80
+
81
+
82
+ def parse_limit(raw: Optional[str]) -> int:
83
+ try:
84
+ value = int(raw) if raw not in (None, "") else EVENTS_DEFAULT_LIMIT
85
+ except ValueError:
86
+ raise ServiceError("limit must be an integer.") from None
87
+ return max(1, min(EVENTS_MAX_LIMIT, value))
88
+
89
+
90
+ def events_page(fetch: Callable[[Optional[str], int], Iterable[Dict[str, Any]]],
91
+ after: Optional[str], limit: int) -> Dict[str, Any]:
92
+ """A cursor page over the log the service already keeps.
93
+
94
+ `fetch(after, limit)` returns events with ids greater than `after` in
95
+ ascending order (or the newest `limit`, ascending, when `after` is None).
96
+ """
97
+ events = list(fetch(after, limit))[:limit]
98
+ cursor = events[-1]["id"] if events else (after or None)
99
+ return {"events": events, "cursor": cursor}
100
+
101
+
102
+ class JsonlEventLog:
103
+ """A minimal append-only log for services that keep none yet. Ids are integers as strings."""
104
+
105
+ def __init__(self, path: Path, keep: int = 5000):
106
+ self.path = Path(path)
107
+ self.keep = keep
108
+
109
+ def _all(self) -> List[Dict[str, Any]]:
110
+ if not self.path.exists():
111
+ return []
112
+ out = []
113
+ for line in self.path.read_text(encoding="utf-8").splitlines():
114
+ try:
115
+ out.append(json.loads(line))
116
+ except ValueError:
117
+ continue
118
+ return out
119
+
120
+ def append(self, kind: str, level: str, text: str, **extra: Any) -> Dict[str, Any]:
121
+ rows = self._all()
122
+ next_id = int(rows[-1]["id"]) + 1 if rows else 1
123
+ event = make_event(next_id, now_iso(), kind, level, text, **extra)
124
+ rows.append(event)
125
+ rows = rows[-self.keep:]
126
+ atomic_write(self.path, ("\n".join(json.dumps(r, ensure_ascii=False) for r in rows) + "\n").encode(), 0o600)
127
+ return event
128
+
129
+ def fetch(self, after: Optional[str], limit: int) -> List[Dict[str, Any]]:
130
+ rows = self._all()
131
+ if after is None:
132
+ return rows[-limit:]
133
+ try:
134
+ floor = int(after)
135
+ except ValueError:
136
+ raise ServiceError("cursor %r is not from this service." % after) from None
137
+ return [r for r in rows if int(r["id"]) > floor][:limit]