@jwilleke/ngdpbase-types 5.41.1 → 5.43.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 (70) hide show
  1. package/lib/src/context/ApiContext.js +1 -1
  2. package/lib/src/context/BaseContext.d.ts +20 -20
  3. package/lib/src/context/BaseContext.js +41 -24
  4. package/lib/src/context/JobContext.d.ts +31 -0
  5. package/lib/src/context/JobContext.js +34 -1
  6. package/lib/src/managers/AddonsManager.d.ts +8 -28
  7. package/lib/src/managers/AgentTokenManager.d.ts +8 -0
  8. package/lib/src/managers/AttachmentManager.d.ts +59 -0
  9. package/lib/src/managers/AuditManager.d.ts +477 -0
  10. package/lib/src/managers/AuthManager.d.ts +247 -15
  11. package/lib/src/managers/AuthManager.js +588 -45
  12. package/lib/src/managers/ConfigurationManager.d.ts +15 -0
  13. package/lib/src/managers/ConsentManager.d.ts +15 -0
  14. package/lib/src/managers/ImportManager.d.ts +15 -0
  15. package/lib/src/managers/PageManager.d.ts +185 -23
  16. package/lib/src/managers/PolicyEvaluator.d.ts +99 -24
  17. package/lib/src/managers/ShareManager.d.ts +3 -2
  18. package/lib/src/managers/UserManager.d.ts +19 -5
  19. package/lib/src/managers/ValidationManager.d.ts +3 -1
  20. package/lib/src/providers/AgentTokenAuthProvider.js +1 -1
  21. package/lib/src/providers/BaseAttachmentProvider.d.ts +4 -0
  22. package/lib/src/providers/BaseAuditProvider.d.ts +492 -0
  23. package/lib/src/providers/BaseAuthProvider.d.ts +4 -0
  24. package/lib/src/providers/BaseCredentialsProvider.d.ts +2 -2
  25. package/lib/src/providers/BaseCredentialsProvider.js +1 -1
  26. package/lib/src/providers/BasicAttachmentProvider.d.ts +9 -0
  27. package/lib/src/providers/FileCredentialsProvider.js +1 -1
  28. package/lib/src/providers/NullAuditProvider.d.ts +82 -0
  29. package/lib/src/providers/TotpAuthProvider.d.ts +55 -0
  30. package/lib/src/providers/TotpAuthProvider.js +145 -0
  31. package/lib/src/security/PolicyDecisionPoint.d.ts +57 -1
  32. package/lib/src/security/PolicyInformationPoint.d.ts +86 -6
  33. package/lib/src/security/permissions.generated.d.ts +8 -0
  34. package/lib/src/types/Consent.d.ts +17 -4
  35. package/lib/src/types/Consent.js +11 -1
  36. package/lib/src/types/Page.d.ts +0 -7
  37. package/lib/src/types/Policy.d.ts +67 -6
  38. package/lib/src/types/Policy.js +6 -0
  39. package/lib/src/types/Provider.d.ts +18 -0
  40. package/lib/src/types/User.d.ts +6 -0
  41. package/lib/src/types/express.d.ts +22 -0
  42. package/lib/src/utils/addonPageSync.d.ts +0 -12
  43. package/lib/src/utils/auditChain.d.ts +83 -0
  44. package/lib/src/utils/auditEventNames.generated.d.ts +2 -0
  45. package/lib/src/utils/auditEventNames.generated.js +2 -0
  46. package/lib/src/utils/auditEvents.d.ts +2 -0
  47. package/lib/src/utils/auditEvents.js +3 -1
  48. package/lib/src/utils/auditHeadWitness.d.ts +97 -0
  49. package/lib/src/utils/auditLifecycle.d.ts +71 -0
  50. package/lib/src/utils/browserDescription.d.ts +18 -0
  51. package/lib/src/utils/browserDescription.js +12 -0
  52. package/lib/src/utils/frontmatterAccess.d.ts +15 -24
  53. package/lib/src/utils/legacyStoreKeys.d.ts +16 -0
  54. package/lib/src/utils/pageWritePolicies.d.ts +55 -0
  55. package/lib/src/utils/permissionCatalog.d.ts +2 -0
  56. package/lib/src/utils/permissionCatalog.js +19 -0
  57. package/lib/src/utils/policyAttributes.d.ts +141 -0
  58. package/lib/src/utils/policyAttributes.js +219 -0
  59. package/lib/src/utils/postureRecord.d.ts +73 -0
  60. package/lib/src/utils/privateStoreAccess.d.ts +24 -4
  61. package/lib/src/utils/privateStoreAccess.js +19 -4
  62. package/lib/src/utils/privateStorePath.d.ts +6 -0
  63. package/lib/src/utils/privateStorePath.js +8 -0
  64. package/lib/src/utils/securityPosture.d.ts +52 -0
  65. package/lib/src/utils/tlsConfig.d.ts +59 -0
  66. package/lib/src/utils/totpSeedCipher.d.ts +4 -0
  67. package/lib/src/utils/totpSeedCipher.js +45 -0
  68. package/lib/src/utils/username.d.ts +13 -2
  69. package/lib/src/utils/username.js +18 -3
  70. package/package.json +5 -2
@@ -253,7 +253,7 @@ export class ApiContext extends BaseContext {
253
253
  // one is asked as #1745 always has: holds the permission, may not act now —
254
254
  // exactly the condition under which the PDP writes STEP_UP_REASON.
255
255
  const stepUp = Boolean(this.request) && await this.holdsPermission(permission);
256
- await this.enforceDeny(permission, { permit: false, applicable: true, reason: stepUp ? STEP_UP_REASON : 'policy_deny' });
256
+ await this.enforceDeny(permission, { permit: false, applicable: true, reason: stepUp ? STEP_UP_REASON : 'policy_deny', policyIds: [] });
257
257
  }
258
258
  /**
259
259
  * Turn a deny into the refusal (#1431) — the one implementation of refusal
@@ -67,19 +67,13 @@ export declare abstract class BaseContext {
67
67
  * asked of the PDP (#1431 step 14): the token and share ceilings, then the
68
68
  * policies, with deny policies and role expansion.
69
69
  *
70
- * For a check against a PAGE, ask `canAccess` on a context that has one —
71
- * resource attributes beat global policy.
72
- *
73
- * #1504: with `attributes` — today `{ 'system-category': … }` — the same
74
- * door asks about pages OF that kind, so a policy on the `system-category`
75
- * resource type can decide (e.g. `page-public`: may this person make pages
76
- * of this system-category public?). It is the policy question alone: unlike
77
- * `canAccess` it walks none of one page's own tiers (private owner,
78
- * frontmatter), so a page's owner gets no pass on it.
70
+ * For a check against a PAGE, ask {@link hasPermissionOn}: the PDP then
71
+ * reads the page's own attributes (its system-category, its owner) from the
72
+ * PIP (#2255). Asked here, a page write (`page-edit`, `-delete`, `-rename`)
73
+ * means "on some category", which is right for an affordance or a token's
74
+ * scope and wrong for a door.
79
75
  */
80
- hasPermission(action: string, attributes?: {
81
- 'system-category'?: unknown;
82
- }): Promise<boolean>;
76
+ hasPermission(action: string): Promise<boolean>;
83
77
  /**
84
78
  * Does this request's subject HOLD the permission (#1635)? Policy alone,
85
79
  * without the step-up freshness rule — for affordances only (show this
@@ -90,16 +84,22 @@ export declare abstract class BaseContext {
90
84
  /** One answer per question for this request — `hasPermission` and `hasPermissionOn` share it. */
91
85
  private cached;
92
86
  /**
93
- * A capability asked about the page it is for (#1539). For a page in a
94
- * vault: the container rule first — the owner, never a role, the same rule
95
- * as Tier 0 of the page tiers — then the capability with the page's
96
- * `vault`, asked about the page itself as the page tiers do — a `page: *`
97
- * grant does not match a vault page's name, so only a policy on the `vault`
98
- * resource type (`vault-owner`) decides there. For any other page, `hasPermission(action)`. A route that acts on
99
- * a named page asks this instead of the bare capability, so a vault page is
100
- * not judged by the caller's site-wide role.
87
+ * A capability asked about the page it is for (#1539, #2255). For a page in
88
+ * a vault: the container rule first — the owner, never a role, the same
89
+ * rule as Tier 0 of the page tiers — then the capability with the page's
90
+ * `vault`, so a policy on the `vault` resource type (`vault-owner`)
91
+ * decides. For any other page, the PDP is asked about the page by name and
92
+ * reads its system-category and owner from the PIP, so the policies on its
93
+ * category decide. A route that acts on a named page asks this instead of
94
+ * the bare capability.
101
95
  */
102
96
  hasPermissionOn(action: string, pageName: string): Promise<boolean>;
97
+ /**
98
+ * {@link hasPermissionOn} as an affordance (#2255): does the subject HOLD
99
+ * `action` on this page, policy alone, without the step-up rule? So the
100
+ * step-up fallback of a page-scoped check asks about the same page.
101
+ */
102
+ holdsPermissionOn(action: string, pageName: string): Promise<boolean>;
103
103
  /**
104
104
  * The identifiers that match audience-style filters — every role, plus the
105
105
  * username when there is one. Used where running the full evaluator per
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
  import { parsePrivatePageName } from '../utils/privateStorePath.js';
3
- import { permitsInVault } from '../utils/privateStoreAccess.js';
3
+ import { holdsInVault, permitsInVault } from '../utils/privateStoreAccess.js';
4
4
  export class BaseContext {
5
5
  /**
6
6
  * The caller's subject as the middleware wrote it, or `null` for a context
@@ -57,25 +57,19 @@ export class BaseContext {
57
57
  * asked of the PDP (#1431 step 14): the token and share ceilings, then the
58
58
  * policies, with deny policies and role expansion.
59
59
  *
60
- * For a check against a PAGE, ask `canAccess` on a context that has one —
61
- * resource attributes beat global policy.
62
- *
63
- * #1504: with `attributes` — today `{ 'system-category': … }` — the same
64
- * door asks about pages OF that kind, so a policy on the `system-category`
65
- * resource type can decide (e.g. `page-public`: may this person make pages
66
- * of this system-category public?). It is the policy question alone: unlike
67
- * `canAccess` it walks none of one page's own tiers (private owner,
68
- * frontmatter), so a page's owner gets no pass on it.
60
+ * For a check against a PAGE, ask {@link hasPermissionOn}: the PDP then
61
+ * reads the page's own attributes (its system-category, its owner) from the
62
+ * PIP (#2255). Asked here, a page write (`page-edit`, `-delete`, `-rename`)
63
+ * means "on some category", which is right for an affordance or a token's
64
+ * scope and wrong for a door.
69
65
  */
70
- async hasPermission(action, attributes) {
66
+ async hasPermission(action) {
71
67
  const pdp = this.engineRef.getManager('PolicyDecisionPoint');
72
68
  if (!pdp)
73
69
  return false;
74
70
  // #637: hand over the already-resolved subject so the PDP need not
75
71
  // resolve the account and its roles again.
76
- return this.cached(attributes ? `${action}\u0000${JSON.stringify(attributes)}` : action, () => (attributes
77
- ? pdp.decide(this.getActor(), { action, resource: { type: 'page', id: '*' }, attributes }).then((d) => d.permit)
78
- : pdp.permits(this.getActor(), action)));
72
+ return this.cached(action, () => pdp.permits(this.getActor(), action));
79
73
  }
80
74
  /**
81
75
  * Does this request's subject HOLD the permission (#1635)? Policy alone,
@@ -101,24 +95,47 @@ export class BaseContext {
101
95
  return promise;
102
96
  }
103
97
  /**
104
- * A capability asked about the page it is for (#1539). For a page in a
105
- * vault: the container rule first — the owner, never a role, the same rule
106
- * as Tier 0 of the page tiers — then the capability with the page's
107
- * `vault`, asked about the page itself as the page tiers do — a `page: *`
108
- * grant does not match a vault page's name, so only a policy on the `vault`
109
- * resource type (`vault-owner`) decides there. For any other page, `hasPermission(action)`. A route that acts on
110
- * a named page asks this instead of the bare capability, so a vault page is
111
- * not judged by the caller's site-wide role.
98
+ * A capability asked about the page it is for (#1539, #2255). For a page in
99
+ * a vault: the container rule first — the owner, never a role, the same
100
+ * rule as Tier 0 of the page tiers — then the capability with the page's
101
+ * `vault`, so a policy on the `vault` resource type (`vault-owner`)
102
+ * decides. For any other page, the PDP is asked about the page by name and
103
+ * reads its system-category and owner from the PIP, so the policies on its
104
+ * category decide. A route that acts on a named page asks this instead of
105
+ * the bare capability.
112
106
  */
113
107
  async hasPermissionOn(action, pageName) {
114
108
  const name = parsePrivatePageName(pageName);
115
- if (!name)
116
- return this.hasPermission(action);
109
+ if (!name) {
110
+ const pdp = this.engineRef.getManager('PolicyDecisionPoint');
111
+ if (!pdp)
112
+ return false;
113
+ // A stand-in decider with no `decide` (test doubles) answers the capability, as `holdsPermission` does.
114
+ if (typeof pdp.decide !== 'function')
115
+ return this.hasPermission(action);
116
+ return this.cached(`${action}\u0000${pageName}`, () => pdp.decide(this.getActor(), { action, resource: { type: 'page', id: pageName } }).then((d) => d.permit));
117
+ }
117
118
  if (!this.hasSubject())
118
119
  return false;
119
120
  const pdp = this.engineRef.getManager('PolicyDecisionPoint');
120
121
  return this.cached(`${action}\u0000${pageName}`, () => permitsInVault(pdp, this.getActor(), action, { owner: name.owner, vault: name.store, resource: pageName }));
121
122
  }
123
+ /**
124
+ * {@link hasPermissionOn} as an affordance (#2255): does the subject HOLD
125
+ * `action` on this page, policy alone, without the step-up rule? So the
126
+ * step-up fallback of a page-scoped check asks about the same page.
127
+ */
128
+ async holdsPermissionOn(action, pageName) {
129
+ const pdp = this.engineRef.getManager('PolicyDecisionPoint');
130
+ if (!pdp || typeof pdp.holds !== 'function')
131
+ return this.hasPermissionOn(action, pageName);
132
+ const name = parsePrivatePageName(pageName);
133
+ if (!name)
134
+ return this.cached(`holds\u0000${action}\u0000${pageName}`, () => pdp.holds(this.getActor(), action, { resource: { type: 'page', id: pageName } }));
135
+ if (!this.hasSubject())
136
+ return false;
137
+ return this.cached(`holds\u0000${action}\u0000${pageName}`, () => holdsInVault(pdp, this.getActor(), action, { owner: name.owner, vault: name.store, resource: pageName }));
138
+ }
122
139
  /**
123
140
  * The identifiers that match audience-style filters — every role, plus the
124
141
  * username when there is one. Used where running the full evaluator per
@@ -67,6 +67,15 @@ export interface JobContext {
67
67
  viaProxy?: ProxyClaim;
68
68
  /** The address the requesting call came from (#1179), when there was one. Provenance for the record. */
69
69
  ipAddress?: string;
70
+ /**
71
+ * The assurance level (1 to 3) of the requester's session when the work was
72
+ * requested (#2261), which is the job's `subject:aal` whenever it runs. Taken
73
+ * from the requester's own session or token, never from an argument, so it
74
+ * cannot exceed it. Absent for work no session asked for (boot, a schedule,
75
+ * the command line) and for a job requested before it was stored: its
76
+ * `subject:aal` is missing and a condition on it fails closed.
77
+ */
78
+ aal?: 1 | 2 | 3;
70
79
  /** ISO 8601. When the work was requested, not when it ran. */
71
80
  requestedAt: string;
72
81
  /** Why, for origins with no person to ask. Free text, for the audit record. */
@@ -84,10 +93,23 @@ export interface JobContext {
84
93
  /** The shape a request-bound context exposes; both WikiContext and ApiContext satisfy it. */
85
94
  export interface RequestIdentity {
86
95
  username?: string | null;
96
+ /** With `signIn`, `aalCap` and `viaToken`, what the level the job stores is read from (#2261). */
97
+ isAuthenticated?: boolean;
98
+ signIn?: {
99
+ aal?: unknown;
100
+ } | null;
101
+ aalCap?: number;
87
102
  viaToken?: AgentTokenGrant;
88
103
  viaShare?: ShareGrant;
89
104
  viaProxy?: ProxyClaim;
90
105
  ipAddress?: string;
106
+ /** An `ApiContext` holds the request's subject here, a `WikiContext` as `userContext`: the level is read from that, not from the wrapper (#2287). */
107
+ subject?: object | null;
108
+ userContext?: object | null;
109
+ /** A `JobContext` handed back in: it already holds the level its requester's session had. */
110
+ origin?: unknown;
111
+ requestedAt?: unknown;
112
+ aal?: unknown;
91
113
  }
92
114
  /**
93
115
  * The username a request-origin job carries when the request had no identity.
@@ -150,6 +172,15 @@ export declare function jobContextFromOperator(username: string, reason: string,
150
172
  * a context to pass, including code that runs with no request at all.
151
173
  */
152
174
  export declare function jobContextFromSystem(systemPrincipal: string, reason: string, now?: Date): JobContext;
175
+ /**
176
+ * Boot work done in one user's own vaults, on that user's behalf (#1815) —
177
+ * the start-up vault upgrades. The job carries the owner's username, so the
178
+ * vault container rule admits it as it admits any job carrying who asked, and
179
+ * what it may do there is the owner's own rights, resolved by policy when
180
+ * each write is decided. Never the system principal, which no vault admits.
181
+ * Like every job it carries no key, so an encrypted vault stays closed to it.
182
+ */
183
+ export declare function jobContextForOwner(owner: string, reason: string, now?: Date): JobContext;
153
184
  /** As {@link jobContextFromSystem}, for work started by a timer rather than at boot. */
154
185
  export declare function jobContextFromSchedule(systemPrincipal: string, reason: string, now?: Date): JobContext;
155
186
  /**
@@ -30,6 +30,7 @@
30
30
  * enqueued by a token-bearing request escapes the agent-token ceiling simply by
31
31
  * becoming asynchronous.
32
32
  */
33
+ import { effectiveAal, toAal } from '../utils/policyAttributes.js';
33
34
  /**
34
35
  * The username a request-origin job carries when the request had no identity.
35
36
  *
@@ -52,6 +53,7 @@ export function jobContextFromRequest(identity, now = new Date()) {
52
53
  return {
53
54
  username: identity?.username ?? ANONYMOUS_USERNAME,
54
55
  origin: 'request',
56
+ ...aalOf(identity),
55
57
  ...(identity?.viaToken ? { viaToken: identity.viaToken } : {}),
56
58
  ...(identity?.viaShare ? { viaShare: identity.viaShare } : {}),
57
59
  ...(identity?.viaProxy ? { viaProxy: proxyClaimForJob(identity.viaProxy) } : {}),
@@ -59,6 +61,24 @@ export function jobContextFromRequest(identity, now = new Date()) {
59
61
  requestedAt: now.toISOString()
60
62
  };
61
63
  }
64
+ /**
65
+ * The level the requester's session or token has right now (#2261), as a field
66
+ * to store with the job; nothing when it has none. Read from the granting
67
+ * session's own subject whatever the caller passed (#2287): a request's subject
68
+ * as is, an `ApiContext`'s `subject`, a `WikiContext`'s `userContext`, or a
69
+ * `JobContext` handed back in, whose stored level is kept as it is.
70
+ */
71
+ function aalOf(identity) {
72
+ if (!identity)
73
+ return {};
74
+ if (typeof identity.origin === 'string' && typeof identity.requestedAt === 'string') {
75
+ const kept = toAal(identity.aal);
76
+ return kept ? { aal: kept } : {};
77
+ }
78
+ const wrapped = identity.subject ?? identity.userContext;
79
+ const aal = effectiveAal(wrapped && typeof wrapped === 'object' ? (wrapped) : identity);
80
+ return aal ? { aal } : {};
81
+ }
62
82
  /** The proxy claim without its key handle (#2047): the actor, grant and scopes stay for the ceiling and the record. */
63
83
  function proxyClaimForJob(claim) {
64
84
  const { keyHandle: _dropped, ...kept } = claim;
@@ -119,6 +139,17 @@ export function jobContextFromSystem(systemPrincipal, reason, now = new Date())
119
139
  reason
120
140
  };
121
141
  }
142
+ /**
143
+ * Boot work done in one user's own vaults, on that user's behalf (#1815) —
144
+ * the start-up vault upgrades. The job carries the owner's username, so the
145
+ * vault container rule admits it as it admits any job carrying who asked, and
146
+ * what it may do there is the owner's own rights, resolved by policy when
147
+ * each write is decided. Never the system principal, which no vault admits.
148
+ * Like every job it carries no key, so an encrypted vault stays closed to it.
149
+ */
150
+ export function jobContextForOwner(owner, reason, now = new Date()) {
151
+ return { username: owner, origin: 'boot', requestedAt: now.toISOString(), reason };
152
+ }
122
153
  /** As {@link jobContextFromSystem}, for work started by a timer rather than at boot. */
123
154
  export function jobContextFromSchedule(systemPrincipal, reason, now = new Date()) {
124
155
  return { ...jobContextFromSystem(systemPrincipal, reason, now), origin: 'schedule' };
@@ -149,7 +180,9 @@ export function toPermissionSubject(ctx) {
149
180
  ...(ctx.viaShare ? { viaShare: ctx.viaShare } : {}),
150
181
  ...(ctx.viaProxy ? { viaProxy: ctx.viaProxy } : {}),
151
182
  // #1999: the job a scheduled run acts as, for the PIP to resolve.
152
- ...(ctx.job ? { job: ctx.job } : {})
183
+ ...(ctx.job ? { job: ctx.job } : {}),
184
+ // #2261: the level stored when it was requested, for `subject:aal`.
185
+ ...(ctx.aal ? { grantedAal: ctx.aal } : {})
153
186
  };
154
187
  }
155
188
  /** One line for a log or an audit record: who, from where, and how. */
@@ -175,6 +175,14 @@ export interface AddonProfileSection {
175
175
  title: string;
176
176
  /** Raw HTML rendered inside the card body; should contain form input fields. */
177
177
  html: string;
178
+ /**
179
+ * Font Awesome icon shown before the title in the card's header, such as
180
+ * `'fa-book'` (#1541). Optional; the host shows `fa-puzzle-piece` when it is
181
+ * missing or not a plain `fa-` name.
182
+ */
183
+ icon?: string;
184
+ /** Optional muted text shown after the title in the card's header (#1541). */
185
+ hint?: string;
178
186
  }
179
187
  /**
180
188
  * One entry per addon that contributed a profile section, returned by
@@ -596,34 +604,6 @@ declare class AddonsManager extends BaseManager {
596
604
  * @param addonName Name of the add-on (for logging)
597
605
  * @param addonPath Filesystem path to the add-on directory
598
606
  */
599
- /**
600
- * Default edit-protection for a seeded addon page (#971, addons.md §3).
601
- *
602
- * §3 settled the mechanism: the seeder stamps frontmatter `access`, which
603
- * Tier 1 of the evaluator honours directly, needs no `PolicyEvaluator`
604
- * change, and is hash-neutral (`pageSourceHash` covers the body only, so
605
- * adding metadata cannot disturb the #920 reseed comparison).
606
- *
607
- * §9 settled *who owns what*, and the two must be read together. A blanket
608
- * admin lock would contradict the operator's decision that domain content
609
- * pages "should be normal pages, purely seeded by the addon" — the addon
610
- * provides a starting corpus and then lets go. So the default follows §9's
611
- * ownership column rather than locking everything:
612
- *
613
- * | Category | Purpose | Default |
614
- * |---|---|---|
615
- * | `system` | Feature UI, site chrome — infrastructure | admin-only edit |
616
- * | `documentation` | Help / docs — addon-owned | admin-only edit |
617
- * | `general` | Domain content, demo — **instance-owned** | no stamp |
618
- * | anything else | unclassified, treated as addon-owned | admin-only edit |
619
- *
620
- * An addon can always override by declaring its own `access`, which is the
621
- * documented escape hatch in §3 and the reason the default is a default.
622
- *
623
- * @param category - The page's resolved `system-category`
624
- * @returns The access object to stamp, or undefined to leave the page unprotected
625
- */
626
- private defaultAddonPageAccess;
627
607
  /**
628
608
  * Surface a page-level seed failure beyond the boot log (#951).
629
609
  *
@@ -82,6 +82,14 @@ export interface AgentTokenRecord {
82
82
  approvedBy?: string;
83
83
  /** The proxy grant it was minted under. Ending that grant ends this token; it works only while the grant is in force. */
84
84
  proxyGrantId?: string;
85
+ /**
86
+ * #2261: the assurance level (1 to 3) of the minting session when the token
87
+ * was made, which is `subject:aal` for every request the token makes. Taken
88
+ * from the session, never from the request, so it cannot exceed it. Absent
89
+ * on a token made before this was stored: its `subject:aal` is missing and a
90
+ * condition on it fails closed.
91
+ */
92
+ aal?: 1 | 2 | 3;
85
93
  }
86
94
  /** A record safe to return over the API — no hash. */
87
95
  export type AgentTokenPublic = Omit<AgentTokenRecord, 'hash'>;
@@ -56,6 +56,8 @@ interface BaseAttachmentProvider {
56
56
  filesInStoreForPage(location: StoreFileLocation, pageName: string): Promise<StoreFileEntry[]>;
57
57
  setFileMentionInStore(location: StoreFileLocation, id: string, pageName: string, mentioned: boolean): Promise<boolean>;
58
58
  deleteFileInStore(location: StoreFileLocation, id: string): Promise<StoreFileEntry | null>;
59
+ readStoreRenumbered(location: StoreFileLocation): Promise<Map<string, string>>;
60
+ noteStoreRenumbered(location: StoreFileLocation, pairs: ReadonlyMap<string, string>): Promise<void>;
59
61
  searchFilesInStore(location: StoreFileLocation, query: AssetQuery): Promise<AssetRecord[]>;
60
62
  privateRecordsInSharedIndex(): Array<{
61
63
  id: string;
@@ -117,6 +119,23 @@ export interface PutOptions {
117
119
  * record names the file by id only, so it is not recorded there.
118
120
  */
119
121
  importedFrom?: string;
122
+ /**
123
+ * A takeout import's file (#2188): the ids it had, the one to try first
124
+ * first, so a page still linking `/attachments/{id}` finds it again. The
125
+ * file is stored under the first that is free ({@link restoreIdIsFree});
126
+ * when none is, it gets a new id, as any upload does. Honoured only in a
127
+ * vault, never a shared folder or the shared pool; the import passes it
128
+ * only for one of its user's own stores. The stored id is in the returned
129
+ * metadata either way.
130
+ */
131
+ restoreIds?: readonly string[];
132
+ /**
133
+ * The ids files on the site held when the caller's run began, from
134
+ * {@link heldFileIds}, read once per run (#2188). With it, a restore checks
135
+ * the set instead of every store's folder on disk, and adds the id it
136
+ * stores to it.
137
+ */
138
+ heldIds?: Set<string>;
120
139
  }
121
140
  /**
122
141
  * The page-tied upload's options (#1396, #1398): today's callers name the page
@@ -216,6 +235,10 @@ declare class AttachmentManager extends BaseManager implements CatalogSource {
216
235
  /** What the last check for vault pages using shared-pool files found, and the degraded reason it set (#1842). */
217
236
  private poolFileFindings;
218
237
  private poolFileReason;
238
+ /** #2188: ids a restore has claimed and not yet written; one id is claimed by one put at a time. */
239
+ private readonly claimedRestoreIds;
240
+ /** #2188: ids a restore has stored since start, which a run's held ids read earlier may not list. */
241
+ private readonly restoredIds;
219
242
  private maxSize;
220
243
  private allowedTypes;
221
244
  /**
@@ -365,6 +388,42 @@ declare class AttachmentManager extends BaseManager implements CatalogSource {
365
388
  * store's file by id only, #2048), and FileManager, the owner, records it.
366
389
  */
367
390
  storeInPlace(ctx: ActorContext, place: FilePlace, file: PutFile, options?: PutOptions): Promise<StoredInPlace>;
391
+ /**
392
+ * Whether a file brought back by a takeout re-import may be stored under
393
+ * the id it had, `id` (#2188). Only an id of the shape a store gives
394
+ * (`isRestorableFileId`), and only when no file holds it: not the target
395
+ * store's index, not the shared pool, and no file on disk in any store of
396
+ * any owner or shared folder. A store names each file's bytes by its id
397
+ * (`{id}.ext`) in the clear, sealed or not, so another owner's sealed store
398
+ * is checked without its key. An id another file holds is never reused, so
399
+ * a takeout cannot claim another file's id, its own or anyone's.
400
+ *
401
+ * With `held` ({@link heldFileIds}, read once by the caller's run), the
402
+ * stores on disk are not read again: the id is free when the set lacks it,
403
+ * unless a restore stored that id since, which only the disk can settle.
404
+ */
405
+ private restoreIdIsFree;
406
+ /**
407
+ * The ids of every file on disk in any store of any owner or shared folder,
408
+ * by the names of their bytes (#2188). With `only`, stops at that id.
409
+ */
410
+ private storeIdsOnDisk;
411
+ /**
412
+ * The ids files in the site's stores hold now, read once, for a takeout
413
+ * import to pass as `PutOptions.heldIds` to each file it puts (#2188).
414
+ * Ids only, from the names of the bytes on disk; no store is opened.
415
+ */
416
+ heldFileIds(): Promise<Set<string>>;
417
+ /**
418
+ * The ids a takeout import gave the files of `ctx`'s own store `store` that
419
+ * could not keep their own, as takeout id → id here (#2188), or null when
420
+ * this context cannot open the store. Ids only.
421
+ */
422
+ ownStoreRenumbered(ctx: ActorContext, store: string): Promise<Map<string, string> | null>;
423
+ /** Adds `pairs` to {@link ownStoreRenumbered}'s, for a run that stored files under new ids (#2188). */
424
+ noteOwnStoreRenumbered(ctx: ActorContext, store: string, pairs: ReadonlyMap<string, string>): Promise<void>;
425
+ /** `ctx`'s own store `store`, as {@link ownStoreFingerprints} reaches it, or null. */
426
+ private ownStoreLocation;
368
427
  /**
369
428
  * The username an upload is recorded under (#1926): the person the context
370
429
  * names. Asked after policy has allowed the upload and before anything is