@fungi.computer/seam 0.2.1 → 0.2.2

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/dist/index.d.ts CHANGED
@@ -66,7 +66,24 @@ export type SeamReplayStore = Readonly<{
66
66
  export declare class SeamReplayGuard implements SeamReplayStore {
67
67
  #private;
68
68
  /** Record one nonce, refusing capacity rather than evicting live history. */
69
- admit({ nonce, now, expiresAt }: SeamReplayRecord): SeamReplayAdmission;
69
+ admit(record: SeamReplayRecord): SeamReplayAdmission;
70
+ }
71
+ /**
72
+ * Process-local replay history split into independent bounded partitions, one
73
+ * per sender-controlled traffic source (for example one VM behind a shared
74
+ * host). A source that exhausts its own partition is refused there and never
75
+ * evicts or fills another partition. The partition key must be covered by the
76
+ * request signature: a partition is created only by `admit`, which
77
+ * {@link verifySeamRequest} calls after the signature verified, so unsigned
78
+ * traffic allocates nothing. Partitions whose history pruned to empty are
79
+ * dropped before a new one is refused at the partition ceiling.
80
+ */
81
+ export declare class SeamReplayPartitions {
82
+ #private;
83
+ /** `maximumPartitions` is the number of distinct sources that may be live at once. */
84
+ constructor(maximumPartitions: number);
85
+ /** The replay store for one source; nothing is allocated before it admits. */
86
+ partition(key: string): SeamReplayStore;
70
87
  }
71
88
  /**
72
89
  * Verify one request's seam header. `digest` is the receiver's own digest of
package/dist/index.js CHANGED
@@ -109,22 +109,68 @@ export async function signSeamRequest(signer, audience, method, url, digest, now
109
109
  export class SeamReplayGuard {
110
110
  #seen = new Map();
111
111
  /** Record one nonce, refusing capacity rather than evicting live history. */
112
- admit({ nonce, now, expiresAt }) {
113
- this.#prune(now);
114
- if (this.#seen.has(nonce))
115
- return "replayed";
116
- if (this.#seen.size >= maximumNonces)
117
- return "at-capacity";
118
- this.#seen.set(nonce, expiresAt);
119
- return "admitted";
112
+ admit(record) {
113
+ return admitNonce(this.#seen, record);
120
114
  }
121
- #prune(now) {
122
- for (const [seen, expires] of this.#seen) {
123
- // Freshness accepts the boundary instant. Clock steps also mean Map
124
- // insertion order need not be expiry order: inspect every bounded entry.
125
- if (expires < now)
126
- this.#seen.delete(seen);
115
+ }
116
+ /**
117
+ * Process-local replay history split into independent bounded partitions, one
118
+ * per sender-controlled traffic source (for example one VM behind a shared
119
+ * host). A source that exhausts its own partition is refused there and never
120
+ * evicts or fills another partition. The partition key must be covered by the
121
+ * request signature: a partition is created only by `admit`, which
122
+ * {@link verifySeamRequest} calls after the signature verified, so unsigned
123
+ * traffic allocates nothing. Partitions whose history pruned to empty are
124
+ * dropped before a new one is refused at the partition ceiling.
125
+ */
126
+ export class SeamReplayPartitions {
127
+ #partitions = new Map();
128
+ #maximumPartitions;
129
+ /** `maximumPartitions` is the number of distinct sources that may be live at once. */
130
+ constructor(maximumPartitions) {
131
+ if (!Number.isSafeInteger(maximumPartitions) || maximumPartitions < 1)
132
+ throw new RangeError("Seam replay partitions need a positive ceiling");
133
+ this.#maximumPartitions = maximumPartitions;
134
+ }
135
+ /** The replay store for one source; nothing is allocated before it admits. */
136
+ partition(key) {
137
+ return { admit: (record) => this.#admit(key, record) };
138
+ }
139
+ #admit(key, record) {
140
+ let seen = this.#partitions.get(key);
141
+ if (seen === undefined) {
142
+ if (this.#partitions.size >= this.#maximumPartitions)
143
+ this.#dropEmpty(record.now);
144
+ if (this.#partitions.size >= this.#maximumPartitions)
145
+ return "at-capacity";
146
+ seen = new Map();
147
+ this.#partitions.set(key, seen);
127
148
  }
149
+ return admitNonce(seen, record);
150
+ }
151
+ #dropEmpty(now) {
152
+ for (const [key, seen] of this.#partitions) {
153
+ pruneNonces(seen, now);
154
+ if (seen.size === 0)
155
+ this.#partitions.delete(key);
156
+ }
157
+ }
158
+ }
159
+ function admitNonce(seen, { nonce, now, expiresAt }) {
160
+ pruneNonces(seen, now);
161
+ if (seen.has(nonce))
162
+ return "replayed";
163
+ if (seen.size >= maximumNonces)
164
+ return "at-capacity";
165
+ seen.set(nonce, expiresAt);
166
+ return "admitted";
167
+ }
168
+ function pruneNonces(seen, now) {
169
+ for (const [nonce, expires] of seen) {
170
+ // Freshness accepts the boundary instant. Clock steps also mean Map
171
+ // insertion order need not be expiry order: inspect every bounded entry.
172
+ if (expires < now)
173
+ seen.delete(nonce);
128
174
  }
129
175
  }
130
176
  function seamFields(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fungi.computer/seam",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "author": "Levi <levi@technomancy.dev>",
5
5
  "private": false,
6
6
  "description": "Ed25519 authentication between Fungi Hub and on-box hosts",