@yolk-sdk/emulators 0.1.0-canary.96
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/LICENSE +21 -0
- package/README.md +2135 -0
- package/dist/anthropic.d.mts +269 -0
- package/dist/anthropic.d.mts.map +1 -0
- package/dist/anthropic.mjs +177 -0
- package/dist/anthropic.mjs.map +1 -0
- package/dist/chat-completions.d.mts +288 -0
- package/dist/chat-completions.d.mts.map +1 -0
- package/dist/chat-completions.mjs +451 -0
- package/dist/chat-completions.mjs.map +1 -0
- package/dist/codex.d.mts +265 -0
- package/dist/codex.d.mts.map +1 -0
- package/dist/codex.mjs +199 -0
- package/dist/codex.mjs.map +1 -0
- package/dist/dropbox/api.d.mts +63 -0
- package/dist/dropbox/api.d.mts.map +1 -0
- package/dist/dropbox/api.mjs +565 -0
- package/dist/dropbox/api.mjs.map +1 -0
- package/dist/dropbox/state.d.mts +136 -0
- package/dist/dropbox/state.d.mts.map +1 -0
- package/dist/dropbox/state.mjs +209 -0
- package/dist/dropbox/state.mjs.map +1 -0
- package/dist/dropbox.d.mts +79 -0
- package/dist/dropbox.d.mts.map +1 -0
- package/dist/dropbox.mjs +124 -0
- package/dist/dropbox.mjs.map +1 -0
- package/dist/email-fixtures.d.mts +36 -0
- package/dist/email-fixtures.d.mts.map +1 -0
- package/dist/email-fixtures.mjs +1080 -0
- package/dist/email-fixtures.mjs.map +1 -0
- package/dist/email.d.mts +160 -0
- package/dist/email.d.mts.map +1 -0
- package/dist/email.mjs +608 -0
- package/dist/email.mjs.map +1 -0
- package/dist/emulator-compose.d.mts +40 -0
- package/dist/emulator-compose.d.mts.map +1 -0
- package/dist/emulator-compose.mjs +91 -0
- package/dist/emulator-compose.mjs.map +1 -0
- package/dist/emulator-http.d.mts +53 -0
- package/dist/emulator-http.d.mts.map +1 -0
- package/dist/emulator-http.mjs +117 -0
- package/dist/emulator-http.mjs.map +1 -0
- package/dist/emulator-kernel.d.mts +176 -0
- package/dist/emulator-kernel.d.mts.map +1 -0
- package/dist/emulator-kernel.mjs +413 -0
- package/dist/emulator-kernel.mjs.map +1 -0
- package/dist/fixture-route.d.mts +129 -0
- package/dist/fixture-route.d.mts.map +1 -0
- package/dist/fixture-route.mjs +257 -0
- package/dist/fixture-route.mjs.map +1 -0
- package/dist/fortnox/api.d.mts +92 -0
- package/dist/fortnox/api.d.mts.map +1 -0
- package/dist/fortnox/api.mjs +750 -0
- package/dist/fortnox/api.mjs.map +1 -0
- package/dist/fortnox/state.d.mts +533 -0
- package/dist/fortnox/state.d.mts.map +1 -0
- package/dist/fortnox/state.mjs +612 -0
- package/dist/fortnox/state.mjs.map +1 -0
- package/dist/fortnox.d.mts +128 -0
- package/dist/fortnox.d.mts.map +1 -0
- package/dist/fortnox.mjs +403 -0
- package/dist/fortnox.mjs.map +1 -0
- package/dist/gateway-evaluate-recordings.d.mts +12 -0
- package/dist/gateway-evaluate-recordings.d.mts.map +1 -0
- package/dist/gateway-evaluate-recordings.mjs +128 -0
- package/dist/gateway-evaluate-recordings.mjs.map +1 -0
- package/dist/gateway.d.mts +272 -0
- package/dist/gateway.d.mts.map +1 -0
- package/dist/gateway.mjs +400 -0
- package/dist/gateway.mjs.map +1 -0
- package/dist/github/api.d.mts +63 -0
- package/dist/github/api.d.mts.map +1 -0
- package/dist/github/api.mjs +527 -0
- package/dist/github/api.mjs.map +1 -0
- package/dist/github/state.d.mts +272 -0
- package/dist/github/state.d.mts.map +1 -0
- package/dist/github/state.mjs +303 -0
- package/dist/github/state.mjs.map +1 -0
- package/dist/github.d.mts +77 -0
- package/dist/github.d.mts.map +1 -0
- package/dist/github.mjs +180 -0
- package/dist/github.mjs.map +1 -0
- package/dist/google/calendar.d.mts +10 -0
- package/dist/google/calendar.d.mts.map +1 -0
- package/dist/google/calendar.mjs +252 -0
- package/dist/google/calendar.mjs.map +1 -0
- package/dist/google/drive.d.mts +8 -0
- package/dist/google/drive.d.mts.map +1 -0
- package/dist/google/drive.mjs +252 -0
- package/dist/google/drive.mjs.map +1 -0
- package/dist/google/gmail.d.mts +10 -0
- package/dist/google/gmail.d.mts.map +1 -0
- package/dist/google/gmail.mjs +591 -0
- package/dist/google/gmail.mjs.map +1 -0
- package/dist/google/shared.d.mts +107 -0
- package/dist/google/shared.d.mts.map +1 -0
- package/dist/google/shared.mjs +146 -0
- package/dist/google/shared.mjs.map +1 -0
- package/dist/google/state.d.mts +385 -0
- package/dist/google/state.d.mts.map +1 -0
- package/dist/google/state.mjs +496 -0
- package/dist/google/state.mjs.map +1 -0
- package/dist/google.d.mts +75 -0
- package/dist/google.d.mts.map +1 -0
- package/dist/google.mjs +210 -0
- package/dist/google.mjs.map +1 -0
- package/dist/linkedin-search/api.d.mts +45 -0
- package/dist/linkedin-search/api.d.mts.map +1 -0
- package/dist/linkedin-search/api.mjs +181 -0
- package/dist/linkedin-search/api.mjs.map +1 -0
- package/dist/linkedin-search/state.d.mts +123 -0
- package/dist/linkedin-search/state.d.mts.map +1 -0
- package/dist/linkedin-search/state.mjs +261 -0
- package/dist/linkedin-search/state.mjs.map +1 -0
- package/dist/linkedin-search.d.mts +69 -0
- package/dist/linkedin-search.d.mts.map +1 -0
- package/dist/linkedin-search.mjs +158 -0
- package/dist/linkedin-search.mjs.map +1 -0
- package/dist/mcp/api.d.mts +44 -0
- package/dist/mcp/api.d.mts.map +1 -0
- package/dist/mcp/api.mjs +557 -0
- package/dist/mcp/api.mjs.map +1 -0
- package/dist/mcp/recordings.d.mts +40 -0
- package/dist/mcp/recordings.d.mts.map +1 -0
- package/dist/mcp/recordings.mjs +2390 -0
- package/dist/mcp/recordings.mjs.map +1 -0
- package/dist/mcp/state.d.mts +71 -0
- package/dist/mcp/state.d.mts.map +1 -0
- package/dist/mcp/state.mjs +83 -0
- package/dist/mcp/state.mjs.map +1 -0
- package/dist/mcp.d.mts +84 -0
- package/dist/mcp.d.mts.map +1 -0
- package/dist/mcp.mjs +241 -0
- package/dist/mcp.mjs.map +1 -0
- package/dist/messages.d.mts +183 -0
- package/dist/messages.d.mts.map +1 -0
- package/dist/messages.mjs +532 -0
- package/dist/messages.mjs.map +1 -0
- package/dist/microsoft/api.d.mts +47 -0
- package/dist/microsoft/api.d.mts.map +1 -0
- package/dist/microsoft/api.mjs +178 -0
- package/dist/microsoft/api.mjs.map +1 -0
- package/dist/microsoft/calendar.d.mts +30 -0
- package/dist/microsoft/calendar.d.mts.map +1 -0
- package/dist/microsoft/calendar.mjs +271 -0
- package/dist/microsoft/calendar.mjs.map +1 -0
- package/dist/microsoft/drive.d.mts +36 -0
- package/dist/microsoft/drive.d.mts.map +1 -0
- package/dist/microsoft/drive.mjs +298 -0
- package/dist/microsoft/drive.mjs.map +1 -0
- package/dist/microsoft/graph.d.mts +198 -0
- package/dist/microsoft/graph.d.mts.map +1 -0
- package/dist/microsoft/graph.mjs +264 -0
- package/dist/microsoft/graph.mjs.map +1 -0
- package/dist/microsoft/mail.d.mts +48 -0
- package/dist/microsoft/mail.d.mts.map +1 -0
- package/dist/microsoft/mail.mjs +488 -0
- package/dist/microsoft/mail.mjs.map +1 -0
- package/dist/microsoft/state.d.mts +480 -0
- package/dist/microsoft/state.d.mts.map +1 -0
- package/dist/microsoft/state.mjs +625 -0
- package/dist/microsoft/state.mjs.map +1 -0
- package/dist/microsoft.d.mts +168 -0
- package/dist/microsoft.d.mts.map +1 -0
- package/dist/microsoft.mjs +483 -0
- package/dist/microsoft.mjs.map +1 -0
- package/dist/node.d.mts +47 -0
- package/dist/node.d.mts.map +1 -0
- package/dist/node.mjs +178 -0
- package/dist/node.mjs.map +1 -0
- package/dist/notion/api.d.mts +51 -0
- package/dist/notion/api.d.mts.map +1 -0
- package/dist/notion/api.mjs +507 -0
- package/dist/notion/api.mjs.map +1 -0
- package/dist/notion/state.d.mts +329 -0
- package/dist/notion/state.d.mts.map +1 -0
- package/dist/notion/state.mjs +432 -0
- package/dist/notion/state.mjs.map +1 -0
- package/dist/notion.d.mts +72 -0
- package/dist/notion.d.mts.map +1 -0
- package/dist/notion.mjs +127 -0
- package/dist/notion.mjs.map +1 -0
- package/dist/openai.d.mts +165 -0
- package/dist/openai.d.mts.map +1 -0
- package/dist/openai.mjs +141 -0
- package/dist/openai.mjs.map +1 -0
- package/dist/opencode-recordings.d.mts +7 -0
- package/dist/opencode-recordings.d.mts.map +1 -0
- package/dist/opencode-recordings.mjs +222 -0
- package/dist/opencode-recordings.mjs.map +1 -0
- package/dist/opencode.d.mts +70 -0
- package/dist/opencode.d.mts.map +1 -0
- package/dist/opencode.mjs +217 -0
- package/dist/opencode.mjs.map +1 -0
- package/dist/r2-fixtures.d.mts +36 -0
- package/dist/r2-fixtures.d.mts.map +1 -0
- package/dist/r2-fixtures.mjs +245 -0
- package/dist/r2-fixtures.mjs.map +1 -0
- package/dist/r2-guard.d.mts +43 -0
- package/dist/r2-guard.d.mts.map +1 -0
- package/dist/r2-guard.mjs +215 -0
- package/dist/r2-guard.mjs.map +1 -0
- package/dist/r2.d.mts +166 -0
- package/dist/r2.d.mts.map +1 -0
- package/dist/r2.mjs +517 -0
- package/dist/r2.mjs.map +1 -0
- package/dist/responses.d.mts +231 -0
- package/dist/responses.d.mts.map +1 -0
- package/dist/responses.mjs +556 -0
- package/dist/responses.mjs.map +1 -0
- package/dist/route-evidence.d.mts +45 -0
- package/dist/route-evidence.d.mts.map +1 -0
- package/dist/route-evidence.mjs +56 -0
- package/dist/route-evidence.mjs.map +1 -0
- package/dist/router.d.mts +93 -0
- package/dist/router.d.mts.map +1 -0
- package/dist/router.mjs +259 -0
- package/dist/router.mjs.map +1 -0
- package/dist/stateful-core.d.mts +17 -0
- package/dist/stateful-core.d.mts.map +1 -0
- package/dist/stateful-core.mjs +41 -0
- package/dist/stateful-core.mjs.map +1 -0
- package/dist/stateful-emulator.d.mts +565 -0
- package/dist/stateful-emulator.d.mts.map +1 -0
- package/dist/stateful-emulator.mjs +1228 -0
- package/dist/stateful-emulator.mjs.map +1 -0
- package/dist/stateful-secrets.d.mts +84 -0
- package/dist/stateful-secrets.d.mts.map +1 -0
- package/dist/stateful-secrets.mjs +216 -0
- package/dist/stateful-secrets.mjs.map +1 -0
- package/dist/subscription-usage-recordings.d.mts +9 -0
- package/dist/subscription-usage-recordings.d.mts.map +1 -0
- package/dist/subscription-usage-recordings.mjs +53 -0
- package/dist/subscription-usage-recordings.mjs.map +1 -0
- package/dist/subscription-usage.d.mts +53 -0
- package/dist/subscription-usage.d.mts.map +1 -0
- package/dist/subscription-usage.mjs +21 -0
- package/dist/subscription-usage.mjs.map +1 -0
- package/dist/telegram/api.d.mts +34 -0
- package/dist/telegram/api.d.mts.map +1 -0
- package/dist/telegram/api.mjs +257 -0
- package/dist/telegram/api.mjs.map +1 -0
- package/dist/telegram/state.d.mts +128 -0
- package/dist/telegram/state.d.mts.map +1 -0
- package/dist/telegram/state.mjs +154 -0
- package/dist/telegram/state.mjs.map +1 -0
- package/dist/telegram.d.mts +62 -0
- package/dist/telegram.d.mts.map +1 -0
- package/dist/telegram.mjs +127 -0
- package/dist/telegram.mjs.map +1 -0
- package/dist/todoist/api.d.mts +55 -0
- package/dist/todoist/api.d.mts.map +1 -0
- package/dist/todoist/api.mjs +415 -0
- package/dist/todoist/api.mjs.map +1 -0
- package/dist/todoist/state.d.mts +275 -0
- package/dist/todoist/state.d.mts.map +1 -0
- package/dist/todoist/state.mjs +345 -0
- package/dist/todoist/state.mjs.map +1 -0
- package/dist/todoist.d.mts +68 -0
- package/dist/todoist.d.mts.map +1 -0
- package/dist/todoist.mjs +181 -0
- package/dist/todoist.mjs.map +1 -0
- package/dist/xai.d.mts +267 -0
- package/dist/xai.d.mts.map +1 -0
- package/dist/xai.mjs +252 -0
- package/dist/xai.mjs.map +1 -0
- package/package.json +157 -0
- package/src/anthropic.ts +289 -0
- package/src/chat-completions.ts +924 -0
- package/src/codex.ts +296 -0
- package/src/dropbox/api.ts +1084 -0
- package/src/dropbox/state.ts +364 -0
- package/src/dropbox.ts +203 -0
- package/src/email-fixtures.ts +1297 -0
- package/src/email.ts +1108 -0
- package/src/emulator-compose.ts +147 -0
- package/src/emulator-http.ts +184 -0
- package/src/emulator-kernel.ts +844 -0
- package/src/fixture-route.ts +561 -0
- package/src/fortnox/api.ts +1352 -0
- package/src/fortnox/state.ts +798 -0
- package/src/fortnox.ts +801 -0
- package/src/gateway-evaluate-recordings.ts +153 -0
- package/src/gateway.ts +530 -0
- package/src/github/api.ts +986 -0
- package/src/github/state.ts +439 -0
- package/src/github.ts +271 -0
- package/src/google/calendar.ts +486 -0
- package/src/google/drive.ts +471 -0
- package/src/google/gmail.ts +1011 -0
- package/src/google/shared.ts +321 -0
- package/src/google/state.ts +686 -0
- package/src/google.ts +298 -0
- package/src/linkedin-search/api.ts +363 -0
- package/src/linkedin-search/state.ts +373 -0
- package/src/linkedin-search.ts +241 -0
- package/src/mcp/api.ts +995 -0
- package/src/mcp/recordings.ts +2665 -0
- package/src/mcp/state.ts +125 -0
- package/src/mcp.ts +319 -0
- package/src/messages.ts +844 -0
- package/src/microsoft/api.ts +426 -0
- package/src/microsoft/calendar.ts +491 -0
- package/src/microsoft/drive.ts +541 -0
- package/src/microsoft/graph.ts +550 -0
- package/src/microsoft/mail.ts +833 -0
- package/src/microsoft/state.ts +822 -0
- package/src/microsoft.ts +982 -0
- package/src/node.ts +293 -0
- package/src/notion/api.ts +976 -0
- package/src/notion/state.ts +512 -0
- package/src/notion.ts +210 -0
- package/src/openai.ts +198 -0
- package/src/opencode-recordings.ts +257 -0
- package/src/opencode.ts +271 -0
- package/src/r2-fixtures.ts +295 -0
- package/src/r2-guard.ts +323 -0
- package/src/r2.ts +890 -0
- package/src/responses.ts +901 -0
- package/src/route-evidence.ts +90 -0
- package/src/router.ts +490 -0
- package/src/stateful-core.ts +70 -0
- package/src/stateful-emulator.ts +2571 -0
- package/src/stateful-secrets.ts +299 -0
- package/src/subscription-usage-recordings.ts +77 -0
- package/src/subscription-usage.ts +68 -0
- package/src/telegram/api.ts +437 -0
- package/src/telegram/state.ts +229 -0
- package/src/telegram.ts +227 -0
- package/src/todoist/api.ts +736 -0
- package/src/todoist/state.ts +456 -0
- package/src/todoist.ts +316 -0
- package/src/xai.ts +341 -0
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Microsoft Graph emulator state: the typed entities, the seed input, the default seed, and the
|
|
3
|
+
* profiles (internal; re-exported by `src/microsoft.ts`).
|
|
4
|
+
*
|
|
5
|
+
* Entity shapes and the default entities follow the synthetic Microsoft conformance fixtures,
|
|
6
|
+
* copied as data (the same mailbox, calendar, event, message, folder, drive, and item ids as
|
|
7
|
+
* `microsoftConformanceFixtureSeeds`), never imported from SDK code. Where no fixture records a
|
|
8
|
+
* value, the default is synthesized and says so below: the user id, the well-known mail folders
|
|
9
|
+
* other than Drafts and Deleted Items, the attachment message itself, and the drive root and
|
|
10
|
+
* `Sources` folder have no fixture.
|
|
11
|
+
*
|
|
12
|
+
* @experimental
|
|
13
|
+
*/
|
|
14
|
+
import { Result, Struct } from 'effect'
|
|
15
|
+
import * as Schema from 'effect/Schema'
|
|
16
|
+
|
|
17
|
+
const Timestamp = Schema.String
|
|
18
|
+
|
|
19
|
+
/** A recipient as stored: `name` falls back to the address when Graph has none. */
|
|
20
|
+
export const MicrosoftEmulatorRecipient = Schema.Struct({
|
|
21
|
+
name: Schema.NullOr(Schema.String),
|
|
22
|
+
address: Schema.String
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
export type MicrosoftEmulatorRecipient = typeof MicrosoftEmulatorRecipient.Type
|
|
26
|
+
|
|
27
|
+
/** The mailbox owner every `/users/{userId}` route answers for (id, `mail`, or UPN). */
|
|
28
|
+
export const MicrosoftEmulatorUser = Schema.Struct({
|
|
29
|
+
id: Schema.String,
|
|
30
|
+
displayName: Schema.String,
|
|
31
|
+
mail: Schema.String,
|
|
32
|
+
userPrincipalName: Schema.String
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
export type MicrosoftEmulatorUser = typeof MicrosoftEmulatorUser.Type
|
|
36
|
+
|
|
37
|
+
const WellKnownFolder = Schema.Literals(['inbox', 'drafts', 'deleteditems'])
|
|
38
|
+
|
|
39
|
+
/** A mail folder; `wellKnownName` lets routes address it by name (`deleteditems`). */
|
|
40
|
+
export const MicrosoftEmulatorMailFolder = Schema.Struct({
|
|
41
|
+
id: Schema.String,
|
|
42
|
+
displayName: Schema.String,
|
|
43
|
+
wellKnownName: Schema.NullOr(WellKnownFolder)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
export type MicrosoftEmulatorMailFolder = typeof MicrosoftEmulatorMailFolder.Type
|
|
47
|
+
|
|
48
|
+
const BodyContentType = Schema.Literals(['text', 'html'])
|
|
49
|
+
|
|
50
|
+
const messageFields = {
|
|
51
|
+
/** The immutable id (`Prefer: IdType="ImmutableId"`): stable across moves. */
|
|
52
|
+
id: Schema.String,
|
|
53
|
+
parentFolderId: Schema.String,
|
|
54
|
+
subject: Schema.String,
|
|
55
|
+
bodyContentType: BodyContentType,
|
|
56
|
+
bodyContent: Schema.String,
|
|
57
|
+
from: Schema.NullOr(MicrosoftEmulatorRecipient),
|
|
58
|
+
sender: Schema.NullOr(MicrosoftEmulatorRecipient),
|
|
59
|
+
toRecipients: Schema.Array(MicrosoftEmulatorRecipient),
|
|
60
|
+
ccRecipients: Schema.Array(MicrosoftEmulatorRecipient),
|
|
61
|
+
bccRecipients: Schema.Array(MicrosoftEmulatorRecipient),
|
|
62
|
+
replyTo: Schema.Array(MicrosoftEmulatorRecipient),
|
|
63
|
+
isRead: Schema.Boolean,
|
|
64
|
+
isDraft: Schema.Boolean,
|
|
65
|
+
importance: Schema.Literals(['low', 'normal', 'high']),
|
|
66
|
+
flagStatus: Schema.Literals(['notFlagged', 'flagged', 'complete']),
|
|
67
|
+
categories: Schema.Array(Schema.String),
|
|
68
|
+
/** Answered as stored (not derived from the attachments; no fixture shows the rule). */
|
|
69
|
+
hasAttachments: Schema.Boolean,
|
|
70
|
+
createdDateTime: Timestamp,
|
|
71
|
+
lastModifiedDateTime: Timestamp,
|
|
72
|
+
receivedDateTime: Timestamp,
|
|
73
|
+
sentDateTime: Timestamp,
|
|
74
|
+
conversationId: Schema.String,
|
|
75
|
+
internetMessageId: Schema.String,
|
|
76
|
+
changeKey: Schema.String
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A stored message. Every message is addressed by its immutable id. */
|
|
80
|
+
export const MicrosoftEmulatorMessage = Schema.Struct(messageFields)
|
|
81
|
+
|
|
82
|
+
export type MicrosoftEmulatorMessage = typeof MicrosoftEmulatorMessage.Type
|
|
83
|
+
|
|
84
|
+
const attachmentFields = {
|
|
85
|
+
id: Schema.String,
|
|
86
|
+
/** Immutable id of the owning message. */
|
|
87
|
+
messageId: Schema.String,
|
|
88
|
+
name: Schema.String,
|
|
89
|
+
contentType: Schema.String,
|
|
90
|
+
size: Schema.Int,
|
|
91
|
+
isInline: Schema.Boolean,
|
|
92
|
+
contentId: Schema.NullOr(Schema.String),
|
|
93
|
+
contentLocation: Schema.NullOr(Schema.String),
|
|
94
|
+
contentBytes: Schema.String,
|
|
95
|
+
lastModifiedDateTime: Timestamp
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A stored file attachment (`#microsoft.graph.fileAttachment`). */
|
|
99
|
+
export const MicrosoftEmulatorAttachment = Schema.Struct(attachmentFields)
|
|
100
|
+
|
|
101
|
+
export type MicrosoftEmulatorAttachment = typeof MicrosoftEmulatorAttachment.Type
|
|
102
|
+
|
|
103
|
+
export const MicrosoftEmulatorCalendar = Schema.Struct({
|
|
104
|
+
id: Schema.String,
|
|
105
|
+
name: Schema.String
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
export type MicrosoftEmulatorCalendar = typeof MicrosoftEmulatorCalendar.Type
|
|
109
|
+
|
|
110
|
+
/** Graph local date-time in UTC with seven fractional digits (`2026-09-23T12:00:00.0000000`). */
|
|
111
|
+
export const microsoftEmulatorDateTimePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{7}$/
|
|
112
|
+
|
|
113
|
+
const UtcDateTime = Schema.String.check(Schema.isPattern(microsoftEmulatorDateTimePattern))
|
|
114
|
+
|
|
115
|
+
const eventFields = {
|
|
116
|
+
id: Schema.String,
|
|
117
|
+
calendarId: Schema.String,
|
|
118
|
+
subject: Schema.String,
|
|
119
|
+
bodyContentType: BodyContentType,
|
|
120
|
+
bodyContent: Schema.String,
|
|
121
|
+
/** Start in UTC, seven fractional digits. */
|
|
122
|
+
start: UtcDateTime,
|
|
123
|
+
/** End in UTC, seven fractional digits. */
|
|
124
|
+
end: UtcDateTime,
|
|
125
|
+
isCancelled: Schema.Boolean,
|
|
126
|
+
isReminderOn: Schema.Boolean,
|
|
127
|
+
showAs: Schema.Literals(['free', 'tentative', 'busy', 'oof', 'workingElsewhere', 'unknown']),
|
|
128
|
+
changeKey: Schema.String,
|
|
129
|
+
createdDateTime: Timestamp,
|
|
130
|
+
lastModifiedDateTime: Timestamp
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A stored single-instance calendar event (no attendees, no recurrence). */
|
|
134
|
+
export const MicrosoftEmulatorEvent = Schema.Struct(eventFields)
|
|
135
|
+
|
|
136
|
+
export type MicrosoftEmulatorEvent = typeof MicrosoftEmulatorEvent.Type
|
|
137
|
+
|
|
138
|
+
/** The one emulated drive. */
|
|
139
|
+
export const MicrosoftEmulatorDrive = Schema.Struct({
|
|
140
|
+
id: Schema.String,
|
|
141
|
+
driveType: Schema.Literals(['business', 'personal']),
|
|
142
|
+
rootId: Schema.String
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
export type MicrosoftEmulatorDrive = typeof MicrosoftEmulatorDrive.Type
|
|
146
|
+
|
|
147
|
+
const driveItemFields = {
|
|
148
|
+
id: Schema.String,
|
|
149
|
+
/** `null` only for the drive root. */
|
|
150
|
+
parentId: Schema.NullOr(Schema.String),
|
|
151
|
+
name: Schema.String,
|
|
152
|
+
kind: Schema.Literals(['folder', 'file']),
|
|
153
|
+
/** File size in bytes; folders report the sum of their contents instead. */
|
|
154
|
+
size: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
155
|
+
mimeType: Schema.NullOr(Schema.String),
|
|
156
|
+
quickXorHash: Schema.NullOr(Schema.String),
|
|
157
|
+
createdDateTime: Timestamp,
|
|
158
|
+
lastModifiedDateTime: Timestamp
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** A stored drive item (file or folder). Deleted items are removed (no recycle bin). */
|
|
162
|
+
export const MicrosoftEmulatorDriveItem = Schema.Struct(driveItemFields)
|
|
163
|
+
|
|
164
|
+
export type MicrosoftEmulatorDriveItem = typeof MicrosoftEmulatorDriveItem.Type
|
|
165
|
+
|
|
166
|
+
const Counter = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))
|
|
167
|
+
|
|
168
|
+
const Counters = Schema.Struct({
|
|
169
|
+
/** Next number in created event ids (`AAMkAGI2-synthetic-event-0101=`). */
|
|
170
|
+
nextEventNumber: Counter,
|
|
171
|
+
/** Next number in created message immutable ids. */
|
|
172
|
+
nextMessageNumber: Counter,
|
|
173
|
+
/** Next change key number (message and event writes). */
|
|
174
|
+
nextChangeKeyNumber: Counter,
|
|
175
|
+
/** Next number in created drive item ids. */
|
|
176
|
+
nextItemNumber: Counter
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
/** The whole emulator state (JSON-compatible; what `snapshot()` returns). */
|
|
180
|
+
export const MicrosoftEmulatorStateSchema = Schema.Struct({
|
|
181
|
+
user: MicrosoftEmulatorUser,
|
|
182
|
+
mailFolders: Schema.Array(MicrosoftEmulatorMailFolder),
|
|
183
|
+
messages: Schema.Array(MicrosoftEmulatorMessage),
|
|
184
|
+
attachments: Schema.Array(MicrosoftEmulatorAttachment),
|
|
185
|
+
calendars: Schema.Array(MicrosoftEmulatorCalendar),
|
|
186
|
+
events: Schema.Array(MicrosoftEmulatorEvent),
|
|
187
|
+
drive: MicrosoftEmulatorDrive,
|
|
188
|
+
driveItems: Schema.Array(MicrosoftEmulatorDriveItem),
|
|
189
|
+
counters: Counters
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The emulator state. The container is mutable (route handlers replace whole entity lists);
|
|
194
|
+
* every entity is replaced, never edited in place.
|
|
195
|
+
*/
|
|
196
|
+
export type MicrosoftEmulatorState = {
|
|
197
|
+
user: MicrosoftEmulatorUser
|
|
198
|
+
mailFolders: ReadonlyArray<MicrosoftEmulatorMailFolder>
|
|
199
|
+
messages: ReadonlyArray<MicrosoftEmulatorMessage>
|
|
200
|
+
attachments: ReadonlyArray<MicrosoftEmulatorAttachment>
|
|
201
|
+
calendars: ReadonlyArray<MicrosoftEmulatorCalendar>
|
|
202
|
+
events: ReadonlyArray<MicrosoftEmulatorEvent>
|
|
203
|
+
drive: MicrosoftEmulatorDrive
|
|
204
|
+
driveItems: ReadonlyArray<MicrosoftEmulatorDriveItem>
|
|
205
|
+
counters: typeof Counters.Type
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** A seeded message: `id`, `parentFolderId`, and `subject` are required. */
|
|
209
|
+
export const MicrosoftEmulatorMessageSeed = Schema.Struct(
|
|
210
|
+
Struct.mapOmit(messageFields, ['id', 'parentFolderId', 'subject'], Schema.optionalKey)
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
export type MicrosoftEmulatorMessageSeed = typeof MicrosoftEmulatorMessageSeed.Type
|
|
214
|
+
|
|
215
|
+
/** A seeded attachment: `id`, `messageId`, and `name` are required. */
|
|
216
|
+
export const MicrosoftEmulatorAttachmentSeed = Schema.Struct(
|
|
217
|
+
Struct.mapOmit(attachmentFields, ['id', 'messageId', 'name'], Schema.optionalKey)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
export type MicrosoftEmulatorAttachmentSeed = typeof MicrosoftEmulatorAttachmentSeed.Type
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* A seeded event: `id`, `calendarId`, `subject`, `start`, and `end` are required. `start` and
|
|
224
|
+
* `end` are UTC: either an ISO instant ending in `Z` or a seven-digit local date-time.
|
|
225
|
+
*/
|
|
226
|
+
export const MicrosoftEmulatorEventSeed = Schema.Struct({
|
|
227
|
+
...Struct.mapOmit(
|
|
228
|
+
eventFields,
|
|
229
|
+
['id', 'calendarId', 'subject', 'start', 'end'],
|
|
230
|
+
Schema.optionalKey
|
|
231
|
+
),
|
|
232
|
+
start: Schema.String,
|
|
233
|
+
end: Schema.String
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
export type MicrosoftEmulatorEventSeed = typeof MicrosoftEmulatorEventSeed.Type
|
|
237
|
+
|
|
238
|
+
/** A seeded drive item: `id`, `parentId`, `name`, and `kind` are required. */
|
|
239
|
+
export const MicrosoftEmulatorDriveItemSeed = Schema.Struct(
|
|
240
|
+
Struct.mapOmit(driveItemFields, ['id', 'parentId', 'name', 'kind'], Schema.optionalKey)
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
export type MicrosoftEmulatorDriveItemSeed = typeof MicrosoftEmulatorDriveItemSeed.Type
|
|
244
|
+
|
|
245
|
+
/** Account-variance profiles for the default seed. */
|
|
246
|
+
export const MicrosoftEmulatorProfile = Schema.Literals(['default', 'empty'])
|
|
247
|
+
|
|
248
|
+
export type MicrosoftEmulatorProfile = typeof MicrosoftEmulatorProfile.Type
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* A typed seed. Start from `profile` (default `'default'`, the fixture entities); every other
|
|
252
|
+
* key, when given, replaces that part of the profile.
|
|
253
|
+
*/
|
|
254
|
+
export const MicrosoftEmulatorSeed = Schema.Struct({
|
|
255
|
+
profile: Schema.optionalKey(MicrosoftEmulatorProfile),
|
|
256
|
+
user: Schema.optionalKey(MicrosoftEmulatorUser),
|
|
257
|
+
mailFolders: Schema.optionalKey(Schema.Array(MicrosoftEmulatorMailFolder)),
|
|
258
|
+
messages: Schema.optionalKey(Schema.Array(MicrosoftEmulatorMessageSeed)),
|
|
259
|
+
attachments: Schema.optionalKey(Schema.Array(MicrosoftEmulatorAttachmentSeed)),
|
|
260
|
+
calendars: Schema.optionalKey(Schema.Array(MicrosoftEmulatorCalendar)),
|
|
261
|
+
events: Schema.optionalKey(Schema.Array(MicrosoftEmulatorEventSeed)),
|
|
262
|
+
drive: Schema.optionalKey(MicrosoftEmulatorDrive),
|
|
263
|
+
driveItems: Schema.optionalKey(Schema.Array(MicrosoftEmulatorDriveItemSeed))
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
export type MicrosoftEmulatorSeed = typeof MicrosoftEmulatorSeed.Type
|
|
267
|
+
|
|
268
|
+
const strict = { onExcessProperty: 'error' } as const
|
|
269
|
+
|
|
270
|
+
const decodeSeedInput = Schema.decodeUnknownResult(MicrosoftEmulatorSeed, strict)
|
|
271
|
+
|
|
272
|
+
const decodeStateInput = Schema.decodeUnknownResult(MicrosoftEmulatorStateSchema, strict)
|
|
273
|
+
|
|
274
|
+
const issueMessage = (issue: Schema.SchemaError['issue']): string =>
|
|
275
|
+
new Schema.SchemaError(issue).message
|
|
276
|
+
|
|
277
|
+
// Instants: 100-nanosecond ticks since the epoch (BigInt, so seven fractional digits stay exact).
|
|
278
|
+
|
|
279
|
+
const ticksPerMilli = BigInt(10_000)
|
|
280
|
+
|
|
281
|
+
const ticksPerSecond = BigInt(10_000_000)
|
|
282
|
+
|
|
283
|
+
const instantPattern =
|
|
284
|
+
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,7}))?(Z|[+-]\d{2}:\d{2})?$/
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Ticks of an ISO-8601 date-time. `zone` `'required'` needs `Z` or an offset; `'forbidden'`
|
|
288
|
+
* needs none (a Graph local date-time, read as UTC); `'optional'` reads a missing one as UTC.
|
|
289
|
+
*/
|
|
290
|
+
export const parseInstant = (
|
|
291
|
+
value: string,
|
|
292
|
+
zone: 'required' | 'forbidden' | 'optional'
|
|
293
|
+
): bigint | undefined => {
|
|
294
|
+
const match = instantPattern.exec(value)
|
|
295
|
+
|
|
296
|
+
if (match === null) return undefined
|
|
297
|
+
|
|
298
|
+
const [, year, month, day, hour, minute, second, fraction = '', offset] = match
|
|
299
|
+
|
|
300
|
+
if (
|
|
301
|
+
(zone === 'required' && offset === undefined) ||
|
|
302
|
+
(zone === 'forbidden' && offset !== undefined)
|
|
303
|
+
) {
|
|
304
|
+
return undefined
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const wholeMillis = Date.UTC(
|
|
308
|
+
Number(year),
|
|
309
|
+
Number(month) - 1,
|
|
310
|
+
Number(day),
|
|
311
|
+
Number(hour),
|
|
312
|
+
Number(minute),
|
|
313
|
+
Number(second)
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
const check = new Date(wholeMillis)
|
|
317
|
+
|
|
318
|
+
// Reject rollovers such as 2026-02-30 or 25:00.
|
|
319
|
+
if (
|
|
320
|
+
!Number.isFinite(wholeMillis) ||
|
|
321
|
+
check.getUTCFullYear() !== Number(year) ||
|
|
322
|
+
check.getUTCMonth() !== Number(month) - 1 ||
|
|
323
|
+
check.getUTCDate() !== Number(day) ||
|
|
324
|
+
check.getUTCHours() !== Number(hour) ||
|
|
325
|
+
check.getUTCMinutes() !== Number(minute)
|
|
326
|
+
) {
|
|
327
|
+
return undefined
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const offsetMinutes =
|
|
331
|
+
offset === undefined || offset === 'Z'
|
|
332
|
+
? 0
|
|
333
|
+
: (offset.startsWith('-') ? -1 : 1) *
|
|
334
|
+
(Number(offset.slice(1, 3)) * 60 + Number(offset.slice(4, 6)))
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
BigInt(wholeMillis - offsetMinutes * 60_000) * ticksPerMilli + BigInt(fraction.padEnd(7, '0'))
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const pad = (value: number, width: number): string => String(value).padStart(width, '0')
|
|
342
|
+
|
|
343
|
+
/** A UTC local date-time with `digits` fractional digits (Graph uses seven). */
|
|
344
|
+
export const formatLocalDateTime = (ticks: bigint, digits: number): string => {
|
|
345
|
+
const millis = Number(ticks / ticksPerMilli)
|
|
346
|
+
const date = new Date(millis)
|
|
347
|
+
const fraction = pad(Number(((ticks % ticksPerSecond) + ticksPerSecond) % ticksPerSecond), 7)
|
|
348
|
+
|
|
349
|
+
const whole = `${pad(date.getUTCFullYear(), 4)}-${pad(date.getUTCMonth() + 1, 2)}-${pad(
|
|
350
|
+
date.getUTCDate(),
|
|
351
|
+
2
|
|
352
|
+
)}T${pad(date.getUTCHours(), 2)}:${pad(date.getUTCMinutes(), 2)}:${pad(date.getUTCSeconds(), 2)}`
|
|
353
|
+
|
|
354
|
+
return digits === 0 ? whole : `${whole}.${fraction.slice(0, digits)}`
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Default entities. Fixture-derived: the mailbox, the calendar and its two events, the attachment
|
|
358
|
+
// message's two attachments, the paging folder's three messages, the Drafts and Deleted Items
|
|
359
|
+
// folder ids, the drive, the `Conformance` parent folder with its existing file, and the copy
|
|
360
|
+
// source file. Synthesized: the user id, the Inbox folder, the attachment message's own fields,
|
|
361
|
+
// the drive root, the `Sources` folder, and every default the `*FromSeed` builders fill in.
|
|
362
|
+
|
|
363
|
+
const ada: MicrosoftEmulatorUser = {
|
|
364
|
+
id: '00000000-0000-4000-8000-00000000ada1',
|
|
365
|
+
displayName: 'Ada Example',
|
|
366
|
+
mail: 'ada@example.test',
|
|
367
|
+
userPrincipalName: 'ada@example.test'
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const grace: MicrosoftEmulatorRecipient = { name: 'Grace Example', address: 'grace@example.test' }
|
|
371
|
+
|
|
372
|
+
const adaRecipient: MicrosoftEmulatorRecipient = {
|
|
373
|
+
name: 'Ada Example',
|
|
374
|
+
address: 'ada@example.test'
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const inboxId = 'AAMkAGI2-synthetic-inbox-folder='
|
|
378
|
+
|
|
379
|
+
const pagingFolderId = 'AAMkAGI2-synthetic-folder-0001='
|
|
380
|
+
|
|
381
|
+
const defaultMailFolders: ReadonlyArray<MicrosoftEmulatorMailFolder> = [
|
|
382
|
+
{ id: inboxId, displayName: 'Inbox', wellKnownName: 'inbox' },
|
|
383
|
+
{ id: 'AAMkAGI2-synthetic-drafts-folder=', displayName: 'Drafts', wellKnownName: 'drafts' },
|
|
384
|
+
{
|
|
385
|
+
id: 'AAMkAGI2-synthetic-deleteditems-folder=',
|
|
386
|
+
displayName: 'Deleted Items',
|
|
387
|
+
wellKnownName: 'deleteditems'
|
|
388
|
+
},
|
|
389
|
+
{ id: pagingFolderId, displayName: 'Synthetic updates', wellKnownName: null }
|
|
390
|
+
]
|
|
391
|
+
|
|
392
|
+
const pagingMessage = (number: number, hour: number): MicrosoftEmulatorMessageSeed => {
|
|
393
|
+
const id = `AAMkAGI2-synthetic-message-${pad(number, 4)}=`
|
|
394
|
+
const at = `2026-09-22T${pad(hour, 2)}:00:00Z`
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
id,
|
|
398
|
+
parentFolderId: pagingFolderId,
|
|
399
|
+
subject: `Synthetic update ${number - 100}`,
|
|
400
|
+
bodyContent: 'Synthetic message body.',
|
|
401
|
+
from: grace,
|
|
402
|
+
sender: grace,
|
|
403
|
+
toRecipients: [adaRecipient],
|
|
404
|
+
isRead: true,
|
|
405
|
+
isDraft: false,
|
|
406
|
+
receivedDateTime: at,
|
|
407
|
+
sentDateTime: at,
|
|
408
|
+
createdDateTime: at,
|
|
409
|
+
lastModifiedDateTime: at,
|
|
410
|
+
conversationId: `AAQkAGI2-synthetic-conversation-${pad(number, 4)}=`,
|
|
411
|
+
internetMessageId: `<synthetic-${pad(number, 4)}@example.test>`,
|
|
412
|
+
changeKey: `CQAAABYAAAAsynthetic${pad(number, 4)}`
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const attachmentMessageId = 'AAMkAGI2-synthetic-message-0001='
|
|
417
|
+
|
|
418
|
+
const defaultMessages: ReadonlyArray<MicrosoftEmulatorMessageSeed> = [
|
|
419
|
+
{
|
|
420
|
+
id: attachmentMessageId,
|
|
421
|
+
parentFolderId: inboxId,
|
|
422
|
+
subject: 'Synthetic message with attachments',
|
|
423
|
+
bodyContentType: 'html',
|
|
424
|
+
bodyContent: '<p>Synthetic body <img src="cid:image001.png@01DD2E00.00000000"></p>',
|
|
425
|
+
from: grace,
|
|
426
|
+
sender: grace,
|
|
427
|
+
toRecipients: [adaRecipient],
|
|
428
|
+
isRead: false,
|
|
429
|
+
isDraft: false,
|
|
430
|
+
// Synthesized (no fixture lists this message): it has a regular (non-inline) attachment.
|
|
431
|
+
hasAttachments: true,
|
|
432
|
+
receivedDateTime: '2026-09-22T08:15:00Z',
|
|
433
|
+
sentDateTime: '2026-09-22T08:15:00Z',
|
|
434
|
+
createdDateTime: '2026-09-22T08:15:00Z',
|
|
435
|
+
lastModifiedDateTime: '2026-09-22T08:15:00Z',
|
|
436
|
+
conversationId: 'AAQkAGI2-synthetic-conversation-0001=',
|
|
437
|
+
internetMessageId: '<synthetic-0001@example.test>',
|
|
438
|
+
changeKey: 'CQAAABYAAAAsynthetic0001'
|
|
439
|
+
},
|
|
440
|
+
pagingMessage(101, 9),
|
|
441
|
+
pagingMessage(102, 8),
|
|
442
|
+
pagingMessage(103, 7)
|
|
443
|
+
]
|
|
444
|
+
|
|
445
|
+
const defaultAttachments: ReadonlyArray<MicrosoftEmulatorAttachmentSeed> = [
|
|
446
|
+
{
|
|
447
|
+
id: 'AAMkAGI2-synthetic-attachment-0001=',
|
|
448
|
+
messageId: attachmentMessageId,
|
|
449
|
+
name: 'image001.png',
|
|
450
|
+
contentType: 'image/png',
|
|
451
|
+
size: 1024,
|
|
452
|
+
isInline: true,
|
|
453
|
+
contentId: 'image001.png@01DD2E00.00000000',
|
|
454
|
+
contentBytes: 'iVBORw0KGgo=',
|
|
455
|
+
lastModifiedDateTime: '2026-09-22T08:15:00Z'
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
id: 'AAMkAGI2-synthetic-attachment-0002=',
|
|
459
|
+
messageId: attachmentMessageId,
|
|
460
|
+
name: 'synthetic-report.pdf',
|
|
461
|
+
contentType: 'application/pdf',
|
|
462
|
+
size: 2048,
|
|
463
|
+
isInline: false,
|
|
464
|
+
contentId: null,
|
|
465
|
+
contentBytes: 'JVBERi0xLjQK',
|
|
466
|
+
lastModifiedDateTime: '2026-09-22T08:15:00Z'
|
|
467
|
+
}
|
|
468
|
+
]
|
|
469
|
+
|
|
470
|
+
const calendarId = 'AAMkAGI2-synthetic-calendar-0001='
|
|
471
|
+
|
|
472
|
+
const defaultCalendars: ReadonlyArray<MicrosoftEmulatorCalendar> = [
|
|
473
|
+
{ id: calendarId, name: 'Calendar' }
|
|
474
|
+
]
|
|
475
|
+
|
|
476
|
+
const defaultEvents: ReadonlyArray<MicrosoftEmulatorEventSeed> = [
|
|
477
|
+
{
|
|
478
|
+
id: 'AAMkAGI2-synthetic-event-0001=',
|
|
479
|
+
calendarId,
|
|
480
|
+
subject: 'Synthetic planning session',
|
|
481
|
+
start: '2026-09-23T12:00:00Z',
|
|
482
|
+
end: '2026-09-23T13:00:00Z',
|
|
483
|
+
changeKey: 'DwAAABYAAAAsynthetic0001'
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
id: 'AAMkAGI2-synthetic-event-0002=',
|
|
487
|
+
calendarId,
|
|
488
|
+
subject: 'Synthetic review',
|
|
489
|
+
start: '2026-09-24T08:30:00Z',
|
|
490
|
+
end: '2026-09-24T09:00:00Z',
|
|
491
|
+
changeKey: 'DwAAABYAAAAsynthetic0002'
|
|
492
|
+
}
|
|
493
|
+
]
|
|
494
|
+
|
|
495
|
+
const defaultDrive: MicrosoftEmulatorDrive = {
|
|
496
|
+
id: 'b!synthetic-drive-0001',
|
|
497
|
+
driveType: 'business',
|
|
498
|
+
rootId: '01SYNTHETICROOTFOLDER00000000001'
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const quickXorHash = 'AAAAAAAAAAAAAAAAAAAAAAAAAAA='
|
|
502
|
+
|
|
503
|
+
const conformanceFolderId = '01SYNTHETICPARENTFOLDER0000000001'
|
|
504
|
+
|
|
505
|
+
const sourcesFolderId = '01SYNTHETICSOURCEPARENT000000001'
|
|
506
|
+
|
|
507
|
+
const rootItem: MicrosoftEmulatorDriveItemSeed = {
|
|
508
|
+
id: defaultDrive.rootId,
|
|
509
|
+
parentId: null,
|
|
510
|
+
name: 'root',
|
|
511
|
+
kind: 'folder'
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const defaultDriveItems: ReadonlyArray<MicrosoftEmulatorDriveItemSeed> = [
|
|
515
|
+
rootItem,
|
|
516
|
+
{ id: conformanceFolderId, parentId: defaultDrive.rootId, name: 'Conformance', kind: 'folder' },
|
|
517
|
+
{
|
|
518
|
+
id: '01SYNTHETICEXISTINGFILE000000001',
|
|
519
|
+
parentId: conformanceFolderId,
|
|
520
|
+
name: 'synthetic-existing.txt',
|
|
521
|
+
kind: 'file',
|
|
522
|
+
size: 12,
|
|
523
|
+
mimeType: 'text/plain',
|
|
524
|
+
quickXorHash
|
|
525
|
+
},
|
|
526
|
+
{ id: sourcesFolderId, parentId: defaultDrive.rootId, name: 'Sources', kind: 'folder' },
|
|
527
|
+
{
|
|
528
|
+
id: '01SYNTHETICSOURCEFILE00000000001',
|
|
529
|
+
parentId: sourcesFolderId,
|
|
530
|
+
name: 'synthetic-notes.txt',
|
|
531
|
+
kind: 'file',
|
|
532
|
+
size: 24,
|
|
533
|
+
mimeType: 'text/plain',
|
|
534
|
+
quickXorHash
|
|
535
|
+
}
|
|
536
|
+
]
|
|
537
|
+
|
|
538
|
+
type ProfileEntities = {
|
|
539
|
+
readonly user: MicrosoftEmulatorUser
|
|
540
|
+
readonly mailFolders: ReadonlyArray<MicrosoftEmulatorMailFolder>
|
|
541
|
+
readonly messages: ReadonlyArray<MicrosoftEmulatorMessageSeed>
|
|
542
|
+
readonly attachments: ReadonlyArray<MicrosoftEmulatorAttachmentSeed>
|
|
543
|
+
readonly calendars: ReadonlyArray<MicrosoftEmulatorCalendar>
|
|
544
|
+
readonly events: ReadonlyArray<MicrosoftEmulatorEventSeed>
|
|
545
|
+
readonly drive: MicrosoftEmulatorDrive
|
|
546
|
+
readonly driveItems: ReadonlyArray<MicrosoftEmulatorDriveItemSeed>
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const profileEntities = (profile: MicrosoftEmulatorProfile): ProfileEntities => {
|
|
550
|
+
switch (profile) {
|
|
551
|
+
case 'default':
|
|
552
|
+
return {
|
|
553
|
+
user: ada,
|
|
554
|
+
mailFolders: defaultMailFolders,
|
|
555
|
+
messages: defaultMessages,
|
|
556
|
+
attachments: defaultAttachments,
|
|
557
|
+
calendars: defaultCalendars,
|
|
558
|
+
events: defaultEvents,
|
|
559
|
+
drive: defaultDrive,
|
|
560
|
+
driveItems: defaultDriveItems
|
|
561
|
+
}
|
|
562
|
+
case 'empty':
|
|
563
|
+
return {
|
|
564
|
+
user: ada,
|
|
565
|
+
mailFolders: defaultMailFolders.filter(folder => folder.wellKnownName !== null),
|
|
566
|
+
messages: [],
|
|
567
|
+
attachments: [],
|
|
568
|
+
calendars: defaultCalendars,
|
|
569
|
+
events: [],
|
|
570
|
+
drive: defaultDrive,
|
|
571
|
+
driveItems: [rootItem]
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const defaultTimestamp = '2026-09-29T10:00:00Z'
|
|
577
|
+
|
|
578
|
+
const messageFromSeed = (
|
|
579
|
+
seed: MicrosoftEmulatorMessageSeed,
|
|
580
|
+
user: MicrosoftEmulatorUser
|
|
581
|
+
): MicrosoftEmulatorMessage => {
|
|
582
|
+
const owner: MicrosoftEmulatorRecipient = { name: user.displayName, address: user.mail }
|
|
583
|
+
const created = seed.createdDateTime ?? defaultTimestamp
|
|
584
|
+
|
|
585
|
+
return {
|
|
586
|
+
id: seed.id,
|
|
587
|
+
parentFolderId: seed.parentFolderId,
|
|
588
|
+
subject: seed.subject,
|
|
589
|
+
bodyContentType: seed.bodyContentType ?? 'text',
|
|
590
|
+
bodyContent: seed.bodyContent ?? '',
|
|
591
|
+
from: seed.from === undefined ? owner : seed.from,
|
|
592
|
+
sender: seed.sender === undefined ? owner : seed.sender,
|
|
593
|
+
toRecipients: seed.toRecipients ?? [],
|
|
594
|
+
ccRecipients: seed.ccRecipients ?? [],
|
|
595
|
+
bccRecipients: seed.bccRecipients ?? [],
|
|
596
|
+
replyTo: seed.replyTo ?? [],
|
|
597
|
+
isRead: seed.isRead ?? true,
|
|
598
|
+
isDraft: seed.isDraft ?? false,
|
|
599
|
+
importance: seed.importance ?? 'normal',
|
|
600
|
+
flagStatus: seed.flagStatus ?? 'notFlagged',
|
|
601
|
+
categories: seed.categories ?? [],
|
|
602
|
+
hasAttachments: seed.hasAttachments ?? false,
|
|
603
|
+
createdDateTime: created,
|
|
604
|
+
lastModifiedDateTime: seed.lastModifiedDateTime ?? created,
|
|
605
|
+
receivedDateTime: seed.receivedDateTime ?? created,
|
|
606
|
+
sentDateTime: seed.sentDateTime ?? created,
|
|
607
|
+
conversationId: seed.conversationId ?? `AAQkAGI2-synthetic-conversation-${seed.id}`,
|
|
608
|
+
internetMessageId: seed.internetMessageId ?? `<${seed.id}@example.test>`,
|
|
609
|
+
changeKey: seed.changeKey ?? 'CQAAABYAAAAsynthetic0000'
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Decoded size of a base64 string (no validation beyond the padding count). */
|
|
614
|
+
const base64Size = (value: string): number =>
|
|
615
|
+
Math.max(0, Math.floor((value.length * 3) / 4) - (value.match(/=+$/)?.[0].length ?? 0))
|
|
616
|
+
|
|
617
|
+
const attachmentFromSeed = (seed: MicrosoftEmulatorAttachmentSeed): MicrosoftEmulatorAttachment => {
|
|
618
|
+
const contentBytes = seed.contentBytes ?? ''
|
|
619
|
+
|
|
620
|
+
return {
|
|
621
|
+
id: seed.id,
|
|
622
|
+
messageId: seed.messageId,
|
|
623
|
+
name: seed.name,
|
|
624
|
+
contentType: seed.contentType ?? 'application/octet-stream',
|
|
625
|
+
size: seed.size ?? base64Size(contentBytes),
|
|
626
|
+
isInline: seed.isInline ?? false,
|
|
627
|
+
contentId: seed.contentId ?? null,
|
|
628
|
+
contentLocation: seed.contentLocation ?? null,
|
|
629
|
+
contentBytes,
|
|
630
|
+
lastModifiedDateTime: seed.lastModifiedDateTime ?? defaultTimestamp
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** A seeded start/end as a seven-digit UTC local date-time, or `undefined` when unreadable. */
|
|
635
|
+
const seedDateTime = (value: string): string | undefined => {
|
|
636
|
+
const ticks = value.endsWith('Z')
|
|
637
|
+
? parseInstant(value, 'required')
|
|
638
|
+
: parseInstant(value, 'forbidden')
|
|
639
|
+
|
|
640
|
+
return ticks === undefined ? undefined : formatLocalDateTime(ticks, 7)
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const eventFromSeed = (
|
|
644
|
+
seed: MicrosoftEmulatorEventSeed,
|
|
645
|
+
start: string,
|
|
646
|
+
end: string
|
|
647
|
+
): MicrosoftEmulatorEvent => ({
|
|
648
|
+
id: seed.id,
|
|
649
|
+
calendarId: seed.calendarId,
|
|
650
|
+
subject: seed.subject,
|
|
651
|
+
bodyContentType: seed.bodyContentType ?? 'text',
|
|
652
|
+
bodyContent: seed.bodyContent ?? '',
|
|
653
|
+
start,
|
|
654
|
+
end,
|
|
655
|
+
isCancelled: seed.isCancelled ?? false,
|
|
656
|
+
isReminderOn: seed.isReminderOn ?? false,
|
|
657
|
+
showAs: seed.showAs ?? 'busy',
|
|
658
|
+
changeKey: seed.changeKey ?? 'DwAAABYAAAAsynthetic0000',
|
|
659
|
+
createdDateTime: seed.createdDateTime ?? defaultTimestamp,
|
|
660
|
+
lastModifiedDateTime: seed.lastModifiedDateTime ?? seed.createdDateTime ?? defaultTimestamp
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
const driveItemFromSeed = (seed: MicrosoftEmulatorDriveItemSeed): MicrosoftEmulatorDriveItem => ({
|
|
664
|
+
id: seed.id,
|
|
665
|
+
parentId: seed.parentId,
|
|
666
|
+
name: seed.name,
|
|
667
|
+
kind: seed.kind,
|
|
668
|
+
size: seed.kind === 'folder' ? 0 : (seed.size ?? 0),
|
|
669
|
+
mimeType: seed.kind === 'folder' ? null : (seed.mimeType ?? 'application/octet-stream'),
|
|
670
|
+
quickXorHash: seed.kind === 'folder' ? null : (seed.quickXorHash ?? null),
|
|
671
|
+
createdDateTime: seed.createdDateTime ?? defaultTimestamp,
|
|
672
|
+
lastModifiedDateTime: seed.lastModifiedDateTime ?? seed.createdDateTime ?? defaultTimestamp
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
const duplicate = (values: ReadonlyArray<string>): string | undefined =>
|
|
676
|
+
values.find((value, index) => values.indexOf(value) !== index)
|
|
677
|
+
|
|
678
|
+
/** Integrity problems a decoded seed can still have (duplicates, dangling references, cycles). */
|
|
679
|
+
const seedProblem = (entities: ProfileEntities): string | undefined => {
|
|
680
|
+
const folderIds = entities.mailFolders.map(folder => folder.id)
|
|
681
|
+
const messageIds = entities.messages.map(message => message.id)
|
|
682
|
+
const calendarIds = entities.calendars.map(calendar => calendar.id)
|
|
683
|
+
const itemIds = entities.driveItems.map(item => item.id)
|
|
684
|
+
|
|
685
|
+
const duplicates: ReadonlyArray<readonly [string, string | undefined]> = [
|
|
686
|
+
['mail folder id', duplicate(folderIds)],
|
|
687
|
+
[
|
|
688
|
+
'well-known folder',
|
|
689
|
+
duplicate(entities.mailFolders.flatMap(folder => folder.wellKnownName ?? []))
|
|
690
|
+
],
|
|
691
|
+
['message id', duplicate(messageIds)],
|
|
692
|
+
['attachment id', duplicate(entities.attachments.map(attachment => attachment.id))],
|
|
693
|
+
['calendar id', duplicate(calendarIds)],
|
|
694
|
+
['event id', duplicate(entities.events.map(event => event.id))],
|
|
695
|
+
['drive item id', duplicate(itemIds)]
|
|
696
|
+
]
|
|
697
|
+
|
|
698
|
+
for (const [label, value] of duplicates) {
|
|
699
|
+
if (value !== undefined) return `duplicate ${label} ${value}`
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
for (const wellKnown of ['drafts', 'deleteditems'] as const) {
|
|
703
|
+
if (!entities.mailFolders.some(folder => folder.wellKnownName === wellKnown)) {
|
|
704
|
+
return `the mail folders need a ${wellKnown} folder`
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const message = entities.messages.find(candidate => !folderIds.includes(candidate.parentFolderId))
|
|
709
|
+
|
|
710
|
+
if (message !== undefined) {
|
|
711
|
+
return `message ${message.id} references missing folder ${message.parentFolderId}`
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const attachment = entities.attachments.find(
|
|
715
|
+
candidate => !messageIds.includes(candidate.messageId)
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
if (attachment !== undefined) {
|
|
719
|
+
return `attachment ${attachment.id} references missing message ${attachment.messageId}`
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const event = entities.events.find(candidate => !calendarIds.includes(candidate.calendarId))
|
|
723
|
+
|
|
724
|
+
if (event !== undefined) {
|
|
725
|
+
return `event ${event.id} references missing calendar ${event.calendarId}`
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const roots = entities.driveItems.filter(item => item.parentId === null)
|
|
729
|
+
|
|
730
|
+
if (roots.length !== 1 || roots[0]?.id !== entities.drive.rootId || roots[0].kind !== 'folder') {
|
|
731
|
+
return `the drive items need exactly one root folder with id ${entities.drive.rootId}`
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const byId = new Map(entities.driveItems.map(item => [item.id, item]))
|
|
735
|
+
|
|
736
|
+
for (const item of entities.driveItems) {
|
|
737
|
+
let parentId = item.parentId
|
|
738
|
+
let steps = 0
|
|
739
|
+
|
|
740
|
+
while (parentId !== null) {
|
|
741
|
+
const parent = byId.get(parentId)
|
|
742
|
+
|
|
743
|
+
if (parent === undefined || parent.kind !== 'folder') {
|
|
744
|
+
return `drive item ${item.id} has a missing or non-folder parent ${parentId}`
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
steps += 1
|
|
748
|
+
|
|
749
|
+
if (steps > entities.driveItems.length) return `drive item ${item.id} is in a parent cycle`
|
|
750
|
+
|
|
751
|
+
parentId = parent.parentId
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return undefined
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/** Build the emulator state for a decoded seed; a string is an integrity problem. */
|
|
759
|
+
const stateFromSeed = (seed: MicrosoftEmulatorSeed): MicrosoftEmulatorState | string => {
|
|
760
|
+
const profile = profileEntities(seed.profile ?? 'default')
|
|
761
|
+
|
|
762
|
+
const entities: ProfileEntities = {
|
|
763
|
+
user: seed.user ?? profile.user,
|
|
764
|
+
mailFolders: seed.mailFolders ?? profile.mailFolders,
|
|
765
|
+
messages: seed.messages ?? profile.messages,
|
|
766
|
+
attachments: seed.attachments ?? profile.attachments,
|
|
767
|
+
calendars: seed.calendars ?? profile.calendars,
|
|
768
|
+
events: seed.events ?? profile.events,
|
|
769
|
+
drive: seed.drive ?? profile.drive,
|
|
770
|
+
driveItems: seed.driveItems ?? profile.driveItems
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const problem = seedProblem(entities)
|
|
774
|
+
|
|
775
|
+
if (problem !== undefined) return problem
|
|
776
|
+
|
|
777
|
+
const events: Array<MicrosoftEmulatorEvent> = []
|
|
778
|
+
|
|
779
|
+
for (const event of entities.events) {
|
|
780
|
+
const start = seedDateTime(event.start)
|
|
781
|
+
const end = seedDateTime(event.end)
|
|
782
|
+
|
|
783
|
+
if (start === undefined || end === undefined || end < start) {
|
|
784
|
+
return `event ${event.id} needs readable UTC start and end, with end not before start`
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
events.push(eventFromSeed(event, start, end))
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return {
|
|
791
|
+
user: entities.user,
|
|
792
|
+
mailFolders: entities.mailFolders,
|
|
793
|
+
messages: entities.messages.map(message => messageFromSeed(message, entities.user)),
|
|
794
|
+
attachments: entities.attachments.map(attachmentFromSeed),
|
|
795
|
+
calendars: entities.calendars,
|
|
796
|
+
events,
|
|
797
|
+
drive: entities.drive,
|
|
798
|
+
driveItems: entities.driveItems.map(driveItemFromSeed),
|
|
799
|
+
counters: {
|
|
800
|
+
nextEventNumber: 101,
|
|
801
|
+
nextMessageNumber: 1,
|
|
802
|
+
nextChangeKeyNumber: 1001,
|
|
803
|
+
nextItemNumber: 1
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/** Decode and build a seed; a string is the reason it is invalid. */
|
|
809
|
+
export const buildSeedState = (input: unknown): MicrosoftEmulatorState | string => {
|
|
810
|
+
const decoded = decodeSeedInput(input)
|
|
811
|
+
|
|
812
|
+
return Result.isFailure(decoded)
|
|
813
|
+
? issueMessage(decoded.failure.issue)
|
|
814
|
+
: stateFromSeed(decoded.success)
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/** Decode a full state (a restored snapshot); a string is the reason it is invalid. */
|
|
818
|
+
export const decodeState = (input: unknown): MicrosoftEmulatorState | string => {
|
|
819
|
+
const decoded = decodeStateInput(input)
|
|
820
|
+
|
|
821
|
+
return Result.isFailure(decoded) ? issueMessage(decoded.failure.issue) : { ...decoded.success }
|
|
822
|
+
}
|