@passioncode-ai/fabric-agent-adapter 0.39.1 → 0.41.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 (123) hide show
  1. package/CHANGELOG.md +203 -0
  2. package/README.md +18 -6
  3. package/SECURITY.md +27 -6
  4. package/bin/fabric-agent-adapter.js +71 -6
  5. package/package.json +2 -2
  6. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  7. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +42 -66
  8. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +4 -2
  9. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/operator-channel-and-repository.md +55 -0
  10. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +9 -3
  11. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/verification.md +15 -0
  12. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +181 -26
  13. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +66 -5
  14. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +81 -23
  15. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +1 -1
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +42 -33
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +21 -9
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -4
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +104 -0
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/config.example.json +15 -0
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/knowledge.example.json +13 -0
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +465 -0
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +21 -5
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +93 -0
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +2 -1
  28. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +4 -2
  29. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +3 -2
  30. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +5 -4
  31. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +5 -0
  32. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +129 -0
  33. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +3 -1
  34. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +5 -1
  35. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/usage.md +2 -0
  36. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_layout.mjs +48 -19
  37. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +66 -5
  38. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +11 -184
  39. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +204 -0
  40. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/dashboard_kit.py +53 -19
  41. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +288 -0
  42. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-backups.mjs +327 -0
  43. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs +47 -0
  44. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-fixture.mjs +79 -0
  45. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +145 -0
  46. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +848 -0
  47. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-shapes.mjs +340 -0
  48. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +42 -1237
  49. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ceremonies.mjs +7 -18
  50. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-crypto.mjs +41 -0
  51. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +15 -46
  52. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +387 -0
  53. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +11 -68
  54. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs +52 -0
  55. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +231 -0
  56. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +50 -16
  57. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-jsvalue.mjs +67 -0
  58. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +235 -0
  59. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-logs.mjs +136 -0
  60. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +335 -0
  61. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +146 -32
  62. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +69 -0
  63. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-platform.mjs +164 -0
  64. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-profiles.mjs +1 -32
  65. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +122 -0
  66. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +177 -2541
  67. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-share.mjs +1 -8
  68. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +404 -0
  69. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-time.mjs +46 -0
  70. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +458 -0
  71. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +408 -0
  72. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_backups.py +399 -0
  73. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +91 -0
  74. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +64 -1398
  75. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py +116 -0
  76. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +162 -0
  77. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +882 -0
  78. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_shapes.py +457 -0
  79. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ceremonies.py +36 -39
  80. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_crypto.py +336 -0
  81. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +113 -222
  82. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +586 -0
  83. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +44 -405
  84. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +140 -0
  85. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +402 -0
  86. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +6 -2
  87. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +119 -77
  88. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_jsvalue.py +310 -0
  89. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_kit.py +243 -0
  90. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +308 -0
  91. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_logs.py +202 -0
  92. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +431 -0
  93. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +285 -109
  94. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +102 -0
  95. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_platform.py +338 -0
  96. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +32 -39
  97. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +173 -0
  98. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +1 -1
  99. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +298 -3450
  100. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_share.py +8 -8
  101. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +437 -0
  102. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_time.py +120 -0
  103. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +697 -0
  104. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +1621 -0
  105. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +25 -7
  106. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +29 -60
  107. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +4 -2
  108. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +3 -0
  109. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/operator-channel-and-repository.md +55 -0
  110. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +66 -5
  111. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +3 -3
  112. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/native-host-hooks.md +2 -1
  113. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +26 -13
  114. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +122 -1392
  115. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +278 -0
  116. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +539 -0
  117. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_hosts.py +324 -0
  118. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mail.py +173 -0
  119. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +491 -0
  120. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/host_hooks.py +11 -1
  121. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/hosts/context.mjs +25 -2
  122. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/hosts/openclaw/index.mjs +4 -2
  123. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/hosts/opencode/fabric.mjs +2 -1
@@ -20,7 +20,6 @@ in ``fabric_delegation``.
20
20
  from __future__ import annotations
21
21
 
22
22
  import base64
23
- import datetime as _dt
24
23
  import hashlib
25
24
  import json
26
25
  import math
@@ -30,8 +29,39 @@ from pathlib import Path
30
29
  from typing import Any, Dict, List, Optional, Tuple
31
30
 
32
31
  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
32
+ from fabric_crypto import ed25519_verify, is_small_order_key # noqa: E402 - beside this file (FAA-65)
33
+ from fabric_jsvalue import jcs # noqa: E402 - beside this file (FAA-65)
34
34
  import fabric_secrets as _secrets # noqa: E402 - beside this file: the contract's credential detector
35
+ from fabric_crypto import ( # noqa: E402 - the core (FAA-65)
36
+ _P256_B as _B,
37
+ _P256_N as _N,
38
+ _P256_P as _P,
39
+ _der_signature,
40
+ b64url as _b64url,
41
+ b64url_bytes as _b64url_bytes,
42
+ es256_verify,
43
+ is_valid_p256_key,
44
+ node_base64 as _node_base64,
45
+ sha256 as _sha256,
46
+ )
47
+ from fabric_jsvalue import ( # noqa: E402 - the core (FAA-65)
48
+ MISSING as _MISSING,
49
+ contains as _contains,
50
+ get as _get,
51
+ get_string as _s,
52
+ indexed_objects as _indexed,
53
+ js_string as _js_string,
54
+ js_to_number as _js_to_number,
55
+ key_of as _key_of,
56
+ objects as _list,
57
+ safe_integer as _safe_integer,
58
+ same as _same,
59
+ show as _show,
60
+ )
61
+ from fabric_time import ( # noqa: E402 - the core (FAA-65)
62
+ clock_ms as _clock,
63
+ parse_rfc3339_ms as estate_time,
64
+ )
35
65
 
36
66
  ESTATE_RECORD_SCHEMA = "estate-record/1"
37
67
  ESTATE_CHALLENGE_TYPE = "estate-revision/1"
@@ -41,202 +71,12 @@ MAX_VALIDITY_SECONDS = 30 * 24 * 60 * 60
41
71
  CLOCK_SKEW_SECONDS = 60
42
72
  ASSERTION_ALGORITHMS = ("ES256", "EdDSA")
43
73
  FLAGS = {"UP": 0x01, "UV": 0x04, "BE": 0x08, "BS": 0x10, "AT": 0x40, "ED": 0x80}
44
- _MISSING = object()
45
74
  _B64URL = re.compile(r"[A-Za-z0-9_-]+")
46
75
  _SURROGATE = re.compile("[\ud800-\udfff]")
47
76
 
48
77
 
49
78
  # --- JavaScript's view of JSON values, which the contract's answers are written in ---------------------------------
50
79
 
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
80
 
241
81
  #: DEC-0082: the personal context, and an organization's context ``org:<orgId>`` (the contract's ``context-route.ts``).
242
82
  PERSONAL_CONTEXT = "personal"
@@ -264,161 +104,8 @@ def _context_of(entry: Any) -> Any:
264
104
  return PERSONAL_CONTEXT if context is _MISSING else context
265
105
 
266
106
 
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
107
  # --- P-256 (DEC-0053; SEC 1 v2, FIPS 186-5) ------------------------------------------------------------------------
296
108
 
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
109
 
423
110
  # --- WebAuthn Level 3 §7.2, offline (src/webauthn.ts) ----------------------------------------------------------------
424
111
 
@@ -451,7 +138,7 @@ def credential_key(public_key: Any, alg: str) -> Dict[str, Any]:
451
138
  raw = _b64url_bytes(public_key["x"])
452
139
  if len(raw) != 32:
453
140
  return {"problem": "unusable-key"}
454
- if _delegation.is_small_order_key(raw):
141
+ if is_small_order_key(raw):
455
142
  return {"problem": "small-order-key"} # DEC-0045 (CT-29): refused before any use
456
143
  return {"key": ("EdDSA", raw)}
457
144
 
@@ -522,7 +209,7 @@ def verify_assertion(assertion: Any, credential: Dict[str, Any], challenge: byte
522
209
  signature = _b64url_bytes(sig)
523
210
  key = found["key"]
524
211
  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))
212
+ else len(signature) == 64 and ed25519_verify(key[1], signed, signature))
526
213
  if not good:
527
214
  return fail("bad-signature")
528
215
  return {"ok": True, "flags": flags, "signCount": int.from_bytes(auth_data[33:37], "big")}
@@ -534,11 +221,11 @@ def estate_record_digest(record: Dict[str, Any]) -> str:
534
221
  """`sha256:` and the hex SHA-256 of the UTF-8 RFC 8785 form of the record without `digest` and `signature`.
535
222
  Raises TypeError for a value RFC 8785 has no form for."""
536
223
  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()
224
+ return "sha256:" + hashlib.sha256(jcs(covered).encode("utf-8")).hexdigest()
538
225
 
539
226
 
540
227
  def estate_challenge_input(digest: str) -> bytes:
541
- return _delegation.jcs({"type": ESTATE_CHALLENGE_TYPE, "digest": digest}).encode("utf-8")
228
+ return jcs({"type": ESTATE_CHALLENGE_TYPE, "digest": digest}).encode("utf-8")
542
229
 
543
230
 
544
231
  def estate_challenge(digest: str) -> bytes:
@@ -546,55 +233,6 @@ def estate_challenge(digest: str) -> bytes:
546
233
  return _sha256(estate_challenge_input(digest))
547
234
 
548
235
 
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
236
  _LABEL = "[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?"
599
237
  #: `sign.` and at least two more labels, lower case: a signing RP ID is never a bare `sign.<tld>` (DEC-0063).
600
238
  _SIGNING_RP = re.compile(r"sign(?:\.%s){2,}" % _LABEL, re.ASCII)
@@ -758,12 +396,13 @@ def _revision_number(value: Any) -> bool:
758
396
  return _safe_integer(value) and value >= 1
759
397
 
760
398
 
761
- def hub_key_certified(record: Any, kid: Any, x: Any, at: Any, applied: Any = None,
399
+ def hub_key_certified(record: Any, kid: Any, x: Any, at: Any, applied: Any = _MISSING,
762
400
  extra_revocations: Any = _MISSING) -> Dict[str, Any]:
763
401
  """DL-14 (DEC-0055, DEC-0060): whether ``record`` certifies the delegation key ``kid`` with the published key's ``x``
764
402
  at ``at`` (an aware datetime or epoch seconds), for a verifier whose machine applied ``applied``
765
- (``{"revision", "digest"?}``). ``extra_revocations`` are the revocation delta's entries applied since the revision
766
- (DEC-0054). The contract's ``hubKeyCertified``, in its order; never raises. Answers ``{"certified": True, "machine",
403
+ (``{"revision", "digest"?}``; ``_MISSING``, the default, when none is known; an explicit None is no revision and
404
+ fails closed as ``estate-unreadable``, as Node's null does, decision D3). ``extra_revocations`` are the revocation
405
+ delta's entries applied since the revision (DEC-0054). The contract's ``hubKeyCertified``, in its order; never raises. Answers ``{"certified": True, "machine",
767
406
  "revision"}`` or ``{"certified": False, "fault": "token"|"estate"|"clock", "detail"}`` with detail
768
407
  ``kid-not-certified`` (the token's fault), ``estate-unreadable``, ``estate-outside-window``, ``estate-rolled-back``,
769
408
  ``estate-forked`` (the trust source's) or ``no-clock``."""
@@ -782,7 +421,7 @@ def hub_key_certified(record: Any, kid: Any, x: Any, at: Any, applied: Any = Non
782
421
  # A window that is not a time, not positive or longer than 30 days is no window: NaN fails every comparison below.
783
422
  if math.isnan(issued_at) or math.isnan(valid_until) or valid_until <= issued_at or valid_until - issued_at > _MAX_WINDOW_MS:
784
423
  return _refused("estate", "estate-unreadable")
785
- if applied is not None:
424
+ if applied is not _MISSING:
786
425
  if (not isinstance(applied, dict) or not _revision_number(_get(applied, "revision"))
787
426
  or (_get(applied, "digest") is not _MISSING and not isinstance(_get(applied, "digest"), str))):
788
427
  return _refused("estate", "estate-unreadable")
@@ -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,140 @@
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
+ store_lock,
18
+ )
19
+ from fabric_paths import (
20
+ atomic_write,
21
+ )
22
+ from fabric_protocols import (
23
+ feed_data_problems,
24
+ )
25
+
26
+
27
+ EVENTS_DEFAULT_LIMIT = 50
28
+
29
+
30
+ EVENTS_MAX_LIMIT = 200
31
+
32
+
33
+ LEVELS = ("info", "notice", "warning", "error")
34
+
35
+
36
+ _KIND = re.compile(r"^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*){0,5}\Z")
37
+
38
+
39
+ _TRACE_ID = re.compile(r"^(?!0{32}\Z)[0-9a-f]{32}\Z")
40
+
41
+
42
+ _SPAN_ID = re.compile(r"^(?!0{16}\Z)[0-9a-f]{16}\Z")
43
+
44
+
45
+ def make_event(event_id: Any, at: str, kind: str, level: str, text: str, *, subject: Optional[Dict[str, str]] = None,
46
+ link: Optional[str] = None, notify: bool = False, trace_id: Optional[str] = None,
47
+ span_id: Optional[str] = None, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
48
+ """One activity event: one sentence a person reads, never a machine id. `data` (DEC-0070) adds machine-readable
49
+ fields beside the sentence, `{schema, value}`, never a secret: it is refused here, so no feed carries one.
50
+
51
+ An event about traced work carries its trace as a pair, `trace_id` and `span_id`
52
+ (fabric-interop/0.1 C3.4 c); fabric_interop.trace_ids(traceparent) gives both."""
53
+ if level not in LEVELS:
54
+ raise ServiceError("level must be one of %s." % ", ".join(LEVELS))
55
+ if not _KIND.match(kind):
56
+ raise ServiceError("kind %r must be dotted lowercase." % kind)
57
+ text = " ".join(str(text).split())
58
+ if not text:
59
+ raise ServiceError("An event needs a sentence.")
60
+ if link is not None and (not link.startswith("/") or link.startswith("//")):
61
+ raise ServiceError("link must be a path on this service.")
62
+ if (trace_id is None) != (span_id is None):
63
+ raise ServiceError("An event carries traceId and spanId together, or neither.")
64
+ if trace_id is not None and not (_TRACE_ID.match(trace_id) and _SPAN_ID.match(str(span_id))):
65
+ raise ServiceError("traceId is 32 and spanId 16 lowercase hex characters, not all zeros.")
66
+ event: Dict[str, Any] = {"id": str(event_id), "at": at, "kind": kind, "level": level, "text": text[:500]}
67
+ if subject:
68
+ event["subject"] = subject
69
+ if link:
70
+ event["link"] = link
71
+ if notify:
72
+ event["notify"] = True
73
+ if trace_id is not None:
74
+ event["traceId"], event["spanId"] = trace_id, span_id
75
+ if data is not None:
76
+ problems = feed_data_problems(data)
77
+ if problems:
78
+ raise ServiceError("Event data: %s." % "; ".join(problems))
79
+ event["data"] = {"schema": data["schema"], "value": data["value"]}
80
+ return event
81
+
82
+
83
+ def parse_limit(raw: Optional[str]) -> int:
84
+ try:
85
+ value = int(raw) if raw not in (None, "") else EVENTS_DEFAULT_LIMIT
86
+ except ValueError:
87
+ raise ServiceError("limit must be an integer.") from None
88
+ return max(1, min(EVENTS_MAX_LIMIT, value))
89
+
90
+
91
+ def events_page(fetch: Callable[[Optional[str], int], Iterable[Dict[str, Any]]],
92
+ after: Optional[str], limit: int) -> Dict[str, Any]:
93
+ """A cursor page over the log the service already keeps.
94
+
95
+ `fetch(after, limit)` returns events with ids greater than `after` in
96
+ ascending order (or the newest `limit`, ascending, when `after` is None).
97
+ """
98
+ events = list(fetch(after, limit))[:limit]
99
+ cursor = events[-1]["id"] if events else (after or None)
100
+ return {"events": events, "cursor": cursor}
101
+
102
+
103
+ class JsonlEventLog:
104
+ """A minimal append-only log for services that keep none yet. Ids are integers as strings."""
105
+
106
+ def __init__(self, path: Path, keep: int = 5000):
107
+ self.path = Path(path)
108
+ self.keep = keep
109
+ self._lock = store_lock(self.path) # R3: appends from a threaded server's handlers never lose one another
110
+
111
+ def _all(self) -> List[Dict[str, Any]]:
112
+ if not self.path.exists():
113
+ return []
114
+ out = []
115
+ for line in self.path.read_text(encoding="utf-8").splitlines():
116
+ try:
117
+ out.append(json.loads(line))
118
+ except ValueError:
119
+ continue
120
+ return out
121
+
122
+ def append(self, kind: str, level: str, text: str, **extra: Any) -> Dict[str, Any]:
123
+ with self._lock:
124
+ rows = self._all()
125
+ next_id = int(rows[-1]["id"]) + 1 if rows else 1
126
+ event = make_event(next_id, now_iso(), kind, level, text, **extra)
127
+ rows.append(event)
128
+ rows = rows[-self.keep:]
129
+ atomic_write(self.path, ("\n".join(json.dumps(r, ensure_ascii=False) for r in rows) + "\n").encode(), 0o600)
130
+ return event
131
+
132
+ def fetch(self, after: Optional[str], limit: int) -> List[Dict[str, Any]]:
133
+ rows = self._all()
134
+ if after is None:
135
+ return rows[-limit:]
136
+ try:
137
+ floor = int(after)
138
+ except ValueError:
139
+ raise ServiceError("cursor %r is not from this service." % after) from None
140
+ return [r for r in rows if int(r["id"]) > floor][:limit]