@things-factory/ai-assistant 10.1.40 → 10.1.42
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-server/service/assistant-capability-resolver.js +1 -1
- package/dist-server/service/assistant-capability-resolver.js.map +1 -1
- package/dist-server/service/assistant-chat-resolver.js +1 -1
- package/dist-server/service/assistant-chat-resolver.js.map +1 -1
- package/dist-server/service/assistant-mode/assistant-mode-resolver.js +2 -2
- package/dist-server/service/assistant-mode/assistant-mode-resolver.js.map +1 -1
- package/dist-server/service/chat-session-resolver.js +21 -21
- package/dist-server/service/chat-session-resolver.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/server/service/assistant-capability-resolver.ts +1 -1
- package/server/service/assistant-chat-resolver.ts +1 -1
- package/server/service/assistant-mode/assistant-mode-resolver.ts +2 -2
- package/server/service/chat-session/session-visibility.test.ts +47 -0
- package/server/service/chat-session-resolver.ts +21 -21
- package/test/open-doors.test.ts +16 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/ai-assistant",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.42",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"things-factory": true,
|
|
6
6
|
"author": "heartyoh",
|
|
@@ -44,5 +44,5 @@
|
|
|
44
44
|
"copyfiles": "^2.4.1",
|
|
45
45
|
"rimraf": "^5.0.0"
|
|
46
46
|
},
|
|
47
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "982aa01403b979c4e348fd207a3fb4d1a7b5f521"
|
|
48
48
|
}
|
|
@@ -52,7 +52,7 @@ export class AssistantCapabilityResolver {
|
|
|
52
52
|
@Query(() => AiCapabilityView, {
|
|
53
53
|
description: 'Whether this deployment has an AI configured, and which registrations are incomplete. Read this before offering to ask the assistant — do not find out by failing.'
|
|
54
54
|
})
|
|
55
|
-
@Directive('@privilege(category: "ai-assistant", privilege: "query")')
|
|
55
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "query", domainOwnerGranted: true)')
|
|
56
56
|
async aiCapability(@Ctx() _context: any): Promise<AiCapabilityView> {
|
|
57
57
|
const capability = aiCapability()
|
|
58
58
|
const warnings = [...getToolRegistryWarnings(), ...getAssistantModeWarnings()].map(warning => ({
|
|
@@ -200,7 +200,7 @@ export class AssistantChatResolver {
|
|
|
200
200
|
* 2026-09-10. No `legacy` alias: this operation is new, so no one was ever passing under an old
|
|
201
201
|
* name, and declaring an alias would open a second name that nothing needs.
|
|
202
202
|
*/
|
|
203
|
-
@Directive('@privilege(category: "ai-assistant", privilege: "mutation")')
|
|
203
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "mutation", domainOwnerGranted: true)')
|
|
204
204
|
async assistantChat(@Arg('input') input: AssistantChatInput, @Ctx() context: any): Promise<AssistantChatOutput> {
|
|
205
205
|
const client = resolveAIClient()
|
|
206
206
|
if (!client) {
|
|
@@ -83,7 +83,7 @@ export class AssistantModeResolver {
|
|
|
83
83
|
description:
|
|
84
84
|
'Conversations that stand for this kind of subject, the default first. A screen showing one subject opens the head of this list and offers the rest; a single entry means there is nothing to choose.'
|
|
85
85
|
})
|
|
86
|
-
@Directive('@privilege(category: "ai-assistant", privilege: "query")')
|
|
86
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "query", domainOwnerGranted: true)')
|
|
87
87
|
async assistantModes(@Arg('subject') subject: string, @Ctx() context: any): Promise<AssistantModeChoice[]> {
|
|
88
88
|
const t = requestTranslator(context)
|
|
89
89
|
|
|
@@ -101,7 +101,7 @@ export class AssistantModeResolver {
|
|
|
101
101
|
description: 'What this conversation offers on screen — `/` sentences, the `@` catalog and the notices, translated.'
|
|
102
102
|
})
|
|
103
103
|
/* Reading what a conversation offers is reading, and the same door the session list uses. */
|
|
104
|
-
@Directive('@privilege(category: "ai-assistant", privilege: "query")')
|
|
104
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "query", domainOwnerGranted: true)')
|
|
105
105
|
async assistantMode(@Arg('kind') kind: string, @Ctx() context: any): Promise<AssistantModeView> {
|
|
106
106
|
return assistantModeView(kind, requestTranslator(context)) as AssistantModeView
|
|
107
107
|
}
|
|
@@ -50,3 +50,50 @@ describe("'none' is not an anchor", () => {
|
|
|
50
50
|
expect(initialVisibility({ anchorType: 'space' })).toBe('open')
|
|
51
51
|
})
|
|
52
52
|
})
|
|
53
|
+
|
|
54
|
+
/*
|
|
55
|
+
* Calling a door is not reading a conversation (ruled 2026-09-24).
|
|
56
|
+
*
|
|
57
|
+
* Every ai-assistant door now admits the domain owner (`test/open-doors.test.ts`), so the owner may
|
|
58
|
+
* open the dock anywhere in the domain. That says who may *call* — not whose private conversation they
|
|
59
|
+
* may read. The room rule is asked with two things only, the visibility and whether the caller sits in
|
|
60
|
+
* the room, and neither place that reads a room asks anything about ownership. If one ever did, a domain
|
|
61
|
+
* owner would read every private conversation in the domain, and nothing on the screen would say so.
|
|
62
|
+
*/
|
|
63
|
+
describe('the domain owner still reads a private conversation only as a participant', () => {
|
|
64
|
+
const { readFileSync } = require('fs')
|
|
65
|
+
const { join } = require('path')
|
|
66
|
+
const source = (path: string) => readFileSync(join(__dirname, '..', path), 'utf8') as string
|
|
67
|
+
|
|
68
|
+
/** The body of `chatMessages` — the stored path. The stream's path is the room lookup and `mayHearRoom`. */
|
|
69
|
+
const storedPath = () => {
|
|
70
|
+
const resolver = source('chat-session-resolver.ts')
|
|
71
|
+
const start = resolver.indexOf('async chatMessages(')
|
|
72
|
+
expect(start).toBeGreaterThan(0)
|
|
73
|
+
const end = resolver.indexOf('\n @', start)
|
|
74
|
+
return resolver.slice(start, end > start ? end : undefined)
|
|
75
|
+
}
|
|
76
|
+
const streamPath = () => {
|
|
77
|
+
const broadcast = source('chat-message/chat-message-broadcast.ts')
|
|
78
|
+
return source('chat-message/chat-message-room.ts') + broadcast.slice(broadcast.indexOf('export interface ChatRoomLookup'))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const OWNERSHIP = /owner|superUser|checkPermission|hasPrivilege|privilege/i
|
|
82
|
+
|
|
83
|
+
it('★ the rule has no input for ownership — visibility and participation, nothing else', () => {
|
|
84
|
+
expect(mayReadSession.length).toBe(2)
|
|
85
|
+
expect(mayReadSession('private', false)).toBe(false)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('★ the stored path asks the rule, and asks nothing about ownership', () => {
|
|
89
|
+
const body = storedPath()
|
|
90
|
+
expect(body).toMatch(/mayReadSession\(/)
|
|
91
|
+
expect(body).not.toMatch(OWNERSHIP)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('★ the stream asks the same rule, and asks nothing about ownership', () => {
|
|
95
|
+
const body = streamPath()
|
|
96
|
+
expect(body).toMatch(/mayReadSession\(/)
|
|
97
|
+
expect(body).not.toMatch(OWNERSHIP)
|
|
98
|
+
})
|
|
99
|
+
})
|
|
@@ -100,7 +100,7 @@ class ChatSessionInboxEntry {
|
|
|
100
100
|
@Resolver()
|
|
101
101
|
export class ChatSessionResolver {
|
|
102
102
|
@Query(() => ChatSession, { nullable: true, description: 'Get AI chat session by id.' })
|
|
103
|
-
@Directive('@privilege(category: "ai-assistant", privilege: "query")')
|
|
103
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "query", domainOwnerGranted: true)')
|
|
104
104
|
async chatSession(
|
|
105
105
|
@Arg('id') id: string,
|
|
106
106
|
@Ctx() context: ResolverContext
|
|
@@ -113,7 +113,7 @@ export class ChatSessionResolver {
|
|
|
113
113
|
description:
|
|
114
114
|
'List AI chat sessions anchored to a target (oldest first). Anchor decides the nature and the history boundary of the conversation: board = authoring, space = the site and the twins running there. Board anchor also matches legacy rows that only have boardId.'
|
|
115
115
|
})
|
|
116
|
-
@Directive('@privilege(category: "ai-assistant", privilege: "query")')
|
|
116
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "query", domainOwnerGranted: true)')
|
|
117
117
|
async chatSessionsByAnchor(
|
|
118
118
|
@Arg('anchorType') anchorType: string,
|
|
119
119
|
@Arg('anchorId') anchorId: string,
|
|
@@ -135,7 +135,7 @@ export class ChatSessionResolver {
|
|
|
135
135
|
description:
|
|
136
136
|
"List sessions anchored to a target with this viewer's unread state, most recent activity first. Supports free-text search over name and last preview, a mine-only filter, hidden inclusion, and paging. Uses the denormalized last-activity timestamp compared against the viewer's read point — no per-session aggregation."
|
|
137
137
|
})
|
|
138
|
-
@Directive('@privilege(category: "ai-assistant", privilege: "query")')
|
|
138
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "query", domainOwnerGranted: true)')
|
|
139
139
|
async chatSessionInbox(
|
|
140
140
|
@Arg('anchorType') anchorType: string,
|
|
141
141
|
@Arg('anchorId') anchorId: string,
|
|
@@ -164,7 +164,7 @@ export class ChatSessionResolver {
|
|
|
164
164
|
description:
|
|
165
165
|
'Total number of sessions matching the same filters as chatSessionInbox (before paging). Request it alongside the list so the UI can tell "end of page" from "end of data".'
|
|
166
166
|
})
|
|
167
|
-
@Directive('@privilege(category: "ai-assistant", privilege: "query")')
|
|
167
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "query", domainOwnerGranted: true)')
|
|
168
168
|
async chatSessionInboxTotal(
|
|
169
169
|
@Arg('anchorType') anchorType: string,
|
|
170
170
|
@Arg('anchorId') anchorId: string,
|
|
@@ -199,7 +199,7 @@ export class ChatSessionResolver {
|
|
|
199
199
|
description:
|
|
200
200
|
"Number of sessions on this anchor that the caller has hidden and that have had no activity since. Meant to be requested alongside chatSessionInbox in the same operation so the filter can say how many are folded away."
|
|
201
201
|
})
|
|
202
|
-
@Directive('@privilege(category: "ai-assistant", privilege: "query")')
|
|
202
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "query", domainOwnerGranted: true)')
|
|
203
203
|
async hiddenSessionCount(
|
|
204
204
|
@Arg('anchorType') anchorType: string,
|
|
205
205
|
@Arg('anchorId') anchorId: string,
|
|
@@ -270,7 +270,7 @@ export class ChatSessionResolver {
|
|
|
270
270
|
description:
|
|
271
271
|
'Delete a chat session — creator only, since a shared deliberation cannot be un-deleted from the UI. Participants who are not the creator can hide it from their own list instead (hideAISession). Soft delete; broadcast as meta activity so every list drops it.'
|
|
272
272
|
})
|
|
273
|
-
@Directive('@privilege(category: "ai-assistant", privilege: "mutation")')
|
|
273
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "mutation", domainOwnerGranted: true)')
|
|
274
274
|
async deleteAISession(@Arg('sessionId') sessionId: string, @Ctx() context: ResolverContext): Promise<boolean> {
|
|
275
275
|
const { domain, user } = context.state
|
|
276
276
|
const repo = getRepository(ChatSession)
|
|
@@ -302,7 +302,7 @@ export class ChatSessionResolver {
|
|
|
302
302
|
description:
|
|
303
303
|
'Hide (or unhide) a session from the calling user\'s own list. Per-user and reversible — the session and other participants are unaffected. Activity newer than the hide makes it reappear; the inbox filter (showHidden) reveals hidden ones.'
|
|
304
304
|
})
|
|
305
|
-
@Directive('@privilege(category: "ai-assistant", privilege: "mutation")')
|
|
305
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "mutation", domainOwnerGranted: true)')
|
|
306
306
|
async hideAISession(
|
|
307
307
|
@Arg('sessionId') sessionId: string,
|
|
308
308
|
@Ctx() context: ResolverContext,
|
|
@@ -357,7 +357,7 @@ export class ChatSessionResolver {
|
|
|
357
357
|
description:
|
|
358
358
|
"Open this conversation to everyone in the domain, or close it to its participants. Only the owner may change it. Opening exposes what is already written — the screen says so before asking. Door checks on assistant answers are unaffected (ADR-0073)."
|
|
359
359
|
})
|
|
360
|
-
@Directive('@privilege(category: "ai-assistant", privilege: "mutation")')
|
|
360
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "mutation", domainOwnerGranted: true)')
|
|
361
361
|
async setSessionVisibility(
|
|
362
362
|
@Arg('sessionId') sessionId: string,
|
|
363
363
|
@Arg('visibility') visibility: string,
|
|
@@ -394,7 +394,7 @@ export class ChatSessionResolver {
|
|
|
394
394
|
description:
|
|
395
395
|
'Rename a chat session. Any participant may rename it — the name is the topic label of a shared deliberation and the change is reversible. Broadcast as meta activity so every participant sees it without raising unread badges.'
|
|
396
396
|
})
|
|
397
|
-
@Directive('@privilege(category: "ai-assistant", privilege: "mutation")')
|
|
397
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "mutation", domainOwnerGranted: true)')
|
|
398
398
|
async renameAISession(
|
|
399
399
|
@Arg('sessionId') sessionId: string,
|
|
400
400
|
@Arg('name') name: string,
|
|
@@ -429,7 +429,7 @@ export class ChatSessionResolver {
|
|
|
429
429
|
description:
|
|
430
430
|
"Mark a session as read up to now for the current user. Idempotent. Distinct from presence (lastSeenAt) — a window can be open without being read."
|
|
431
431
|
})
|
|
432
|
-
@Directive('@privilege(category: "ai-assistant", privilege: "mutation")')
|
|
432
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "mutation", domainOwnerGranted: true)')
|
|
433
433
|
async markSessionRead(
|
|
434
434
|
@Arg('sessionId') sessionId: string,
|
|
435
435
|
@Ctx() context: ResolverContext
|
|
@@ -449,7 +449,7 @@ export class ChatSessionResolver {
|
|
|
449
449
|
description:
|
|
450
450
|
'List users in the current domain for the `@` mention popup. Anyone with the chat privilege may search here; this is separate from the admin-only users().'
|
|
451
451
|
})
|
|
452
|
-
@Directive('@privilege(category: "ai-assistant", privilege: "query")')
|
|
452
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "query", domainOwnerGranted: true)')
|
|
453
453
|
async domainUsersForMention(
|
|
454
454
|
@Arg('query', { nullable: true, description: 'Substring to match against name/email (case-insensitive). Empty → all.' }) query: string | undefined,
|
|
455
455
|
@Arg('limit', () => Int, { nullable: true, defaultValue: 50 }) limit: number,
|
|
@@ -468,7 +468,7 @@ export class ChatSessionResolver {
|
|
|
468
468
|
@Query(() => [ChatSessionParticipant], {
|
|
469
469
|
description: 'List participants of a ChatSession (members / owner).'
|
|
470
470
|
})
|
|
471
|
-
@Directive('@privilege(category: "ai-assistant", privilege: "query")')
|
|
471
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "query", domainOwnerGranted: true)')
|
|
472
472
|
async chatSessionParticipants(
|
|
473
473
|
@Arg('sessionId') sessionId: string,
|
|
474
474
|
@Ctx() context: ResolverContext
|
|
@@ -488,7 +488,7 @@ export class ChatSessionResolver {
|
|
|
488
488
|
@Mutation(() => Boolean, {
|
|
489
489
|
description: 'Presence heartbeat — bump current user\'s lastSeenAt in the session (online indicator).'
|
|
490
490
|
})
|
|
491
|
-
@Directive('@privilege(category: "ai-assistant", privilege: "mutation")')
|
|
491
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "mutation", domainOwnerGranted: true)')
|
|
492
492
|
async touchPresence(@Arg('sessionId') sessionId: string, @Ctx() context: ResolverContext): Promise<boolean> {
|
|
493
493
|
const { domain, user } = context.state
|
|
494
494
|
const repo = getRepository(ChatSessionParticipant)
|
|
@@ -501,7 +501,7 @@ export class ChatSessionResolver {
|
|
|
501
501
|
}
|
|
502
502
|
|
|
503
503
|
@Query(() => [ChatMessage], { description: 'List chat messages of a session, oldest first.' })
|
|
504
|
-
@Directive('@privilege(category: "ai-assistant", privilege: "query")')
|
|
504
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "query", domainOwnerGranted: true)')
|
|
505
505
|
async chatMessages(
|
|
506
506
|
@Arg('sessionId') sessionId: string,
|
|
507
507
|
@Arg('limit', () => Int, { nullable: true, defaultValue: 100 }) limit: number,
|
|
@@ -547,7 +547,7 @@ export class ChatSessionResolver {
|
|
|
547
547
|
description:
|
|
548
548
|
'Start (or get existing) AI chat session anchored to a target. Idempotent — returns first existing or creates one. Use anchorType board for board authoring, space for a site (its co-located twins).'
|
|
549
549
|
})
|
|
550
|
-
@Directive('@privilege(category: "ai-assistant", privilege: "mutation")')
|
|
550
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "mutation", domainOwnerGranted: true)')
|
|
551
551
|
async startAISession(
|
|
552
552
|
@Arg('anchorType') anchorType: string,
|
|
553
553
|
@Arg('anchorId') anchorId: string,
|
|
@@ -584,7 +584,7 @@ export class ChatSessionResolver {
|
|
|
584
584
|
description:
|
|
585
585
|
'Always create a new AI chat session anchored to a target (no idempotent reuse). For multi-session UX — opening another tab.'
|
|
586
586
|
})
|
|
587
|
-
@Directive('@privilege(category: "ai-assistant", privilege: "mutation")')
|
|
587
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "mutation", domainOwnerGranted: true)')
|
|
588
588
|
async createAISession(
|
|
589
589
|
@Arg('anchorType') anchorType: string,
|
|
590
590
|
@Arg('anchorId') anchorId: string,
|
|
@@ -626,7 +626,7 @@ export class ChatSessionResolver {
|
|
|
626
626
|
@Mutation(() => Boolean, {
|
|
627
627
|
description: 'Rename a ChatSession (tab label).'
|
|
628
628
|
})
|
|
629
|
-
@Directive('@privilege(category: "ai-assistant", privilege: "mutation")')
|
|
629
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "mutation", domainOwnerGranted: true)')
|
|
630
630
|
async renameChatSession(
|
|
631
631
|
@Arg('sessionId') sessionId: string,
|
|
632
632
|
@Arg('name') name: string,
|
|
@@ -645,7 +645,7 @@ export class ChatSessionResolver {
|
|
|
645
645
|
description:
|
|
646
646
|
"What the host did with a proposal the assistant made. The host calls this once per card it was asked to run; the server records it as a system row, so the panel shows it, a refreshed panel still shows it, and the model reads on its next turn that its proposal was refused instead of assuming it landed (ADR-0070)."
|
|
647
647
|
})
|
|
648
|
-
@Directive('@privilege(category: "ai-assistant", privilege: "mutation")')
|
|
648
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "mutation", domainOwnerGranted: true)')
|
|
649
649
|
async reportProposalOutcome(
|
|
650
650
|
@Arg('sessionId') sessionId: string,
|
|
651
651
|
@Arg('proposalKey') proposalKey: string,
|
|
@@ -695,7 +695,7 @@ export class ChatSessionResolver {
|
|
|
695
695
|
description:
|
|
696
696
|
"Say something to the people in this session. The turn is stored as the speaker's, addressed to the room, and the assistant is not called — it reads the line as context the next time someone asks it something. Returns the stored message id, or null when there was nothing to say (ADR-0070)."
|
|
697
697
|
})
|
|
698
|
-
@Directive('@privilege(category: "ai-assistant", privilege: "mutation")')
|
|
698
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "mutation", domainOwnerGranted: true)')
|
|
699
699
|
async sayToRoom(
|
|
700
700
|
@Arg('sessionId') sessionId: string,
|
|
701
701
|
@Arg('content') content: string,
|
|
@@ -763,7 +763,7 @@ export class ChatSessionResolver {
|
|
|
763
763
|
description:
|
|
764
764
|
'Append a system note to a session — context that did not come from a participant typing (e.g. a conversation carried over from another surface, an external state change). The AI sees it as part of the shared history.'
|
|
765
765
|
})
|
|
766
|
-
@Directive('@privilege(category: "ai-assistant", privilege: "mutation")')
|
|
766
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "mutation", domainOwnerGranted: true)')
|
|
767
767
|
async appendSessionNote(
|
|
768
768
|
@Arg('sessionId') sessionId: string,
|
|
769
769
|
@Arg('content') content: string,
|
|
@@ -813,7 +813,7 @@ export class ChatSessionResolver {
|
|
|
813
813
|
description:
|
|
814
814
|
"Carry a conversation from another surface into this session as its own turns, preserving order. A person's turns are stored as theirs, addressed to the room. An assistant turn is stored as the assistant's only when it names its original (sourceSessionId, sourceMessageId) and the server can read it; otherwise it is kept as a quotation on a system row — roles are the server's to write (ADR-0070)."
|
|
815
815
|
})
|
|
816
|
-
@Directive('@privilege(category: "ai-assistant", privilege: "mutation")')
|
|
816
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "mutation", domainOwnerGranted: true)')
|
|
817
817
|
async appendSessionTurns(
|
|
818
818
|
@Arg('sessionId') sessionId: string,
|
|
819
819
|
@Arg('turns', () => GraphQLJSON) turns: { role?: string; content?: string }[],
|
package/test/open-doors.test.ts
CHANGED
|
@@ -108,6 +108,22 @@ describe('every open door has a written reason', () => {
|
|
|
108
108
|
})
|
|
109
109
|
})
|
|
110
110
|
|
|
111
|
+
describe('the domain owner may call every door (ruled 2026-09-24)', () => {
|
|
112
|
+
/*
|
|
113
|
+
* The conversation doors did not admit domain owners, and warehouse has no role that grants
|
|
114
|
+
* ai-assistant — so on :3500 nobody could open the AI dock, the owner included. Every business door
|
|
115
|
+
* of the apps admits the owner; these now do too. Calling a door is not reading a conversation:
|
|
116
|
+
* private ones stay with their participants (`session-visibility.test.ts`).
|
|
117
|
+
*/
|
|
118
|
+
it.each(['Query', 'Mutation'] as const)('%s — every declared door admits the owner', typeName => {
|
|
119
|
+
const notForOwners = fieldsOf(typeName).filter(field => {
|
|
120
|
+
const gate: any = privilegeOfField(typeName, field)
|
|
121
|
+
return gate.known && gate.gate && gate.gate.owner !== true
|
|
122
|
+
})
|
|
123
|
+
expect(notForOwners).toEqual([])
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
111
127
|
describe('what the walk itself has to be true for', () => {
|
|
112
128
|
it('reaches the whole schema, not a handful of fields', () => {
|
|
113
129
|
/* If the resolver list shrank to nothing, the lists above would pass by having nothing to look at. */
|